Skip to content
Commits on Source (43)
This diff is collapsed.
......@@ -227,6 +227,7 @@ var DBusClient = class AppIndicatorsDBusClient {
// property requests are queued
this._propertiesRequestedFor = new Set(/* ids */);
this._layoutUpdated = false;
Util.connectSmart(this._proxy, 'notify::g-name-owner', this, () => {
if (this.isReady)
this._requestLayoutUpdate();
......@@ -234,7 +235,7 @@ var DBusClient = class AppIndicatorsDBusClient {
}
get isReady() {
return !!this._proxy.g_name_owner;
return this._layoutUpdated && !!this._proxy.g_name_owner;
}
getRoot() {
......@@ -321,14 +322,24 @@ var DBusClient = class AppIndicatorsDBusClient {
this._flagLayoutUpdateInProgress = true;
}
_updateLayoutState(state) {
const wasReady = this.isReady;
this._layoutUpdated = state;
if (this.isReady !== wasReady)
this.emit('ready-changed');
}
_endLayoutUpdate(result, error) {
if (error) {
if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
Util.Logger.warn(`While reading menu layout on proxy ${this._proxy.g_name_owner}: ${error}`);
this._updateLayoutState(false);
}
return;
}
let [revision_, root] = result;
this._updateLayoutState(true);
this._doLayoutUpdate(root);
this._gcItems();
......@@ -739,6 +750,9 @@ var Client = class AppIndicatorsClient {
this._rootMenu = null; // the shell menu
this._rootItem = null; // the DbusMenuItem for the root
this.indicator = indicator;
Util.connectSmart(this._client, 'ready-changed', this,
() => this.emit('ready-changed'));
}
get isReady() {
......
......@@ -77,6 +77,9 @@ class AppIndicatorsIndicatorBaseStatusIcon extends PanelMenu.Button {
Util.connectSmart(settings, 'changed::icon-opacity', this, this._updateOpacity);
this.connect('notify::hover', () => this._onHoverChanged());
if (!super._onDestroy)
this.connect('destroy', () => this._onDestroy());
this._setIconActor(iconActor);
this._showIfReady();
}
......@@ -85,17 +88,28 @@ class AppIndicatorsIndicatorBaseStatusIcon extends PanelMenu.Button {
if (!(icon instanceof Clutter.Actor))
throw new Error(`${icon} is not a valid actor`);
if (!this._icon) {
const settings = SettingsManager.getDefaultGSettings();
Util.connectSmart(settings, 'changed::icon-saturation', this, this._updateSaturation);
Util.connectSmart(settings, 'changed::icon-brightness', this, this._updateBrightnessContrast);
Util.connectSmart(settings, 'changed::icon-contrast', this, this._updateBrightnessContrast);
} else if (this._icon !== icon) {
if (this._icon && this._icon !== icon)
this._icon.destroy();
}
this._icon = icon;
this._updateEffects();
this._monitorIconEffects();
if (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() {
......@@ -144,6 +158,32 @@ class AppIndicatorsIndicatorBaseStatusIcon extends PanelMenu.Button {
}
}
_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;
} 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);
}
}
_updateSaturation() {
const settings = SettingsManager.getDefaultGSettings();
const desaturationValue = settings.get_double('icon-saturation');
......@@ -209,17 +249,23 @@ class AppIndicatorsIndicatorStatusIcon extends BaseStatusIcon {
});
Util.connectSmart(this._indicator, 'accessible-name', this, () =>
this.set_accessible_name(this._indicator.accessibleName));
Util.connectSmart(this._indicator, 'destroy', this, () => this.destroy());
this.connect('destroy', () => {
if (this._menuClient) {
this._menuClient.destroy();
this._menuClient = null;
}
});
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;
}
......@@ -253,20 +299,34 @@ class AppIndicatorsIndicatorStatusIcon extends BaseStatusIcon {
}
_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._indicator.menuPath) {
if (this.visible && this._indicator.menuPath) {
this._menuClient = new DBusMenu.Client(this._indicator.busName,
this._indicator.menuPath, this._indicator);
this._menuClient.attachToMenu(this.menu);
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();
});
}
}
......@@ -426,23 +486,22 @@ class AppIndicatorsIndicatorTrayIcon extends BaseStatusIcon {
const settings = SettingsManager.getDefaultGSettings();
Util.connectSmart(settings, 'changed::icon-size', this, this._updateIconSize);
// eslint-disable-next-line no-undef
const themeContext = St.ThemeContext.get_for_stage(global.stage);
Util.connectSmart(themeContext, 'notify::scale-factor', this, () =>
this._updateIconSize());
this._updateIconSize();
}
this.connect('destroy', () => {
Util.Logger.debug(`Destroying legacy tray icon ${this.uniqueId}`);
this._icon.destroy();
this._icon = null;
_onDestroy() {
Util.Logger.debug(`Destroying legacy tray icon ${this.uniqueId}`);
if (this._waitDoubleClickId) {
GLib.source_remove(this._waitDoubleClickId);
this._waitDoubleClickId = 0;
}
});
if (this._waitDoubleClickId) {
GLib.source_remove(this._waitDoubleClickId);
this._waitDoubleClickId = 0;
}
super._onDestroy();
}
isReady() {
......@@ -524,8 +583,7 @@ class AppIndicatorsIndicatorTrayIcon extends BaseStatusIcon {
_updateIconSize() {
const settings = SettingsManager.getDefaultGSettings();
// eslint-disable-next-line no-undef
const { scale_factor: scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
let iconSize = settings.get_int('icon-size');
if (iconSize <= 0)
......
project('gnome-shell-ubuntu-appindicators',
version : '44',
version : '46',
meson_version : '>= 0.53',
license: 'GPL2',
)
......
......@@ -83,14 +83,8 @@ var StatusNotifierWatcher = class AppIndicatorsStatusNotifierWatcher {
this._watchDog.nameAcquired = false;
}
// create a unique index for the _items dictionary
_getItemId(busName, objPath) {
return busName + objPath;
}
async _registerItem(service, busName, objPath) {
let id = this._getItemId(busName, objPath);
const id = Util.indicatorId(service, busName, objPath);
if (this._items.has(id)) {
Util.Logger.warn(`Item ${id} is already registered`);
......@@ -102,13 +96,19 @@ var StatusNotifierWatcher = class AppIndicatorsStatusNotifierWatcher {
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) {
await new PromiseUtils.TimeoutPromise(500,
GLib.PRIORITY_DEFAULT, this._cancellable);
if (!indicator.hasNameOwner)
this._itemVanished(id);
try {
await new PromiseUtils.TimeoutPromise(500,
GLib.PRIORITY_DEFAULT, this._cancellable);
if (!indicator.hasNameOwner)
indicator.destroy();
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
logError(e);
}
}
});
......@@ -116,7 +116,6 @@ var StatusNotifierWatcher = class AppIndicatorsStatusNotifierWatcher {
await Util.waitForStartupCompletion(indicator.cancellable);
const statusIcon = new IndicatorStatusIcon.IndicatorStatusIcon(indicator);
IndicatorStatusIcon.addIconToPanel(statusIcon);
indicator.connect('destroy', () => statusIcon.destroy());
this._dbusImpl.emit_signal('StatusNotifierItemRegistered',
GLib.Variant.new('(s)', [indicator.uniqueId]));
......@@ -129,8 +128,8 @@ var StatusNotifierWatcher = class AppIndicatorsStatusNotifierWatcher {
}
}
_ensureItemRegistered(service, busName, objPath) {
let id = this._getItemId(busName, objPath);
async _ensureItemRegistered(service, busName, objPath) {
const id = Util.indicatorId(service, busName, objPath);
let item = this._items.get(id);
if (item) {
......@@ -140,7 +139,7 @@ var StatusNotifierWatcher = class AppIndicatorsStatusNotifierWatcher {
return;
}
this._registerItem(service, busName, objPath);
await this._registerItem(service, busName, objPath);
}
async _seekStatusNotifierItems() {
......@@ -155,18 +154,23 @@ var StatusNotifierWatcher = class AppIndicatorsStatusNotifierWatcher {
const uniqueNames = await Util.getBusNames(bus, cancellable);
const introspectName = async name => {
const nodes = await Util.introspectBusObject(bus, name, cancellable);
const services = [...uniqueNames.get(name)];
nodes.forEach(({ nodeInfo, path }) => {
if (Util.dbusNodeImplementsInterfaces(nodeInfo, ['org.kde.StatusNotifierItem'])) {
Util.Logger.debug(`Found ${name} at ${path} implementing StatusNotifierItem iface`);
const id = this._getItemId(name, path);
if (!this._items.has(id)) {
const ids = services.map(s => Util.indicatorId(s, name, path));
if (ids.every(id => !this._items.has(id))) {
const service = services.find(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}`);
this._registerItem(path, name, path);
this._registerItem(service, name, path);
}
}
});
};
await Promise.allSettled([...uniqueNames].map(n => introspectName(n)));
await Promise.allSettled([...uniqueNames.keys()].map(n => introspectName(n)));
}
async RegisterStatusNotifierItemAsync(params, invocation) {
......@@ -199,22 +203,20 @@ var StatusNotifierWatcher = class AppIndicatorsStatusNotifierWatcher {
return;
}
this._ensureItemRegistered(service, busName, objPath);
invocation.return_value(null);
}
_itemVanished(id) {
// FIXME: this is useless if the path name disappears while the bus stays alive (not unheard of)
if (this._items.has(id))
this._remove(id);
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);
}
}
_remove(id) {
const indicator = this._items.get(id);
_onIndicatorDestroyed(indicator) {
const { uniqueId } = indicator;
indicator.destroy();
this._items.delete(id);
this._items.delete(uniqueId);
try {
this._dbusImpl.emit_signal('StatusNotifierItemUnregistered',
......@@ -256,7 +258,7 @@ var StatusNotifierWatcher = class AppIndicatorsStatusNotifierWatcher {
// 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)
Array.from(this._items.keys()).forEach(i => this._remove(i));
this._items.forEach(indicator => indicator.destroy());
this._cancellable.cancel();
try {
......@@ -273,6 +275,11 @@ var StatusNotifierWatcher = class AppIndicatorsStatusNotifierWatcher {
Util.Logger.warn(`Failed to unexport watcher object: ${e}`);
}
AppIndicator.AppIndicator.destroy();
this._dbusImpl.run_dispose();
delete this._dbusImpl;
delete this._items;
this._isDestroyed = true;
}
......
......@@ -16,8 +16,9 @@
/* exported refreshPropertyOnProxy, getUniqueBusName, getBusNames,
introspectBusObject, dbusNodeImplementsInterfaces, waitForStartupCompletion,
connectSmart, versionCheck, getDefaultTheme, tryCleanupOldIndicators,
getProcessName, ensureProxyAsyncMethod, BUS_ADDRESS_REGEX */
connectSmart, disconnectSmart, versionCheck, getDefaultTheme,
getProcessName, ensureProxyAsyncMethod, queueProxyPropertyUpdate,
getProxyProperty, indicatorId, tryCleanupOldIndicators */
const ByteArray = imports.byteArray;
const Gio = imports.gi.Gio;
......@@ -43,66 +44,97 @@ PromiseUtils._promisify(Gio.DBusConnection.prototype, 'call', 'call_finish');
PromiseUtils._promisify(Gio._LocalFilePrototype, 'read', 'read_finish');
PromiseUtils._promisify(Gio.InputStream.prototype, 'read_bytes_async', 'read_bytes_finish');
async function refreshPropertyOnProxy(proxy, propertyName, params) {
if (!proxy._proxyCancellables)
proxy._proxyCancellables = new Map();
function indicatorId(service, busName, objectPath) {
if (service && service !== busName && service.match(BUS_ADDRESS_REGEX))
return service;
return `${busName}@${objectPath}`;
}
function getProxyProperty(proxy, propertyName, cancellable) {
return proxy.g_connection.call(proxy.g_name,
proxy.g_object_path, 'org.freedesktop.DBus.Properties', 'Get',
GLib.Variant.new('(ss)', [proxy.g_interface_name, propertyName]),
GLib.VariantType.new('(v)'), Gio.DBusCallFlags.NONE, -1,
cancellable);
}
async function refreshPropertyOnProxy(proxy, propertyName, params) {
params = Params.parse(params, {
skipEqualityCheck: false,
});
let cancellable = cancelRefreshPropertyOnProxy(proxy, {
const cancellable = cancelRefreshPropertyOnProxy(proxy, {
propertyName,
addNew: true,
});
try {
const [valueVariant] = (await proxy.g_connection.call(proxy.g_name,
proxy.g_object_path, 'org.freedesktop.DBus.Properties', 'Get',
GLib.Variant.new('(ss)', [proxy.g_interface_name, propertyName]),
GLib.VariantType.new('(v)'), Gio.DBusCallFlags.NONE, -1,
cancellable)).deep_unpack();
const [valueVariant] = (await getProxyProperty(
proxy, propertyName, cancellable)).deep_unpack();
proxy._proxyCancellables.delete(propertyName);
if (!params.skipEqualityCheck &&
proxy.get_cached_property(propertyName).equal(valueVariant))
return;
proxy.set_cached_property(propertyName, valueVariant);
// synthesize a batched property changed event
if (!proxy._proxyChangedProperties)
proxy._proxyChangedProperties = {};
proxy._proxyChangedProperties[propertyName] = valueVariant;
if (!proxy._proxyPropertiesEmit || !proxy._proxyPropertiesEmit.pending()) {
proxy._proxyPropertiesEmit = new PromiseUtils.TimeoutPromise(16,
GLib.PRIORITY_DEFAULT_IDLE, cancellable);
await proxy._proxyPropertiesEmit;
proxy.emit('g-properties-changed', GLib.Variant.new('a{sv}',
proxy._proxyChangedProperties), []);
delete proxy._proxyChangedProperties;
}
await queueProxyPropertyUpdate(proxy, propertyName, valueVariant,
{ ...params, cancellable });
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
// the property may not even exist, silently ignore it
Logger.debug(`While refreshing property ${propertyName}: ${e}`);
proxy.set_cached_property(propertyName, null);
proxy._proxyCancellables.delete(propertyName);
delete proxy._proxyChangedProperties[propertyName];
if (proxy._proxyChangedProperties)
delete proxy._proxyChangedProperties[propertyName];
throw e;
}
}
}
function cancelRefreshPropertyOnProxy(proxy, params) {
if (!proxy._proxyCancellables)
return null;
async function queueProxyPropertyUpdate(proxy, propertyName, value, params) {
params = Params.parse(params, {
skipEqualityCheck: false,
cancellable: null,
});
if (!params.skipEqualityCheck &&
value.equal(proxy.get_cached_property(propertyName)))
return;
proxy.set_cached_property(propertyName, value);
// synthesize a batched property changed event
if (!proxy._proxyChangedProperties)
proxy._proxyChangedProperties = {};
proxy._proxyChangedProperties[propertyName] = value;
if (!proxy._proxyPropertiesEmit || !proxy._proxyPropertiesEmit.pending()) {
if (!params.cancellable) {
params.cancellable = cancelRefreshPropertyOnProxy(proxy, {
propertyName,
addNew: true,
});
}
proxy._proxyPropertiesEmit = new PromiseUtils.TimeoutPromise(16,
GLib.PRIORITY_DEFAULT_IDLE, params.cancellable);
await proxy._proxyPropertiesEmit;
proxy.emit('g-properties-changed', GLib.Variant.new('a{sv}',
proxy._proxyChangedProperties), []);
delete proxy._proxyChangedProperties;
}
}
function cancelRefreshPropertyOnProxy(proxy, params) {
params = Params.parse(params, {
propertyName: undefined,
addNew: false,
});
if (!proxy._proxyCancellables) {
if (!params.addNew)
return null;
proxy._proxyCancellables = new Map();
}
if (params.propertyName !== undefined) {
let cancellable = proxy._proxyCancellables.get(params.propertyName);
if (cancellable) {
......@@ -149,16 +181,22 @@ async function getBusNames(bus, cancellable) {
'ListNames', null, new GLib.VariantType('(as)'), Gio.DBusCallFlags.NONE,
-1, cancellable)).deep_unpack();
const uniqueNames = new Set();
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')
uniqueNames.add(result.value);
else if (!result.reason.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
if (result.status === 'fulfilled') {
let namesForBus = uniqueNames.get(result.value);
if (!namesForBus) {
namesForBus = new Set();
uniqueNames.set(result.value, namesForBus);
}
namesForBus.add(result.value !== names[i] ? names[i] : null);
} 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;
......@@ -273,13 +311,16 @@ Signals.addSignalMethods(NameWatcher.prototype);
function connectSmart3A(src, signal, handler) {
let id = src.connect(signal, handler);
let destroyId = 0;
if (src.connect && (!(src instanceof GObject.Object) || GObject.signal_lookup('destroy', src))) {
let destroyId = src.connect('destroy', () => {
destroyId = src.connect('destroy', () => {
src.disconnect(id);
src.disconnect(destroyId);
});
}
return [id, destroyId];
}
function connectSmart4A(src, signal, target, method) {
......@@ -302,6 +343,8 @@ function connectSmart4A(src, signal, target, method) {
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
......@@ -321,6 +364,32 @@ function connectSmart(...args) {
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);
}
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');
}
function getDefaultTheme() {
if (Gdk.Screen.get_default()) {
const defaultTheme = Gtk.IconTheme.get_default();
......@@ -358,7 +427,10 @@ var Logger = class AppIndicatorsLogger {
return;
}
let domain = Extension.metadata.name;
Logger._init(Extension.metadata.name);
if (!Logger._levels.includes(logLevel))
return;
let fields = {
'SYSLOG_IDENTIFIER': Extension.metadata.uuid,
'MESSAGE': `${message}`,
......@@ -385,7 +457,23 @@ var Logger = class AppIndicatorsLogger {
break;
}
GLib.log_structured(domain, logLevel, Object.assign(fields, extraFields));
GLib.log_structured(Logger._domain, logLevel, Object.assign(fields, extraFields));
}
static _init(domain) {
if (Logger._domain)
return;
const allLevels = Object.values(GLib.LogLevelFlags);
const domains = GLib.getenv('G_MESSAGES_DEBUG');
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 debug(message) {
......