Skip to content
Commits on Source (43)
......@@ -11,7 +11,6 @@ This extension integrates Ubuntu AppIndicators and KStatusNotifierItems (KDE's b
## Missing features
* Tooltips: Not implemented in `libappindicator` nor in Unity and I've yet to see any indicator using it for anything relevant (KDE ones maybe?). Also, the GNOME designers decided not to have tooltips in the shell and I'd like to honor that decision.
* Oversized icons like the ones used by `indicator-multiload` are unsupported. They will be shrunk to normal size.
## Known issues
* ClassicMenu Indicator takes ages to load and has been reported to freeze the shell forever. This is probably caused by the insane amount of embedded PNG icons. Try at your own risk.
......
......@@ -29,10 +29,12 @@ const Extension = imports.misc.extensionUtils.getCurrentExtension();
const Signals = imports.signals
const DBusMenu = Extension.imports.dbusMenu;
var IconCache = Extension.imports.iconCache;
const IconCache = Extension.imports.iconCache;
const Util = Extension.imports.util;
const Interfaces = Extension.imports.interfaces;
const MAX_UPDATE_FREQUENCY = 100; // In ms
const SNICategory = {
APPLICATION: 'ApplicationStatus',
COMMUNICATIONS: 'Communications',
......@@ -61,6 +63,7 @@ var AppIndicator = class AppIndicators_AppIndicator {
constructor(bus_name, object) {
this.busName = bus_name
this._uniqueId = bus_name + object
this._accumuledSignals = new Set();
let interface_info = Gio.DBusInterfaceInfo.new_for_xml(Interfaces.StatusNotifierItem)
......@@ -91,13 +94,8 @@ var AppIndicator = class AppIndicators_AppIndicator {
}
}))
this._proxyPropertyList = interface_info.properties.map((propinfo) => { return propinfo.name })
this._addExtraProperty('XAyatanaLabel');
this._addExtraProperty('XAyatanaLabelGuide');
this._addExtraProperty('XAyatanaOrderingIndex');
Util.connectSmart(this._proxy, 'g-properties-changed', this, '_onPropertiesChanged')
Util.connectSmart(this._proxy, 'g-signal', this, '_translateNewSignals')
Util.connectSmart(this._proxy, 'g-signal', this, this._onProxySignal)
Util.connectSmart(this._proxy, 'notify::g-name-owner', this, '_nameOwnerChanged')
}
......@@ -109,6 +107,7 @@ var AppIndicator = class AppIndicators_AppIndicator {
isReady = true;
this.isReady = isReady;
this._setupProxyPropertyList();
if (this.isReady && !wasReady) {
if (this._delayCheck) {
......@@ -129,36 +128,68 @@ var AppIndicator = class AppIndicators_AppIndicator {
}
_addExtraProperty(name) {
let propertyProps = { configurable: false, enumerable: true };
if (this._proxyPropertyList.includes(name))
return;
propertyProps.get = () => {
let v = this._proxy.get_cached_property(name);
return v ? v.deep_unpack() : null
};
if (!(name in this._proxy)) {
Object.defineProperty(this._proxy, name, {
configurable: false,
enumerable: true,
get: () => {
const v = this._proxy.get_cached_property(name);
return v ? v.deep_unpack() : null;
}
});
}
Object.defineProperty(this._proxy, name, propertyProps);
this._proxyPropertyList.push(name);
}
_setupProxyPropertyList() {
let interfaceProps = this._proxy.g_interface_info.properties;
this._proxyPropertyList =
(this._proxy.get_cached_property_names() || []).filter(p =>
interfaceProps.some(propinfo => propinfo.name == p));
if (this._proxyPropertyList.length) {
this._addExtraProperty('XAyatanaLabel');
this._addExtraProperty('XAyatanaLabelGuide');
this._addExtraProperty('XAyatanaOrderingIndex');
}
}
// The Author of the spec didn't like the PropertiesChanged signal, so he invented his own
_translateNewSignals(proxy, sender, signal, params) {
_translateNewSignals(signal) {
let prop = null;
if (signal.substr(0, 3) == 'New')
if (signal.startsWith('New'))
prop = signal.substr(3)
else if (signal.substr(0, 11) == 'XAyatanaNew')
else if (signal.startsWith('XAyatanaNew'))
prop = 'XAyatana' + signal.substr(11)
if (prop) {
if (this._proxyPropertyList.indexOf(prop) > -1)
Util.refreshPropertyOnProxy(this._proxy, prop)
if (!prop)
return;
if (this._proxyPropertyList.indexOf(prop + 'Pixmap') > -1)
Util.refreshPropertyOnProxy(this._proxy, prop + 'Pixmap')
[prop, `${prop}Name`, `${prop}Pixmap`].filter(p =>
this._proxyPropertyList.includes(p)).forEach(p =>
Util.refreshPropertyOnProxy(this._proxy, p, {
skipEqualtyCheck: p.endsWith('Pixmap'),
})
);
}
if (this._proxyPropertyList.indexOf(prop + 'Name') > -1)
Util.refreshPropertyOnProxy(this._proxy, prop + 'Name')
}
_onProxySignal(_proxy, _sender, signal, _params) {
this._accumuledSignals.add(signal);
if (this._signalsAccumulatorId)
return;
this._signalsAccumulatorId = GLib.timeout_add(
GLib.PRIORITY_DEFAULT_IDLE, MAX_UPDATE_FREQUENCY, () => {
this._accumuledSignals.forEach((s) => this._translateNewSignals(s));
this._accumuledSignals.clear();
delete this._signalsAccumulatorId;
});
}
//public property getters
......@@ -209,39 +240,45 @@ var AppIndicator = class AppIndicators_AppIndicator {
}
_onPropertiesChanged(proxy, changed, invalidated) {
let props = Object.keys(changed.deep_unpack())
let props = Object.keys(changed.unpack());
let signalsToEmit = new Set();
props.forEach((property) => {
// some property changes require updates on our part,
// a few need to be passed down to the displaying code
// all these can mean that the icon has to be changed
if (property == 'Status' || property.substr(0, 4) == 'Icon' || property.substr(0, 13) == 'AttentionIcon')
this.emit('icon')
if (property == 'Status' ||
property.startsWith('Icon') ||
property.startsWith('AttentionIcon')) {
signalsToEmit.add('icon')
}
// same for overlays
if (property.substr(0, 11) == 'OverlayIcon')
this.emit('overlay-icon')
if (property.startsWith('OverlayIcon'))
signalsToEmit.add('overlay-icon')
// this may make all of our icons invalid
if (property == 'IconThemePath') {
this.emit('icon')
this.emit('overlay-icon')
signalsToEmit.add('icon')
signalsToEmit.add('overlay-icon')
}
// the label will be handled elsewhere
if (property == 'XAyatanaLabel')
this.emit('label')
signalsToEmit.add('label')
if (property == 'Menu') {
if (!this._checkIfReady() && this.isReady)
this.emit('menu')
signalsToEmit.add('menu')
}
// status updates may cause the indicator to be hidden
if (property == 'Status')
this.emit('status')
}, this);
signalsToEmit.add('status')
});
signalsToEmit.forEach(s => this.emit(s));
}
reset() {
......@@ -257,6 +294,11 @@ var AppIndicator = class AppIndicators_AppIndicator {
delete this._cancellable;
delete this._proxy
if (this._signalsAccumulatorId) {
GLib.Source.remove(this._signalsAccumulatorId);
delete this._signalsAccumulatorId;
}
if (this._delayCheck) {
GLib.Source.remove(this._delayCheck);
delete this._delayCheck;
......@@ -297,6 +339,7 @@ class AppIndicators_IconActor extends St.Icon {
this.name = this.constructor.name;
this.add_style_class_name('appindicator-icon');
this.set_style('padding:0');
let themeContext = St.ThemeContext.get_for_stage(global.stage);
this.height = icon_size * themeContext.scale_factor;
......@@ -330,6 +373,11 @@ class AppIndicators_IconActor extends St.Icon {
this.connect('destroy', () => {
this._iconCache.destroy();
this._cancellable.cancel();
if (this._callbackIdle) {
GLib.source_remove(this._callbackIdle);
delete this._callbackIdle;
}
});
}
......@@ -340,10 +388,6 @@ class AppIndicators_IconActor extends St.Icon {
// Will look the icon up in the cache, if it's found
// it will return it. Otherwise, it will create it and cache it.
// The .inUse flag will be set to true. So when you don't need
// the returned icon anymore, make sure to check the .inUse property
// and set it to false if needed so that it can be picked up by the garbage
// collector.
_cacheOrCreateIconByName(iconSize, iconName, themePath, callback) {
let {scale_factor} = St.ThemeContext.get_for_stage(global.stage);
let id = `${iconName}@${iconSize * scale_factor}${themePath || ''}`;
......@@ -367,10 +411,8 @@ class AppIndicators_IconActor extends St.Icon {
let path = this._getIconInfo(iconName, themePath, iconSize, scale_factor);
this._createIconByName(path, (gicon) => {
this._loadingIcons.delete(id);
if (gicon) {
gicon.inUse = true;
this._iconCache.add(id, gicon);
}
if (gicon)
gicon = this._iconCache.add(id, gicon);
callback(gicon);
});
}
......@@ -403,6 +445,21 @@ class AppIndicators_IconActor extends St.Icon {
}
_createIconByName(path, callback) {
if (!path) {
if (this._callbackIdle)
return;
this._callbackIdle = GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
delete this._callbackIdle;
callback(null);
return false;
});
return;
} else if (this._callbackIdle) {
GLib.source_remove(this._callbackIdle);
delete this._callbackIdle;
}
GdkPixbuf.Pixbuf.get_file_info_async(path, this._cancellable, (_p, res) => {
try {
let [format, width, height] = GdkPixbuf.Pixbuf.get_file_info_finish(res);
......@@ -464,7 +521,8 @@ class AppIndicators_IconActor extends St.Icon {
Gtk.IconLookupFlags.GENERIC_FALLBACK);
// no icon? that's bad!
if (iconInfo === null) {
Util.Logger.warn(`${this._indicator.id}, Impossible to lookup icon for '${name}'`);
let msg = `${this._indicator.id}, Impossible to lookup icon for '${name}' in`;
Util.Logger.warn(`${msg} ${themePath ? `path ${themePath}` : 'default theme'}`);
} else { // we have an icon
// get the icon path
path = iconInfo.get_filename();
......@@ -529,10 +587,17 @@ class AppIndicators_IconActor extends St.Icon {
}
}
// The .inUse 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 .inUse property
// 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) {
this.gicon = new Gio.EmblemedIcon({ gicon });
if (!(gicon instanceof GdkPixbuf.Pixbuf))
gicon.inUse = (this.gicon.get_icon() == gicon);
} else {
this.gicon = null;
Util.Logger.critical(`unable to update icon for ${this._indicator.id}`);
......@@ -540,6 +605,9 @@ class AppIndicators_IconActor extends St.Icon {
} else {
if (gicon) {
this._emblem = new Gio.Emblem({ icon: gicon });
if (!(gicon instanceof GdkPixbuf.Pixbuf))
gicon.inUse = true;
} else {
this._emblem = null;
Util.Logger.debug(`unable to update icon emblem for ${this._indicator.id}`);
......@@ -547,7 +615,7 @@ class AppIndicators_IconActor extends St.Icon {
}
if (this.gicon) {
if (!this._emblem || !this.gicon.get_emblems().includes(this._emblem)) {
if (!this.gicon.get_emblems().some(e => e.equal(this._emblem))) {
this.gicon.clear_emblems();
if (this._emblem)
this.gicon.add_emblem(this._emblem);
......@@ -587,8 +655,8 @@ class AppIndicators_IconActor extends St.Icon {
// updates the base icon
_updateIcon() {
if (this.gicon) {
let { gicon } = this;
if (this.gicon instanceof Gio.EmblemedIcon) {
let { gicon } = this.gicon;
if (gicon.inUse)
gicon.inUse = false
......@@ -602,12 +670,11 @@ class AppIndicators_IconActor extends St.Icon {
}
_updateOverlayIcon() {
// remove old icon
if (this.gicon && this.gicon.get_emblems().length) {
let [emblem] = this.gicon.get_emblems();
if (this._emblem) {
let { icon } = this._emblem;
if (emblem.inUse)
emblem.inUse = false
if (icon.inUse)
icon.inUse = false;
}
// KDE hardcodes the overlay icon size to 10px (normal icon size 16px)
......
......@@ -206,7 +206,12 @@ const BusClientProxy = Gio.DBusProxy.makeProxyWrapper(DBusInterfaces.DBusMenu);
var DBusClient = class AppIndicators_DBusClient {
constructor(busName, busPath) {
this._proxy = new BusClientProxy(Gio.DBus.session, busName, busPath, this._clientReady.bind(this))
this._cancellable = new Gio.Cancellable();
this._proxy = new BusClientProxy(Gio.DBus.session,
busName,
busPath,
this._clientReady.bind(this),
this._cancellable)
this._items = { 0: new DbusMenuItem(this, 0, { 'children-display': GLib.Variant.new_string('submenu') }, []) }
// will be set to true if a layout update is requested while one is already in progress
......@@ -240,8 +245,10 @@ var DBusClient = class AppIndicators_DBusClient {
_requestProperties(id) {
// if we don't have any requests queued, we'll need to add one
if (this._propertiesRequestedFor.length < 1)
GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, this._beginRequestProperties.bind(this))
if (!this._propertiesRequestId) {
this._propertiesRequestId = GLib.idle_add(
GLib.PRIORITY_DEFAULT_IDLE, () => this._beginRequestProperties())
}
if (this._propertiesRequestedFor.filter((e) => { return e === id }).length == 0)
this._propertiesRequestedFor.push(id)
......@@ -249,16 +256,21 @@ var DBusClient = class AppIndicators_DBusClient {
}
_beginRequestProperties() {
this._proxy.GetGroupPropertiesRemote(this._propertiesRequestedFor, [], this._endRequestProperties.bind(this))
this._proxy.GetGroupPropertiesRemote(this._propertiesRequestedFor,
[],
this._cancellable,
this._endRequestProperties.bind(this))
this._propertiesRequestedFor = []
delete this._propertiesRequestId;
return false
}
_endRequestProperties(result, error) {
if (error) {
Util.Logger.warn("Could not retrieve properties: "+error)
if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
Util.Logger.warn(`Could not retrieve properties: ${error}`);
return
}
......@@ -294,7 +306,10 @@ var DBusClient = class AppIndicators_DBusClient {
_beginLayoutUpdate() {
// 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
this._proxy.GetLayoutRemote(0, -1, [ 'type', 'children-display' ], this._endLayoutUpdate.bind(this))
this._proxy.GetLayoutRemote(0, -1,
[ 'type', 'children-display' ],
this._cancellable,
this._endLayoutUpdate.bind(this))
this._flagLayoutUpdateRequired = false
this._flagLayoutUpdateInProgress = true
......@@ -302,7 +317,8 @@ var DBusClient = class AppIndicators_DBusClient {
_endLayoutUpdate(result, error) {
if (error) {
Util.Logger.warn("While reading menu layout on proxy '"+this._proxy.g_name_owner+": "+error)
if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
Util.Logger.warn(`While reading menu layout on proxy ${this._proxy.g_name_owner}: ${error}`);
return
}
......@@ -366,7 +382,8 @@ var DBusClient = class AppIndicators_DBusClient {
_clientReady(result, error) {
if (error) {
Util.Logger.warn("Could not initialize menu proxy: "+error)
if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
Util.Logger.warn(`Could not initialize menu proxy: ${error}`);
return;
}
......@@ -411,7 +428,8 @@ var DBusClient = class AppIndicators_DBusClient {
if (!this._proxy)
return
this._proxy.EventRemote(id, event, params, timestamp, function(result, error) { /* we don't care */ })
this._proxy.EventRemote(id, event, params, timestamp, this._cancellable,
() => { /* we don't care */ })
}
_onLayoutUpdated() {
......@@ -439,6 +457,12 @@ var DBusClient = class AppIndicators_DBusClient {
destroy() {
this.emit('destroy')
if (this._propertiesRequestId) {
GLib.Source.remove(this._propertiesRequestId);
delete this._propertiesRequestId;
}
this._cancellable.cancel();
Signals._disconnectAll.apply(this._proxy)
this._proxy = null
......
gnome-shell-extension-appindicator (33.1-0ubuntu0.20.04.1) focal; urgency=medium
[ Marco Trevisan (Treviño) ]
* New upstream stable release
* debian/gbp.conf:
- Setup for focal branching and use ubuntu/* tagging
- Use multimaint-merge in dch and sign tags
* debian/control:
- Set Ubuntu Developers as maintainer
- Update VCS informations to point to ubuntu/focal
* dbusMenu: Use GCancellable to stop pending async operations (LP: #1881669)
* dbusMenu: Use proper argument name when parsing error (LP: #1881669)
* dbusMenu: Stop idle requests if we've been destroyed (LP: #1870795)
* appIndicator: Remove the callbackIdle if we destroy while waiting it
(LP: #1849142)
* StatusNotifierWatcher: Remove ProtocolVersion method (LP: #1896785)
* appIndicator: Ignore further icons creation during an idle (LP: #1849142)
* README: Remove statement about indicator-multiload not being supported
* statusNotifierWatcher: Fix RegisterStatusNotifierHost method name and
returned error (LP: #1896785)
* interfaces: Sync interfaces XML with upstream ones (LP: #1896785)
* StatusNotifierItem: Disable Tooltip properties and signals (LP: #1896785)
* appIndicator: Don't waste CPU cycles to handle icon updates (LP: #1884396):
- util: Use Shell's param to handle multiple named arguments
- util: Delete proxyCancellables only if we didn't cancel already
- util: Ignore errors if we can't find a listed dbus name
- appIndicator: Cleanup the interface info properties map computation
- appIndicator: Don't deep unpack changed properties array
- appIndicator: Use native checks to look for equal emblems
- appIndicator: Mark a valid cached icon as inUse again
- appIndicator: Correctly mark cached GIcon's as in use
- iconCache: Rewrite simplifying the usage for GIcon's only
- iconCache: Dispose an icon when we remove it
- iconCache: Increase the garbage-collector timings
- appIndicator: Only iterate through the proxy available properties
- appIndicator: Don't try to check equality on Pixmap variants
- util: Try to batch properties updates when they comes close enough
- appIndicator: Accumulate signals to batch close updates
- appIndicator: Emit the same signal once on properties updates
- appIndicator: Improve the warning message on lookup failed
[ Fini Jastrow ]
* appIndicator: Remove unneeded padding (LP: #1896779)
* appIndicator: Fix 'reduce padding' for some icons (LP: #1896779)
[ Sergio Costas ]
* Wait until the desktop ends starting up (LP: #1870795)
* Wait until Gtk.IconTheme.get_default() works (LP: #1870795)
* Don't fail if no icon is found (LP: #1849142)
* Use signal to detect display availability (LP: #1870795)
[ Tasos Sahanidis ]
* kstatusnotifierwatcher: Implement ProtocolVersion property (LP: #1896785)
-- Marco Trevisan (Treviño) <marco@ubuntu.com> Wed, 23 Sep 2020 18:47:33 +0200
gnome-shell-extension-appindicator (33-1) experimental; urgency=medium
* Team upload.
......
Source: gnome-shell-extension-appindicator
Section: gnome
Priority: optional
Maintainer: Debian GNOME Maintainers <pkg-gnome-maintainers@lists.alioth.debian.org>
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
XSBC-Original-Maintainer: Debian GNOME Maintainers <pkg-gnome-maintainers@lists.alioth.debian.org>
Uploaders: Matteo F. Vescovi <mfv@debian.org>
Build-Depends:
debhelper-compat (= 12)
Standards-Version: 4.5.0
Rules-Requires-Root: no
Homepage: https://github.com/ubuntu/gnome-shell-extension-appindicator
Vcs-Browser: https://salsa.debian.org/gnome-team/shell-extensions/gnome-shell-extension-appindicator
Vcs-Git: https://salsa.debian.org/gnome-team/shell-extensions/gnome-shell-extension-appindicator.git
Vcs-Browser: https://salsa.debian.org/gnome-team/shell-extensions/gnome-shell-extension-appindicator/tree/ubuntu/focal
Vcs-Git: https://salsa.debian.org/gnome-team/shell-extensions/gnome-shell-extension-appindicator.git -b ubuntu/focal
Package: gnome-shell-extension-appindicator
Architecture: all
......
[DEFAULT]
debian-branch = debian/master
upstream-branch = master
debian-branch = ubuntu/focal
debian-tag = ubuntu/%(version)s
upstream-branch = gnome-3-36
upstream-tag = v%(version)s
[buildpackage]
sign-tags = True
[dch]
multimaint-merge = True
......@@ -13,8 +13,12 @@
// 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.
const Gio = imports.gi.Gio
const GLib = imports.gi.GLib
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const Gtk = imports.gi.Gtk;
const Gdk = imports.gi.Gdk;
const Main = imports.ui.main;
const Mainloop = imports.mainloop;
const Extension = imports.misc.extensionUtils.getCurrentExtension()
......@@ -24,6 +28,10 @@ const Util = Extension.imports.util
let statusNotifierWatcher = null;
let isEnabled = false;
let watchDog = null;
let startupPreparedId = 0;
let waitForThemeId = 0;
let startupComplete = false;
let displayAvailable = false;
function init() {
watchDog = new NameWatchdog();
......@@ -51,9 +59,36 @@ function maybe_enable_after_name_available() {
statusNotifierWatcher = new StatusNotifierWatcher.StatusNotifierWatcher(watchDog);
}
function inner_enable() {
if (startupComplete && displayAvailable) {
isEnabled = true;
maybe_enable_after_name_available();
}
}
function enable() {
isEnabled = true;
maybe_enable_after_name_available();
// If the desktop is still starting up, we must wait until it is ready
if (Main.layoutManager._startingUp) {
startupPreparedId = Main.layoutManager.connect('startup-complete', () => {
Main.layoutManager.disconnect(startupPreparedId);
startupComplete = true;
inner_enable();
});
} else {
startupComplete = true;
}
// Ensure that the default Gdk Screen is available
if (Gtk.IconTheme.get_default() == null) {
waitForThemeId = Gdk.DisplayManager.get().connect('display-opened', () => {
Gdk.DisplayManager.get().disconnect(waitForThemeId);
displayAvailable = true;
inner_enable();
});
} else {
displayAvailable = true;
}
inner_enable();
}
function disable() {
......
......@@ -15,9 +15,7 @@
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
const GLib = imports.gi.GLib
const GObject = imports.gi.GObject
const Mainloop = imports.mainloop
const Gio = imports.gi.Gio
const Util = imports.misc.extensionUtils.getCurrentExtension().imports.util;
......@@ -27,96 +25,87 @@ const Util = imports.misc.extensionUtils.getCurrentExtension().imports.util;
// If the lifetime of an icon is over, the cache will destroy the icon. (!)
// The presence of an inUse property set to true on the icon will extend the lifetime.
const LIFETIME_TIMESPAN = 5000; // milli-seconds
const GC_INTERVAL = 10; // seconds
const GC_INTERVAL = 60; // seconds
const LIFETIME_TIMESPAN = 10; // seconds
// how to use: see IconCache.add, IconCache.get
var IconCache = class AppIndicators_IconCache {
constructor() {
this._cache = {};
this._lifetime = {}; //we don't want to attach lifetime to the object
this._destroyNotify = {};
this._cache = new Map();
this._lifetime = new Map(); //we don't want to attach lifetime to the object
}
add(id, o) {
if (!(o && id))
add(id, icon) {
if (!(icon instanceof Gio.Icon)) {
Util.Logger.critical('IconCache: Only Gio.Icons are supported');
return null;
}
if (!(id in this._cache) || this._cache[id] !== o) {
this._remove(id);
Util.Logger.debug("IconCache: adding "+id,o);
this._cache[id] = o;
if (!id) {
Util.Logger.critical('IconCache: Invalid ID provided');
return null;
}
if ((o instanceof GObject.Object) && GObject.signal_lookup('destroy', o)) {
this._destroyNotify[id] = o.connect('destroy', () => {
this._remove(id);
});
}
let oldIcon = this._cache.get(id);
if (!oldIcon || !oldIcon.equals(icon)) {
Util.Logger.debug(`IconCache: adding ${id}: ${icon}`);
this._cache.set(id, icon);
} else {
icon = oldIcon;
}
this._renewLifetime(id);
this._checkGC();
return o;
return icon;
}
_remove(id) {
if (!(id in this._cache))
return;
Util.Logger.debug('IconCache: removing '+id);
let object = this._cache[id];
Util.Logger.debug(`IconCache: removing ${id}`);
if ((object instanceof GObject.Object) && GObject.signal_lookup('destroy', object))
object.disconnect(this._destroyNotify[id]);
if (typeof object.destroy === 'function')
object.destroy();
delete this._cache[id];
delete this._lifetime[id];
delete this._destroyNotify[id];
this._checkGC();
this._cache.get(id).run_dispose();
this._cache.delete(id);
this._lifetime.delete(id);
}
_renewLifetime(id) {
if (id in this._cache)
this._lifetime[id] = new Date().getTime() + LIFETIME_TIMESPAN;
this._lifetime.set(id, new Date().getTime() + LIFETIME_TIMESPAN * 1000);
}
forceDestroy(id) {
this._remove(id);
if (this._cache.has(id)) {
this._remove(id);
this._checkGC();
}
}
// removes everything from the cache
clear() {
for (let id in this._cache)
this._remove(id)
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) {
if (id in this._cache) {
Util.Logger.debug('IconCache: retrieving '+id);
let icon = this._cache.get(id);
if (icon) {
Util.Logger.debug(`IconCache: retrieving ${id}: ${icon}`);
this._renewLifetime(id);
return this._cache[id];
return icon;
}
return null;
}
_checkGC() {
let cacheIsEmpty = (Object.keys(this._cache).length === 0);
let cacheIsEmpty = this._cache.size == 0;
if (!cacheIsEmpty && !this._gcTimeout) {
Util.Logger.debug("IconCache: garbage collector started");
this._gcTimeout = Mainloop.timeout_add_seconds(GC_INTERVAL,
this._gc.bind(this));
this._gcTimeout = GLib.timeout_add_seconds(
GLib.PRIORITY_LOW,
GC_INTERVAL,
() => this._gc());
} else if (cacheIsEmpty && this._gcTimeout) {
Util.Logger.debug("IconCache: garbage collector stopped");
GLib.Source.remove(this._gcTimeout);
......@@ -125,17 +114,16 @@ var IconCache = class AppIndicators_IconCache {
}
_gc() {
var time = new Date().getTime();
for (var id in this._cache) {
if (this._cache[id].inUse) {
Util.Logger.debug("IconCache: " + id + " is in use.");
continue;
} else if (this._lifetime[id] < time) {
let time = new Date().getTime();
this._cache.forEach((icon, id) => {
if (icon.inUse) {
Util.Logger.debug(`IconCache: ${id} is in use.`);
} else if (this._lifetime.get(id) < time) {
this._remove(id);
} else {
Util.Logger.debug("IconCache: " + id + " survived this round.");
Util.Logger.debug(`IconCache: ${id} survived this round.`);
}
}
});
return true;
}
......
<!-- Based on:
https://invent.kde.org/frameworks/knotifications/-/blob/master/src/org.kde.StatusNotifierItem.xml
-->
<interface name="org.kde.StatusNotifierItem">
<property name="Category" type="s" access="read"/>
<property name="Id" type="s" access="read"/>
<property name="Title" type="s" access="read"/>
<property name="Status" type="s" access="read"/>
<property name="WindowId" type="i" access="read"/>
<property name="Menu" type="o" access="read" />
<!-- An additional path to add to the theme search path to find the icons specified above. -->
<property name="IconThemePath" type="s" access="read"/>
<property name="Menu" type="o" access="read"/>
<property name="ItemIsMenu" type="b" access="read"/>
<!-- main icon -->
<!-- names are preferred over pixmaps -->
<property name="IconName" type="s" access="read" />
<property name="IconThemePath" type="s" access="read" />
<property name="IconName" type="s" access="read"/>
<!-- struct containing width, height and image data-->
<!-- implementation has been dropped as of now -->
<property name="IconPixmap" type="a(iiay)" access="read" />
<!--struct containing width, height and image data-->
<property name="IconPixmap" type="a(iiay)" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusImageVector"/>
</property>
<!-- not used in ayatana code, no test case so far -->
<property name="OverlayIconName" type="s" access="read"/>
<property name="OverlayIconPixmap" type="a(iiay)" access="read" />
<property name="OverlayIconPixmap" type="a(iiay)" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusImageVector"/>
</property>
<!-- Requesting attention icon -->
<property name="AttentionIconName" type="s" access="read"/>
<!--same definition as image-->
<property name="AttentionIconPixmap" type="a(iiay)" access="read" />
<property name="AttentionIconPixmap" type="a(iiay)" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusImageVector"/>
</property>
<property name="AttentionMovieName" type="s" access="read"/>
<!-- tooltip data -->
<!-- unimplemented as of now -->
<!--(iiay) is an image-->
<property name="ToolTip" type="(sa(iiay)ss)" access="read" />
<!-- We disable this as we don't support tooltip, so no need to go through it
<property name="ToolTip" type="(sa(iiay)ss)" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusToolTipStruct"/>
</property>
-->
<!-- interaction: the systemtray wants the application to do something -->
<method name="ContextMenu">
<!-- we're passing the coordinates of the icon, so the app knows where to put the popup window -->
<arg name="x" type="i" direction="in"/>
<arg name="y" type="i" direction="in"/>
</method>
<!-- interaction: actually, we do not use them. -->
<method name="Activate">
<arg name="x" type="i" direction="in"/>
<arg name="y" type="i" direction="in"/>
</method>
<method name="SecondaryActivate">
<arg name="x" type="i" direction="in"/>
<arg name="y" type="i" direction="in"/>
</method>
<method name="Scroll">
<arg name="delta" type="i" direction="in"/>
<arg name="dir" type="s" direction="in"/>
<arg name="orientation" type="s" direction="in"/>
</method>
<!-- Signals: the client wants to change something in the status-->
<signal name="NewTitle"></signal>
<signal name="NewIcon"></signal>
<signal name="NewTitle">
</signal>
<signal name="NewIcon">
</signal>
<signal name="NewAttentionIcon">
</signal>
<signal name="NewOverlayIcon">
</signal>
<!-- We disable this as we don't support tooltip, so no need to go through it
<signal name="NewToolTip">
</signal>
-->
<signal name="NewStatus">
<arg name="status" type="s"/>
</signal>
<!-- The following items are not supported by specs, but widely used -->
<signal name="NewIconThemePath">
<arg type="s" name="icon_theme_path" direction="out" />
</signal>
<signal name="NewAttentionIcon"></signal>
<signal name="NewOverlayIcon"></signal>
<signal name="NewMenu"></signal>
<signal name="NewToolTip"></signal>
<signal name="NewStatus">
<arg name="status" type="s" />
</signal>
<!-- ayatana labels -->
<!-- These are commented out because GDBusProxy would otherwise require them,
......@@ -70,5 +116,4 @@
<property name="XAyatanaLabel" type="s" access="read" />
<property name="XAyatanaLabelGuide" type="s" access="read" />-->
</interface>
<interface name="org.kde.StatusNotifierWatcher">
<!-- methods -->
<method name="RegisterStatusNotifierItem">
<arg type="s" direction="in" />
<arg name="service" type="s" direction="in"/>
</method>
<method name="RegisterNotificationHost">
<arg type="s" direction="in" />
</method>
<property name="RegisteredStatusNotifierItems" type="as" access="read" />
<method name="ProtocolVersion">
<arg type="s" direction="out" />
</method>
<method name="IsNotificationHostRegistered">
<arg type="b" direction="out" />
<method name="RegisterStatusNotifierHost">
<arg name="service" type="s" direction="in"/>
</method>
<!-- properties -->
<property name="RegisteredStatusNotifierItems" type="as" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QStringList"/>
</property>
<property name="IsStatusNotifierHostRegistered" type="b" access="read"/>
<property name="ProtocolVersion" type="i" access="read"/>
<!-- signals -->
<signal name="StatusNotifierItemRegistered">
<arg type="s" direction="out" />
<arg type="s"/>
</signal>
<signal name="StatusNotifierItemUnregistered">
<arg type="s" direction="out" />
<arg type="s"/>
</signal>
<signal name="StatusNotifierHostRegistered">
</signal>
<signal name="StatusNotifierHostUnregistered">
</signal>
<property name="IsStatusNotifierHostRegistered" type="b" access="read" />
</interface>
......@@ -192,21 +192,17 @@ var StatusNotifierWatcher = class AppIndicators_StatusNotifierWatcher {
this._dbusImpl.emit_property_changed('RegisteredStatusNotifierItems', GLib.Variant.new('as', this.RegisteredStatusNotifierItems));
}
RegisterNotificationHost(service) {
throw new Gio.DBusError('org.gnome.Shell.UnsupportedMethod',
'Registering additional notification hosts is not supported');
RegisterStatusNotifierHostAsync(_service, invocation) {
invocation.return_error_literal(
Gio.DBusError,
Gio.DBusError.NOT_SUPPORTED,
'Registering additional notification hosts is not supported');
}
IsNotificationHostRegistered() {
return true;
}
ProtocolVersion() {
// "The version of the protocol the StatusNotifierWatcher instance implements." [sic]
// in what syntax?
return `${Extension.uuid} (KDE; compatible; mostly) GNOME Shell/${ShellConfig.PACKAGE_VERSION}`;
}
get RegisteredStatusNotifierItems() {
return Object.keys(this._items);
}
......@@ -215,6 +211,10 @@ var StatusNotifierWatcher = class AppIndicators_StatusNotifierWatcher {
return true;
}
get ProtocolVersion() {
return 0;
}
destroy() {
if (!this._isDestroyed) {
// this doesn't do any sync operation and doesn't allow us to hook up the event of being finished
......
......@@ -17,14 +17,23 @@ const Gio = imports.gi.Gio
const GLib = imports.gi.GLib
const GObject = imports.gi.GObject
const Extension = imports.misc.extensionUtils.getCurrentExtension();
const Params = imports.misc.params;
const Signals = imports.signals
var refreshPropertyOnProxy = function(proxy, propertyName) {
var refreshPropertyOnProxy = function(proxy, propertyName, params) {
if (!proxy._proxyCancellables)
proxy._proxyCancellables = new Map();
let cancellable = cancelRefreshPropertyOnProxy(proxy, propertyName, true);
params = Params.parse(params, {
skipEqualtyCheck: false,
});
let cancellable = cancelRefreshPropertyOnProxy(proxy, {
propertyName,
addNew: true
});
proxy.g_connection.call(
proxy.g_name,
proxy.g_object_path,
......@@ -36,49 +45,74 @@ var refreshPropertyOnProxy = function(proxy, propertyName) {
-1,
cancellable,
(conn, result) => {
proxy._proxyCancellables.delete(propertyName);
try {
let valueVariant = conn.call_finish(result).deep_unpack()[0]
let valueVariant = conn.call_finish(result).deep_unpack()[0];
proxy._proxyCancellables.delete(propertyName);
if (proxy.get_cached_property(propertyName).equal(valueVariant))
if (!params.skipEqualtyCheck &&
proxy.get_cached_property(propertyName).equal(valueVariant))
return;
proxy.set_cached_property(propertyName, valueVariant)
// synthesize a property changed event
let changedObj = {}
changedObj[propertyName] = valueVariant
proxy.emit('g-properties-changed', GLib.Variant.new('a{sv}', changedObj), [])
// synthesize a batched property changed event
if (!proxy._proxyChangedProperties)
proxy._proxyChangedProperties = {};
proxy._proxyChangedProperties[propertyName] = valueVariant;
if (!proxy._proxyPropertiesEmitId) {
proxy._proxyPropertiesEmitId = GLib.timeout_add(
GLib.PRIORITY_DEFAULT_IDLE, 16, () => {
delete proxy._proxyPropertiesEmitId;
proxy.emit('g-properties-changed', GLib.Variant.new('a{sv}',
proxy._proxyChangedProperties), []);
delete proxy._proxyChangedProperties;
return GLib.SOURCE_REMOVE;
});
}
} 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._proxyCancellables.delete(propertyName);
delete proxy._proxyChangedProperties[propertyName];
}
}
});
}
var cancelRefreshPropertyOnProxy = function(proxy, propertyName=undefined, addNew=false) {
var cancelRefreshPropertyOnProxy = function(proxy, params) {
if (!proxy._proxyCancellables)
return;
if (propertyName !== undefined) {
let cancellable = proxy._proxyCancellables.get(propertyName);
params = Params.parse(params, {
propertyName: undefined,
addNew: false,
});
if (params.propertyName !== undefined) {
let cancellable = proxy._proxyCancellables.get(params.propertyName);
if (cancellable) {
cancellable.cancel();
if (!addNew)
proxy._proxyCancellables.delete(propertyName);
if (!params.addNew)
proxy._proxyCancellables.delete(params.propertyName);
}
if (addNew) {
if (params.addNew) {
cancellable = new Gio.Cancellable();
proxy._proxyCancellables.set(propertyName, cancellable);
proxy._proxyCancellables.set(params.propertyName, cancellable);
return cancellable;
}
} else {
for (let cancellable of proxy._proxyCancellables)
cancellable.cancel();
if (proxy._proxyPropertiesEmitId) {
GLib.source_remove(proxy._proxyPropertiesEmitId);
delete proxy._proxyPropertiesEmitId;
}
proxy._proxyCancellables.forEach(c => c.cancel());
delete proxy._proxyChangedProperties;
delete proxy._proxyCancellables;
}
}
......@@ -115,9 +149,13 @@ var traverseBusNames = function(bus, cancellable, callback) {
let unique_names = [];
for (let name of names) {
let unique = getUniqueBusNameSync(bus, name);
if (unique_names.indexOf(unique) == -1)
unique_names.push(unique);
try {
let unique = getUniqueBusNameSync(bus, name);
if (unique_names.indexOf(unique) == -1)
unique_names.push(unique);
} catch (e) {
Logger.debug(`Impossible to get the unique name of ${name}: ${e}`);
}
}
for (let name of unique_names)
......