diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..5e3347d --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,3 @@ +extends: + - ./lint/eslintrc-gjs.yml + - ./lint/eslintrc-shell.yml diff --git a/.github/workflows/eslint.yaml b/.github/workflows/eslint.yaml new file mode 100644 index 0000000..189ec6c --- /dev/null +++ b/.github/workflows/eslint.yaml @@ -0,0 +1,11 @@ +name: ESLint +on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install modules + run: sudo npm install eslint -g + - name: Run ESLint + run: eslint . --ext .js,.jsx,.ts,.tsx diff --git a/Makefile b/Makefile index 4f87f10..0b2c5f8 100644 --- a/Makefile +++ b/Makefile @@ -10,5 +10,8 @@ rm -f build/appindicator-support.zip zip build/appindicator-support.zip $(ZIP) +check: + eslint $(shell find -name '*.js') + clean: rm -rf build diff --git a/appIndicator.js b/appIndicator.js index 46c2d41..d606b8f 100644 --- a/appIndicator.js +++ b/appIndicator.js @@ -14,37 +14,43 @@ // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -const Clutter = imports.gi.Clutter -const Cogl = imports.gi.Cogl -const GdkPixbuf = imports.gi.GdkPixbuf -const Gio = imports.gi.Gio -const GLib = imports.gi.GLib -const GObject = imports.gi.GObject -const Gtk = imports.gi.Gtk -const St = imports.gi.St -const Shell = imports.gi.Shell +/* exported AppIndicator, IconActor */ + +const GdkPixbuf = imports.gi.GdkPixbuf; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; +const St = imports.gi.St; const Extension = imports.misc.extensionUtils.getCurrentExtension(); -const Signals = imports.signals - -const DBusMenu = Extension.imports.dbusMenu; +const Signals = imports.signals; + const IconCache = Extension.imports.iconCache; const Util = Extension.imports.util; const Interfaces = Extension.imports.interfaces; +const PromiseUtils = Extension.imports.promiseUtils; + +PromiseUtils._promisify(Gio.File.prototype, 'read_async', 'read_finish'); +PromiseUtils._promisify(Gio._LocalFilePrototype, 'read_async', 'read_finish'); +PromiseUtils._promisify(GdkPixbuf.Pixbuf, 'get_file_info_async', 'get_file_info_finish'); +PromiseUtils._promisify(GdkPixbuf.Pixbuf, 'new_from_stream_at_scale_async', 'new_from_stream_finish'); +PromiseUtils._promisify(Gio.DBusProxy.prototype, 'init_async', 'init_finish'); const MAX_UPDATE_FREQUENCY = 100; // In ms +// eslint-disable-next-line no-unused-vars const SNICategory = { APPLICATION: 'ApplicationStatus', COMMUNICATIONS: 'Communications', SYSTEM: 'SystemServices', - HARDWARE: 'Hardware' + HARDWARE: 'Hardware', }; var SNIStatus = { PASSIVE: 'Passive', ACTIVE: 'Active', - NEEDS_ATTENTION: 'NeedsAttention' + NEEDS_ATTENTION: 'NeedsAttention', }; const SNIconType = { @@ -57,53 +63,53 @@ * the AppIndicator class serves as a generic container for indicator information and functions common * for every displaying implementation (IndicatorMessageSource and IndicatorStatusIcon) */ -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) - - //HACK: we cannot use Gio.DBusProxy.makeProxyWrapper because we need +var AppIndicator = class AppIndicatorsAppIndicator { + + constructor(service, busName, object) { + this.busName = busName; + this._uniqueId = busName + object; + this._accumulatedSignals = new Set(); + + const interfaceInfo = Gio.DBusInterfaceInfo.new_for_xml(Interfaces.StatusNotifierItem); + + // HACK: we cannot use Gio.DBusProxy.makeProxyWrapper because we need // to specify G_DBUS_PROXY_FLAGS_GET_INVALIDATED_PROPERTIES this._cancellable = new Gio.Cancellable(); this._proxy = new Gio.DBusProxy({ g_connection: Gio.DBus.session, - g_interface_name: interface_info.name, - g_interface_info: interface_info, - g_name: bus_name, - g_object_path: object, - g_flags: Gio.DBusProxyFlags.GET_INVALIDATED_PROPERTIES }) - this._proxy.init_async(GLib.PRIORITY_DEFAULT, this._cancellable, ((initable, result) => { - try { - initable.init_finish(result); - this._checkIfReady(); - - if (!this.isReady && !this.menuPath) { - let checks = 0; - this._delayCheck = GLib.timeout_add_seconds( - GLib.PRIORITY_DEFAULT_IDLE, 1, () => { - Util.refreshPropertyOnProxy(this._proxy, 'Menu'); - return !this.isReady && ++checks < 3; - }); - } - } catch(e) { - if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) - Util.Logger.warn(`While intializing proxy for ${bus_name} ${object}: ${e}`); - } - })) - - Util.connectSmart(this._proxy, 'g-properties-changed', this, '_onPropertiesChanged') - Util.connectSmart(this._proxy, 'g-signal', this, this._onProxySignal) - Util.connectSmart(this._proxy, 'notify::g-name-owner', this, '_nameOwnerChanged') + g_interface_name: interfaceInfo.name, + g_interface_info: interfaceInfo, + g_name: busName, + g_object_path: object, + g_flags: Gio.DBusProxyFlags.GET_INVALIDATED_PROPERTIES }); + + this._setupProxy(); + Util.connectSmart(this._proxy, 'g-properties-changed', this, this._onPropertiesChanged); + Util.connectSmart(this._proxy, 'g-signal', this, this._onProxySignal); + Util.connectSmart(this._proxy, 'notify::g-name-owner', this, this._nameOwnerChanged); + + if (service !== busName && service.match(Util.BUS_ADDRESS_REGEX)) { + this._uniqueId = service; + this._nameWatcher = new Util.NameWatcher(service); + Util.connectSmart(this._nameWatcher, 'changed', this, this._nameOwnerChanged); + } + } + + async _setupProxy() { + try { + await this._proxy.init_async(GLib.PRIORITY_DEFAULT, this._cancellable); + this._checkIfReady(); + this._checkMenuReady(); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + Util.Logger.warn(`While initalizing proxy for ${this._uniqueId}: ${e}`); + } } _checkIfReady() { let wasReady = this.isReady; let isReady = false; - if (this._proxy.g_name_owner && this.menuPath) + if (this.hasNameOwner && this.menuPath) isReady = true; this.isReady = isReady; @@ -111,7 +117,7 @@ if (this.isReady && !wasReady) { if (this._delayCheck) { - GLib.Source.remove(this._delayCheck); + this._delayCheck.cancel(); delete this._delayCheck; } @@ -122,9 +128,29 @@ return false; } + async _checkMenuReady() { + if (this.menuPath) + return true; + + const cancellable = this._cancellable; + for (let checks = 0; checks < 3 && !this.isReady; ++checks) { + this._delayCheck = new PromiseUtils.TimeoutSecondsPromise(1, + GLib.PRIORITY_DEFAULT_IDLE, cancellable); + // eslint-disable-next-line no-await-in-loop + await this._delayCheck; + Util.refreshPropertyOnProxy(this._proxy, 'Menu'); + } + + return !!this.menuPath; + } + _nameOwnerChanged() { - if (!this._proxy.g_name_owner) + if (!this.hasNameOwner) this._checkIfReady(); + else + this._checkMenuReady(); + + this.emit('name-owner-changed'); } _addExtraProperty(name) { @@ -138,7 +164,7 @@ get: () => { const v = this._proxy.get_cached_property(name); return v ? v.deep_unpack() : null; - } + }, }); } @@ -149,7 +175,7 @@ let interfaceProps = this._proxy.g_interface_info.properties; this._proxyPropertyList = (this._proxy.get_cached_property_names() || []).filter(p => - interfaceProps.some(propinfo => propinfo.name == p)); + interfaceProps.some(propinfo => propinfo.name === p)); if (this._proxyPropertyList.length) { this._addExtraProperty('XAyatanaLabel'); @@ -163,53 +189,61 @@ let prop = null; if (signal.startsWith('New')) - prop = signal.substr(3) + prop = signal.substr(3); else if (signal.startsWith('XAyatanaNew')) - prop = 'XAyatana' + signal.substr(11) + prop = `XAyatana${signal.substr(11)}`; if (!prop) return; [prop, `${prop}Name`, `${prop}Pixmap`].filter(p => this._proxyPropertyList.includes(p)).forEach(p => - Util.refreshPropertyOnProxy(this._proxy, p, { - skipEqualtyCheck: p.endsWith('Pixmap'), - }) - ); - } - - _onProxySignal(_proxy, _sender, signal, _params) { - this._accumuledSignals.add(signal); - - if (this._signalsAccumulatorId) + Util.refreshPropertyOnProxy(this._proxy, p, { + skipEqualityCheck: p.endsWith('Pixmap'), + }), + ); + } + + async _onProxySignal(_proxy, _sender, signal, _params) { + this._accumulatedSignals.add(signal); + + if (this._signalsAccumulator) 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 + this._signalsAccumulator = new PromiseUtils.TimeoutPromise( + GLib.PRIORITY_DEFAULT_IDLE, MAX_UPDATE_FREQUENCY, this._cancellable); + try { + await this._signalsAccumulator; + this._accumulatedSignals.forEach(s => this._translateNewSignals(s)); + this._accumulatedSignals.clear(); + } finally { + delete this._signalsAccumulator; + } + } + + // 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; } + get menuPath() { - if (this._proxy.Menu == '/NO_DBUSMENU') + if (this._proxy.Menu === '/NO_DBUSMENU') return null; return this._proxy.Menu || '/MenuBar'; @@ -219,63 +253,72 @@ return [ this._proxy.AttentionIconName, this._proxy.AttentionIconPixmap, - this._proxy.IconThemePath - ] + this._proxy.IconThemePath, + ]; } get icon() { return [ this._proxy.IconName, this._proxy.IconPixmap, - this._proxy.IconThemePath - ] + this._proxy.IconThemePath, + ]; } get overlayIcon() { return [ this._proxy.OverlayIconName, this._proxy.OverlayIconPixmap, - this._proxy.IconThemePath - ] - } - - _onPropertiesChanged(proxy, changed, invalidated) { + this._proxy.IconThemePath, + ]; + } + + get hasNameOwner() { + return !!this._proxy.g_name_owner || + this._nameWatcher && this._nameWatcher.nameOnBus; + } + + get cancellable() { + return this._cancellable; + } + + _onPropertiesChanged(_proxy, changed, _invalidated) { let props = Object.keys(changed.unpack()); let signalsToEmit = new Set(); - props.forEach((property) => { + 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' || + if (property === 'Status' || property.startsWith('Icon') || - property.startsWith('AttentionIcon')) { - signalsToEmit.add('icon') - } + property.startsWith('AttentionIcon')) + signalsToEmit.add('icon'); + // same for overlays if (property.startsWith('OverlayIcon')) - signalsToEmit.add('overlay-icon') + signalsToEmit.add('overlay-icon'); // this may make all of our icons invalid - if (property == 'IconThemePath') { - signalsToEmit.add('icon') - signalsToEmit.add('overlay-icon') + 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 (property === 'XAyatanaLabel') + signalsToEmit.add('label'); + + if (property === 'Menu') { if (!this._checkIfReady() && this.isReady) - signalsToEmit.add('menu') + signalsToEmit.add('menu'); } // status updates may cause the indicator to be hidden - if (property == 'Status') - signalsToEmit.add('status') + if (property === 'Status') + signalsToEmit.add('status'); }); signalsToEmit.forEach(s => this.emit(s)); @@ -286,23 +329,16 @@ } destroy() { - this.emit('destroy') - - this.disconnectAll() + this.emit('destroy'); + + this.disconnectAll(); this._cancellable.cancel(); Util.cancelRefreshPropertyOnProxy(this._proxy); + if (this._nameWatcher) + this._nameWatcher.destroy(); 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; - } + delete this._proxy; + delete this._nameWatcher; } open() { @@ -310,27 +346,27 @@ // 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. - this._proxy.ActivateRemote(0, 0) + this._proxy.ActivateRemote(0, 0); } secondaryActivate() { - this._proxy.SecondaryActivateRemote(0, 0) + this._proxy.SecondaryActivateRemote(0, 0); } scroll(dx, dy) { - if (dx != 0) - this._proxy.ScrollRemote(Math.floor(dx), 'horizontal') - - if (dy != 0) - this._proxy.ScrollRemote(Math.floor(dy), 'vertical') + if (dx !== 0) + this._proxy.ScrollRemote(Math.floor(dx), 'horizontal'); + + if (dy !== 0) + this._proxy.ScrollRemote(Math.floor(dy), 'vertical'); } }; Signals.addSignalMethods(AppIndicator.prototype); var IconActor = GObject.registerClass( -class AppIndicators_IconActor extends St.Icon { - - _init(indicator, icon_size) { +class AppIndicatorsIconActor extends St.Icon { + + _init(indicator, iconSize) { super._init({ reactive: true, style_class: 'system-status-icon', @@ -341,43 +377,38 @@ this.add_style_class_name('appindicator-icon'); this.set_style('padding:0'); + // eslint-disable-next-line no-undef let themeContext = St.ThemeContext.get_for_stage(global.stage); - this.height = icon_size * themeContext.scale_factor; - - this._indicator = indicator - this._iconSize = icon_size - this._iconCache = new IconCache.IconCache() + this.height = iconSize * themeContext.scale_factor; + + this._indicator = indicator; + this._iconSize = iconSize; + this._iconCache = new IconCache.IconCache(); this._cancellable = new Gio.Cancellable(); this._loadingIcons = new Set(); - Util.connectSmart(this._indicator, 'icon', this, '_updateIcon') - Util.connectSmart(this._indicator, 'overlay-icon', this, '_updateOverlayIcon') - Util.connectSmart(this._indicator, 'reset', this, '_invalidateIcon') - Util.connectSmart(this, 'scroll-event', this, '_handleScrollEvent') - - Util.connectSmart(themeContext, 'notify::scale-factor', this, (tc) => { - this.height = icon_size * tc.scale_factor; + Util.connectSmart(this._indicator, 'icon', this, this._updateIcon); + Util.connectSmart(this._indicator, 'overlay-icon', this, this._updateOverlayIcon); + Util.connectSmart(this._indicator, 'reset', this, this._invalidateIcon); + + Util.connectSmart(themeContext, 'notify::scale-factor', this, tc => { + this.height = iconSize * tc.scale_factor; this._invalidateIcon(); }); Util.connectSmart(this._indicator, 'ready', this, () => { this._updateIconClass(); this._invalidateIcon(); - }) - - Util.connectSmart(Gtk.IconTheme.get_default(), 'changed', this, '_invalidateIcon') + }); + + Util.connectSmart(Gtk.IconTheme.get_default(), 'changed', this, this._invalidateIcon); if (indicator.isReady) - this._invalidateIcon() + this._invalidateIcon(); this.connect('destroy', () => { this._iconCache.destroy(); this._cancellable.cancel(); - - if (this._callbackIdle) { - GLib.source_remove(this._callbackIdle); - delete this._callbackIdle; - } }); } @@ -386,138 +417,135 @@ `appindicator-icon-${this._indicator.id.toLowerCase().replace(/_|\s/g, '-')}`); } - // Will look the icon up in the cache, if it's found - // it will return it. Otherwise, it will create it and cache it. - _cacheOrCreateIconByName(iconSize, iconName, themePath, callback) { - let {scale_factor} = St.ThemeContext.get_for_stage(global.stage); - let id = `${iconName}@${iconSize * scale_factor}${themePath || ''}`; - let gicon = this._iconCache.get(id); - - if (gicon) { - callback(gicon); - return; - } - - if (this._loadingIcons.has(id)) { - Util.Logger.debug(`${this._indicator.id}, Icon ${id} Is still loading, ignoring the request`); - return; - } else if (this._loadingIcons.size > 0) { + _cancelLoading() { + if (this._loadingIcons.size > 0) { this._cancellable.cancel(); this._cancellable = new Gio.Cancellable(); this._loadingIcons.clear(); } + } + + // 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(iconSize, iconName, themePath) { + // eslint-disable-next-line no-undef + let { scale_factor: scaleFactor } = St.ThemeContext.get_for_stage(global.stage); + let id = `${iconName}@${iconSize * scaleFactor}${themePath || ''}`; + let gicon = this._iconCache.get(id); + + if (gicon) + return gicon; + + if (this._loadingIcons.has(id)) { + Util.Logger.debug(`${this._indicator.id}, Icon ${id} Is still loading, ignoring the request`); + throw new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING, + 'Already in progress'); + } else { + this._cancelLoading(); + } this._loadingIcons.add(id); - let path = this._getIconInfo(iconName, themePath, iconSize, scale_factor); - this._createIconByName(path, (gicon) => { - this._loadingIcons.delete(id); - if (gicon) - gicon = this._iconCache.add(id, gicon); - callback(gicon); - }); - } - - _createIconByPath(path, width, height, callback) { + let path = this._getIconInfo(iconName, themePath, iconSize, scaleFactor); + gicon = await this._createIconByName(path); + this._loadingIcons.delete(id); + if (gicon) + gicon = this._iconCache.add(id, gicon); + return gicon; + } + + async _createIconByPath(path, width, height) { let file = Gio.File.new_for_path(path); - file.read_async(GLib.PRIORITY_DEFAULT, this._cancellable, (file, res) => { + try { + const inputStream = await file.read_async(GLib.PRIORITY_DEFAULT, this._cancellable); + const pixbuf = GdkPixbuf.Pixbuf.new_from_stream_at_scale_async(inputStream, + height, width, true, this._cancellable); + this.icon_size = width > 0 ? width : this._iconSize; + return pixbuf; + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + Util.Logger.warn(`${this._indicator.id}, Impossible to read image from path '${path}': ${e}`); + throw e; + } + } + + async _createIconByName(path) { + if (!path) { + if (this._createIconIdle) { + throw new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING, + 'Already in progress'); + } + try { - let inputStream = file.read_finish(res); - - GdkPixbuf.Pixbuf.new_from_stream_at_scale_async( - inputStream, height, width, true, this._cancellable, (_p, res) => { - try { - callback(GdkPixbuf.Pixbuf.new_from_stream_finish(res)); - this.icon_size = width > 0 ? width : this._iconSize; - } catch (e) { - if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { - Util.Logger.warn(`${this._indicator.id}, Impossible to create image from path '${path}': ${e}`); - callback(null); - } - } - }); + this._createIconIdle = new PromiseUtils.IdlePromise(GLib.PRIORITY_DEFAULT_IDLE, + this._cancellable); + await this._createIconIdle; } catch (e) { - if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { - Util.Logger.warn(`${this._indicator.id}, Impossible to read image from path '${path}': ${e}`); - callback(null); - } - } - }); - } - - _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); - - if (!format) { - Util.Logger.critical(`${this._indicator.id}, Invalid image format: ${path}`); - callback(null); - return; - } - - if (width >= height * 1.5) { - /* Hello indicator-multiload! */ - this._createIconByPath(path, width, -1, callback); - } else { - callback(new Gio.FileIcon({ - file: Gio.File.new_for_path(path) - })); - this.icon_size = this._iconSize; - } - } catch (e) { - if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { - Util.Logger.warn(`${this._indicator.id}, Impossible to read image info from path '${path}': ${e}`); - callback(null); - } - } - }); + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + throw e; + } finally { + delete this._createIconIdle; + } + return null; + } else if (this._createIconIdle) { + this._createIconIdle.cancel(); + delete this._createIconIdle; + } + + try { + const [format, width, height] = await GdkPixbuf.Pixbuf.get_file_info_async( + path, this._cancellable); + + if (!format) { + Util.Logger.critical(`${this._indicator.id}, Invalid image format: ${path}`); + return null; + } + + if (width >= height * 1.5) { + /* Hello indicator-multiload! */ + return this._createIconByPath(path, width, -1); + } else { + this.icon_size = this._iconSize; + return new Gio.FileIcon({ + file: Gio.File.new_for_path(path), + }); + } + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + Util.Logger.warn(`${this._indicator.id}, Impossible to read image info from path '${path}': ${e}`); + throw e; + } } _getIconInfo(name, themePath, size, scale) { let path = null; - if (name && name[0] == "/") { - //HACK: icon is a path name. This is not specified by the api but at least inidcator-sensors uses it. + if (name && name[0] === '/') { + // HACK: icon is a path name. This is not specified by the api but at least inidcator-sensors uses it. path = name; } else if (name) { // we manually look up the icon instead of letting st.icon do it for us // this allows us to sneak in an indicator provided search path and to avoid ugly upscaled icons // indicator-application looks up a special "panel" variant, we just replicate that here - name = name + "-panel"; + name += '-panel'; // icon info as returned by the lookup let iconInfo = null; // we try to avoid messing with the default icon theme, so we'll create a new one if needed - let icon_theme = null; + let iconTheme = null; if (themePath) { - icon_theme = new Gtk.IconTheme(); - Gtk.IconTheme.get_default().get_search_path().forEach((path) => { - icon_theme.append_search_path(path); - }); - icon_theme.append_search_path(themePath); - icon_theme.set_screen(imports.gi.Gdk.Screen.get_default()); + iconTheme = new Gtk.IconTheme(); + Gtk.IconTheme.get_default().get_search_path().forEach(p => + iconTheme.append_search_path(p)); + iconTheme.append_search_path(themePath); + iconTheme.set_screen(imports.gi.Gdk.Screen.get_default()); } else { - icon_theme = Gtk.IconTheme.get_default(); - } - if (icon_theme) { + iconTheme = Gtk.IconTheme.get_default(); + } + if (iconTheme) { // try to look up the icon in the icon theme - iconInfo = icon_theme.lookup_icon_for_scale(name, size, scale, + iconInfo = iconTheme.lookup_icon_for_scale(name, size, scale, Gtk.IconLookupFlags.GENERIC_FALLBACK); // no icon? that's bad! if (iconInfo === null) { @@ -532,59 +560,91 @@ return path; } - argbToRgba(src) { - let dest = new Uint8Array(src.length); - - for (let i = 0; i < src.length; i += 4) { - let srcAlpha = src[i] - - dest[i] = src[i + 1]; /* red */ - dest[i + 1] = src[i + 2]; /* green */ - dest[i + 2] = src[i + 3]; /* blue */ - dest[i + 3] = srcAlpha; /* alpha */ - } - + async argbToRgba(src, cancellable) { + const CHUNK_SIZE = 1024; + const ops = []; + const dest = new Uint8Array(src.length); + + for (let i = 0; i < src.length;) { + const chunkSize = Math.min(CHUNK_SIZE, src.length - i); + + ops.push(new PromiseUtils.CancellablePromise(async resolve => { + const start = i; + const end = i + chunkSize; + await new PromiseUtils.IdlePromise(GLib.PRIORITY_LOW, cancellable); + + for (let j = start; j < end; j += 4) { + let srcAlpha = src[j]; + + dest[j] = src[j + 1]; /* red */ + dest[j + 1] = src[j + 2]; /* green */ + dest[j + 2] = src[j + 3]; /* blue */ + dest[j + 3] = srcAlpha; /* alpha */ + } + resolve(); + }, cancellable)); + + i += chunkSize; + } + + await Promise.all(ops); return dest; } - _createIconFromPixmap(iconSize, iconPixmapArray, snIconType) { - let {scale_factor} = St.ThemeContext.get_for_stage(global.stage); - iconSize = iconSize * scale_factor + async _createIconFromPixmap(iconSize, iconPixmapArray) { + // eslint-disable-next-line no-undef + const { scale_factor: scaleFactor } = St.ThemeContext.get_for_stage(global.stage); + iconSize *= scaleFactor; // the pixmap actually is an array of pixmaps with different sizes // we use the one that is smaller or equal the iconSize // maybe it's empty? that's bad. if (!iconPixmapArray || iconPixmapArray.length < 1) - return null - - let sortedIconPixmapArray = iconPixmapArray.sort((pixmapA, pixmapB) => { - // we sort smallest to biggest - let areaA = pixmapA[0] * pixmapA[1] - let areaB = pixmapB[0] * pixmapB[1] - - return areaA - areaB - }) - - let qualifiedIconPixmapArray = sortedIconPixmapArray.filter((pixmap) => { - // we prefer any pixmap that is equal or bigger than our requested size - return pixmap[0] >= iconSize && pixmap[1] >= iconSize; - }) - - let iconPixmap = qualifiedIconPixmapArray.length > 0 ? qualifiedIconPixmapArray[0] : sortedIconPixmapArray.pop() - - let [ width, height, bytes ] = iconPixmap - let rowstride = width * 4 // hopefully this is correct - - try { - return GdkPixbuf.Pixbuf.new_from_bytes( - this.argbToRgba(bytes), - GdkPixbuf.Colorspace.RGB, true, - 8, width, height, rowstride); - } catch (e) { - // the image data was probably bogus. We don't really know why, but it _does_ happen. - Util.Logger.warn(`${this._indicator.id}, Impossible to create image from data: ${e}`) - return null - } + throw TypeError('Empty Icon found'); + + const sortedIconPixmapArray = iconPixmapArray.sort((pixmapA, pixmapB) => { + // we sort smallest to biggest + const areaA = pixmapA[0] * pixmapA[1]; + const areaB = pixmapB[0] * pixmapB[1]; + + return areaA - areaB; + }); + + const qualifiedIconPixmapArray = sortedIconPixmapArray.filter(pixmap => + // we prefer any pixmap that is equal or bigger than our requested size + pixmap[0] >= iconSize && pixmap[1] >= iconSize); + + const iconPixmap = qualifiedIconPixmapArray.length > 0 + ? qualifiedIconPixmapArray[0] : sortedIconPixmapArray.pop(); + + const [width, height, bytes] = iconPixmap; + const rowStride = width * 4; // hopefully this is correct + + const id = `__PIXMAP_ICON_${width}x${height}`; + if (this._loadingIcons.has(id)) { + Util.Logger.debug(`${this._indicator.id}, Pixmap ${width}x${height} ` + + 'Is still loading, ignoring the request'); + throw new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING, + 'Already in progress'); + } else { + this._cancelLoading(); + } + + this._loadingIcons.add(id); + + try { + return GdkPixbuf.Pixbuf.new_from_bytes( + await this.argbToRgba(bytes, this._cancellable), + GdkPixbuf.Colorspace.RGB, true, + 8, width, height, rowStride); + } 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._indicator.id}, Impossible to create image from data: ${e}`); + throw e; + } finally { + this._loadingIcons.delete(id); + } } // The .inUse flag will be set to true if the used gicon matches the cached @@ -592,26 +652,24 @@ // 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 (iconType !== SNIconType.OVERLAY) { if (gicon) { this.gicon = new Gio.EmblemedIcon({ gicon }); if (!(gicon instanceof GdkPixbuf.Pixbuf)) - gicon.inUse = (this.gicon.get_icon() == gicon); + gicon.inUse = this.gicon.get_icon() === gicon; } else { this.gicon = null; Util.Logger.critical(`unable to update icon for ${this._indicator.id}`); } + } else if (gicon) { + this._emblem = new Gio.Emblem({ icon: gicon }); + + if (!(gicon instanceof GdkPixbuf.Pixbuf)) + gicon.inUse = true; } 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}`); - } + this._emblem = null; + Util.Logger.debug(`unable to update icon emblem for ${this._indicator.id}`); } if (this.gicon) { @@ -623,33 +681,37 @@ } } - _updateIconByType(iconType, iconSize) { + async _updateIconByType(iconType, iconSize) { let icon; switch (iconType) { - case SNIconType.ATTENTION: - icon = this._indicator.attentionIcon; - break; - case SNIconType.NORMAL: - icon = this._indicator.icon; - break; - case SNIconType.OVERLAY: - icon = this._indicator.overlayIcon; - break; - } - - let [name, pixmap, theme] = icon; - if (name && name.length) { - this._cacheOrCreateIconByName(iconSize, name, theme, (gicon) => { - if (!gicon && pixmap) { - gicon = this._createIconFromPixmap(iconSize, - pixmap, iconType); - } - this._setGicon(iconType, gicon); - }); - } else if (pixmap) { - let gicon = this._createIconFromPixmap(iconSize, - pixmap, iconType); + case SNIconType.ATTENTION: + icon = this._indicator.attentionIcon; + break; + case SNIconType.NORMAL: + icon = this._indicator.icon; + break; + case SNIconType.OVERLAY: + icon = this._indicator.overlayIcon; + break; + } + + const [name, pixmap, theme] = icon; + let gicon = null; + try { + if (name && name.length) { + gicon = await this._cacheOrCreateIconByName(iconSize, name, theme); + if (!gicon && pixmap) + gicon = await this._createIconFromPixmap(iconSize, pixmap, iconType); + } else if (pixmap) { + gicon = await this._createIconFromPixmap(iconSize, pixmap, iconType); + } + this._setGicon(iconType, gicon); + } catch (e) { + /* We handle the error messages already */ + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED) && + !e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING)) + Util.Logger.debug(`${this._indicator.id}, Impossible to load icon: ${e}`); } } @@ -659,12 +721,12 @@ let { gicon } = this.gicon; if (gicon.inUse) - gicon.inUse = false + gicon.inUse = false; } // we might need to use the AttentionIcon*, which have precedence over the normal icons - let iconType = this._indicator.status == SNIStatus.NEEDS_ATTENTION ? - SNIconType.ATTENTION : SNIconType.NORMAL; + let iconType = this._indicator.status === SNIStatus.NEEDS_ATTENTION + ? SNIconType.ATTENTION : SNIconType.NORMAL; this._updateIconByType(iconType, this._iconSize); } @@ -680,39 +742,17 @@ // 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. - let iconSize = Math.floor(this._iconSize / 1.6) + let iconSize = Math.floor(this._iconSize / 1.6); this._updateIconByType(SNIconType.OVERLAY, iconSize); - } - - _handleScrollEvent(actor, event) { - if (actor != this) - return Clutter.EVENT_PROPAGATE - - if (event.get_source() != this) - return Clutter.EVENT_PROPAGATE - - if (event.type() != Clutter.EventType.SCROLL) - return Clutter.EVENT_PROPAGATE - - // Since Clutter 1.10, clutter will always send a smooth scrolling event - // with explicit deltas, no matter what input device is used - // In fact, for every scroll there will be a smooth and non-smooth scroll - // event, and we can choose which one we interpret. - if (event.get_scroll_direction() == Clutter.ScrollDirection.SMOOTH) { - let [ dx, dy ] = event.get_scroll_delta() - - this._indicator.scroll(dx, dy) - } - - return Clutter.EVENT_STOP } // called when the icon theme changes _invalidateIcon() { - this._iconCache.clear() - - this._updateIcon() - this._updateOverlayIcon() + this._iconCache.clear(); + this._cancelLoading(); + + this._updateIcon(); + this._updateOverlayIcon(); } }); diff --git a/dbusMenu.js b/dbusMenu.js index aaeff5b..3f4bf63 100644 --- a/dbusMenu.js +++ b/dbusMenu.js @@ -13,43 +13,42 @@ // 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 Atk = imports.gi.Atk -const Clutter = imports.gi.Clutter -const Gio = imports.gi.Gio -const GLib = imports.gi.GLib -const GdkPixbuf = imports.gi.GdkPixbuf -const PopupMenu = imports.ui.popupMenu -const Signals = imports.signals -const St = imports.gi.St - -const Extension = imports.misc.extensionUtils.getCurrentExtension() - -const DBusInterfaces = Extension.imports.interfaces -const Util = Extension.imports.util - -////////////////////////////////////////////////////////////////////////// +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GdkPixbuf = imports.gi.GdkPixbuf; +const PopupMenu = imports.ui.popupMenu; +const Signals = imports.signals; +const St = imports.gi.St; + +const Extension = imports.misc.extensionUtils.getCurrentExtension(); + +const DBusInterfaces = Extension.imports.interfaces; +const PromiseUtils = Extension.imports.promiseUtils; +const Util = Extension.imports.util; + +// //////////////////////////////////////////////////////////////////////// // PART ONE: "ViewModel" backend implementation. // Both code and design are inspired by libdbusmenu -////////////////////////////////////////////////////////////////////////// +// //////////////////////////////////////////////////////////////////////// /** * Saves menu property values and handles type checking and defaults */ -var PropertyStore = class AppIndicators_PropertyStore { - - constructor(initial_properties) { +var PropertyStore = class AppIndicatorsPropertyStore { + + constructor(initialProperties) { this._props = new Map(); - if (initial_properties) { - for (let i in initial_properties) { - this.set(i, initial_properties[i]) - } + if (initialProperties) { + for (let i in initialProperties) + this.set(i, initialProperties[i]); + } } set(name, value) { if (name in PropertyStore.MandatedTypes && value && !value.is_of_type(PropertyStore.MandatedTypes[name])) - Util.Logger.warn("Cannot set property "+name+": type mismatch!") + Util.Logger.warn(`Cannot set property ${name}: type mismatch!`); else if (value) this._props.set(name, value); else @@ -61,140 +60,140 @@ if (prop) return prop; else if (name in PropertyStore.DefaultValues) - return PropertyStore.DefaultValues[name] + return PropertyStore.DefaultValues[name]; else - return null + return null; } }; // we list all the properties we know and use here, so we won' have to deal with unexpected type mismatches PropertyStore.MandatedTypes = { - 'visible' : GLib.VariantType.new("b"), - 'enabled' : GLib.VariantType.new("b"), - 'label' : GLib.VariantType.new("s"), - 'type' : GLib.VariantType.new("s"), - 'children-display' : GLib.VariantType.new("s"), - 'icon-name' : GLib.VariantType.new("s"), - 'icon-data' : GLib.VariantType.new("ay"), - 'toggle-type' : GLib.VariantType.new("s"), - 'toggle-state' : GLib.VariantType.new("i") -} + 'visible': GLib.VariantType.new('b'), + 'enabled': GLib.VariantType.new('b'), + 'label': GLib.VariantType.new('s'), + 'type': GLib.VariantType.new('s'), + 'children-display': GLib.VariantType.new('s'), + 'icon-name': GLib.VariantType.new('s'), + 'icon-data': GLib.VariantType.new('ay'), + 'toggle-type': GLib.VariantType.new('s'), + 'toggle-state': GLib.VariantType.new('i'), +}; PropertyStore.DefaultValues = { 'visible': GLib.Variant.new_boolean(true), 'enabled': GLib.Variant.new_boolean(true), - 'label' : GLib.Variant.new_string(''), - 'type' : GLib.Variant.new_string("standard") + 'label': GLib.Variant.new_string(''), + 'type': GLib.Variant.new_string('standard'), // elements not in here must return null -} +}; /** * Represents a single menu item */ -var DbusMenuItem = class AppIndicators_DbusMenuItem { +var DbusMenuItem = class AppIndicatorsDbusMenuItem { // will steal the properties object - constructor(client, id, properties, children_ids) { - this._client = client - this._id = id - this._propStore = new PropertyStore(properties) - this._children_ids = children_ids - } - - property_get(prop_name) { - let prop = this.property_get_variant(prop_name) - return prop ? prop.get_string()[0] : null - } - - property_get_variant(prop_name) { - return this._propStore.get(prop_name) - } - - property_get_bool(prop_name) { - let prop = this.property_get_variant(prop_name) - return prop ? prop.get_boolean() : false - } - - property_get_int(prop_name) { - let prop = this.property_get_variant(prop_name) - return prop ? prop.get_int32() : 0 - } - - property_set(prop, value) { - this._propStore.set(prop, value) - - this.emit('property-changed', prop, this.property_get_variant(prop)) - } - - get_children_ids() { - return this._children_ids.concat() // clone it! - } - - add_child(pos, child_id) { - this._children_ids.splice(pos, 0, child_id) - this.emit('child-added', this._client.get_item(child_id), pos) - } - - remove_child(child_id) { + constructor(client, id, properties, childrenIds) { + this._client = client; + this._id = id; + this._propStore = new PropertyStore(properties); + this._children_ids = childrenIds; + } + + propertyGet(propName) { + let prop = this.propertyGetVariant(propName); + return prop ? prop.get_string()[0] : null; + } + + propertyGetVariant(propName) { + return this._propStore.get(propName); + } + + propertyGetBool(propName) { + let prop = this.propertyGetVariant(propName); + return prop ? prop.get_boolean() : false; + } + + propertyGetInt(propName) { + let prop = this.propertyGetVariant(propName); + return prop ? prop.get_int32() : 0; + } + + propertySet(prop, value) { + this._propStore.set(prop, value); + + this.emit('property-changed', prop, this.propertyGetVariant(prop)); + } + + getChildrenIds() { + return this._children_ids.concat(); // clone it! + } + + addChild(pos, childId) { + this._children_ids.splice(pos, 0, childId); + this.emit('child-added', this._client.getItem(childId), pos); + } + + removeChild(childId) { // find it - let pos = -1 + let pos = -1; for (let i = 0; i < this._children_ids.length; ++i) { - if (this._children_ids[i] == child_id) { - pos = i - break + if (this._children_ids[i] === childId) { + pos = i; + break; } } if (pos < 0) { - Util.Logger.critical("Trying to remove child which doesn't exist") + Util.Logger.critical("Trying to remove child which doesn't exist"); } else { - this._children_ids.splice(pos, 1) - this.emit('child-removed', this._client.get_item(child_id)) - } - } - - move_child(child_id, newpos) { + this._children_ids.splice(pos, 1); + this.emit('child-removed', this._client.getItem(childId)); + } + } + + moveChild(childId, newPos) { // find the old position - let oldpos = -1 + let oldPos = -1; for (let i = 0; i < this._children_ids.length; ++i) { - if (this._children_ids[i] == child_id) { - oldpos = i - break + if (this._children_ids[i] === childId) { + oldPos = i; + break; } } - if (oldpos < 0) { - Util.Logger.critical("tried to move child which wasn't in the list") - return - } - - if (oldpos != newpos) { - this._children_ids.splice(oldpos, 1) - this._children_ids.splice(newpos, 0, child_id) - this.emit('child-moved', oldpos, newpos, this._client.get_item(child_id)) - } - } - - get_children() { - return this._children_ids.map(el => this._client.get_item(el)); - } - - handle_event(event, data, timestamp) { + if (oldPos < 0) { + Util.Logger.critical("tried to move child which wasn't in the list"); + return; + } + + if (oldPos !== newPos) { + this._children_ids.splice(oldPos, 1); + this._children_ids.splice(newPos, 0, childId); + this.emit('child-moved', oldPos, newPos, this._client.getItem(childId)); + } + } + + getChildren() { + return this._children_ids.map(el => this._client.getItem(el)); + } + + handleEvent(event, data, timestamp) { if (!data) - data = GLib.Variant.new_int32(0) - - this._client.send_event(this._id, event, data, timestamp) - } - - get_id() { - return this._id - } - - send_about_to_show() { - this._client.send_about_to_show(this._id) - } -} -Signals.addSignalMethods(DbusMenuItem.prototype) + data = GLib.Variant.new_int32(0); + + this._client.sendEvent(this._id, event, data, timestamp); + } + + getId() { + return this._id; + } + + sendAboutToShow() { + this._client.sendAboutToShow(this._id); + } +}; +Signals.addSignalMethods(DbusMenuItem.prototype); const BusClientProxy = Gio.DBusProxy.makeProxyWrapper(DBusInterfaces.DBusMenu); @@ -202,7 +201,7 @@ /** * The client does the heavy lifting of actually reading layouts and distributing events */ -var DBusClient = class AppIndicators_DBusClient { +var DBusClient = class AppIndicatorsDBusClient { constructor(busName, busPath) { this._cancellable = new Gio.Cancellable(); @@ -210,20 +209,20 @@ busName, busPath, this._clientReady.bind(this), - this._cancellable) + this._cancellable); this._items = new Map([ [ 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 // then the handler that completes the layout update will request another update - this._flagLayoutUpdateRequired = false - this._flagLayoutUpdateInProgress = false + this._flagLayoutUpdateRequired = false; + this._flagLayoutUpdateInProgress = false; // property requests are queued this._propertiesRequestedFor = new Set(/* ids */); @@ -238,72 +237,72 @@ return !!this._proxy.g_name_owner; } - get_root() { + getRoot() { return this._items.get(0); } _requestLayoutUpdate() { if (this._flagLayoutUpdateInProgress) - this._flagLayoutUpdateRequired = true + this._flagLayoutUpdateRequired = true; else - this._beginLayoutUpdate() - } - - _requestProperties(id) { + this._beginLayoutUpdate(); + } + + async _requestProperties(id) { + this._propertiesRequestedFor.add(id); + // if we don't have any requests queued, we'll need to add one - if (!this._propertiesRequestId) { - this._propertiesRequestId = GLib.idle_add( - GLib.PRIORITY_DEFAULT_IDLE, () => this._beginRequestProperties()) - } - - this._propertiesRequestedFor.add(id); + if (!this._propertiesRequest || !this._propertiesRequest.pending()) { + this._propertiesRequest = new PromiseUtils.IdlePromise( + GLib.PRIORITY_DEFAULT_IDLE, this._cancellable); + await this._propertiesRequest; + this._beginRequestProperties(); + } } _beginRequestProperties() { this._proxy.GetGroupPropertiesRemote( - Array.from(this._propertiesRequestedFor), - [], - this._cancellable, - this._endRequestProperties.bind(this)) + Array.from(this._propertiesRequestedFor), + [], + this._cancellable, + this._endRequestProperties.bind(this)); this._propertiesRequestedFor.clear(); - delete this._propertiesRequestId; - - return false + return false; } _endRequestProperties(result, error) { if (error) { if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) Util.Logger.warn(`Could not retrieve properties: ${error}`); - return + return; } // for some funny reason, the result array is hidden in an array result[0].forEach(([id, properties]) => { let item = this._items.get(id); if (!item) - return + return; for (let prop in properties) - item.property_set(prop, properties[prop]) + item.propertySet(prop, properties[prop]); }); } // Traverses the list of cached menu items and removes everyone that is not in the list // so we don't keep alive unused items _gcItems() { - let tag = new Date().getTime() - - let toTraverse = [ 0 ] + let tag = new Date().getTime(); + + let toTraverse = [0]; while (toTraverse.length > 0) { - let item = this.get_item(toTraverse.shift()) - item._dbusClientGcTag = tag - Array.prototype.push.apply(toTraverse, item.get_children_ids()) + let item = this.getItem(toTraverse.shift()); + item._dbusClientGcTag = tag; + Array.prototype.push.apply(toTraverse, item.getChildrenIds()); } this._items.forEach((i, id) => { - if (i._dbusClientGcTag != tag) + if (i._dbusClientGcTag !== tag) this._items.delete(id); }); } @@ -314,36 +313,36 @@ // 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' ], + ['type', 'children-display'], this._cancellable, - this._endLayoutUpdate.bind(this)) - - this._flagLayoutUpdateRequired = false - this._flagLayoutUpdateInProgress = true + this._endLayoutUpdate.bind(this)); + + this._flagLayoutUpdateRequired = false; + this._flagLayoutUpdateInProgress = true; } _endLayoutUpdate(result, error) { if (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 - } - - let [ revision, root ] = result - this._doLayoutUpdate(root) - this._gcItems() + return; + } + + let [revision_, root] = result; + this._doLayoutUpdate(root); + this._gcItems(); if (this._flagLayoutUpdateRequired) - this._beginLayoutUpdate() + this._beginLayoutUpdate(); else - this._flagLayoutUpdateInProgress = false + this._flagLayoutUpdateInProgress = false; } _doLayoutUpdate(item) { - let [ id, properties, children ] = item - - let childrenUnpacked = children.map(c => c.deep_unpack()) - let childrenIds = childrenUnpacked.map(c => c[0]) + let [id, properties, children] = item; + + let childrenUnpacked = children.map(c => c.deep_unpack()); + let childrenIds = childrenUnpacked.map(c => c[0]); // make sure all our children exist childrenUnpacked.forEach(c => this._doLayoutUpdate(c)); @@ -352,40 +351,40 @@ const menuItem = this._items.get(id); if (menuItem) { // we do, update our properties if necessary - for (let prop in properties) { - menuItem.property_set(prop, properties[prop]) - } + for (let prop in properties) + menuItem.propertySet(prop, properties[prop]); + // make sure our children are all at the right place, and exist - let oldChildrenIds = menuItem.get_children_ids() + let oldChildrenIds = menuItem.getChildrenIds(); for (let i = 0; i < childrenIds.length; ++i) { // try to recycle an old child - let oldChild = -1 + let oldChild = -1; for (let j = 0; j < oldChildrenIds.length; ++j) { - if (oldChildrenIds[j] == childrenIds[i]) { - oldChild = oldChildrenIds.splice(j, 1)[0] - break + if (oldChildrenIds[j] === childrenIds[i]) { + oldChild = oldChildrenIds.splice(j, 1)[0]; + break; } } if (oldChild < 0) { // no old child found, so create a new one! - menuItem.add_child(i, childrenIds[i]) + menuItem.addChild(i, childrenIds[i]); } else { // old child found, reuse it! - menuItem.move_child(childrenIds[i], i) + menuItem.moveChild(childrenIds[i], i); } } // remove any old children that weren't reused - oldChildrenIds.forEach(child_id => menuItem.remove_child(child_id)); + oldChildrenIds.forEach(c => menuItem.removeChild(c)); } else { // we don't, so let's create us this._items.set(id, new DbusMenuItem(this, id, properties, childrenIds)); - this._requestProperties(id) - } - - return id + this._requestProperties(id); + } + + return id; } _clientReady(result, error) { @@ -395,14 +394,14 @@ return; } - this._requestLayoutUpdate() + this._requestLayoutUpdate(); // listen for updated layouts and properties - this._proxy.connectSignal("LayoutUpdated", this._onLayoutUpdated.bind(this)) - this._proxy.connectSignal("ItemsPropertiesUpdated", this._onPropertiesUpdated.bind(this)) - } - - get_item(id) { + this._proxy.connectSignal('LayoutUpdated', this._onLayoutUpdated.bind(this)); + this._proxy.connectSignal('ItemsPropertiesUpdated', this._onPropertiesUpdated.bind(this)); + } + + getItem(id) { let item = this._items.get(id); if (!item) Util.Logger.warn(`trying to retrieve item for non-existing id ${id} !?`); @@ -410,83 +409,78 @@ } // we don't need to cache and burst-send that since it will not happen that frequently - send_about_to_show(id) { + sendAboutToShow(id) { /* Some indicators (you, dropbox!) don't use the right signature * and don't return a boolean, so we need to support both cases */ let connection = this._proxy.get_connection(); connection.call(this._proxy.get_name(), this._proxy.get_object_path(), - this._proxy.get_interface_name(), 'AboutToShow', - new GLib.Variant("(i)", [id]), null, - Gio.DBusCallFlags.NONE, -1, null, (proxy, res) => { - try { - let ret = proxy.call_finish(res); - if ((ret.is_of_type(new GLib.VariantType('(b)')) && + this._proxy.get_interface_name(), 'AboutToShow', + new GLib.Variant('(i)', [id]), null, + Gio.DBusCallFlags.NONE, -1, null, (proxy, res) => { + try { + let ret = proxy.call_finish(res); + if ((ret.is_of_type(new GLib.VariantType('(b)')) && ret.get_child_value(0).get_boolean()) || - ret.is_of_type(new GLib.VariantType('()'))) { - this._requestLayoutUpdate(); + ret.is_of_type(new GLib.VariantType('()'))) + this._requestLayoutUpdate(); + + } catch (e) { + Util.Logger.warn(`Impossible to send about-to-show to menu: ${e}`); } - } catch (e) { - Util.Logger.warn("Impossible to send about-to-show to menu: " + e); - } - }); - } - - send_event(id, event, params, timestamp) { + }); + } + + sendEvent(id, event, params, timestamp) { if (!this._proxy) - return + return; this._proxy.EventRemote(id, event, params, timestamp, this._cancellable, - () => { /* we don't care */ }) + () => { /* we don't care */ }); } _onLayoutUpdated() { - this._requestLayoutUpdate() + this._requestLayoutUpdate(); } _onPropertiesUpdated(proxy, name, [changed, removed]) { changed.forEach(([id, props]) => { let item = this._items.get(id); if (!item) - return + return; for (let prop in props) - item.property_set(prop, props[prop]) + item.propertySet(prop, props[prop]); }); removed.forEach(([id, propNames]) => { let item = this._items.get(id); if (!item) - return - - propNames.forEach(propName => item.property_set(propName, null)); + return; + + propNames.forEach(propName => item.propertySet(propName, null)); }); } destroy() { - this.emit('destroy') - - if (this._propertiesRequestId) { - GLib.Source.remove(this._propertiesRequestId); - delete this._propertiesRequestId; - } + this.emit('destroy'); this._cancellable.cancel(); - Signals._disconnectAll.apply(this._proxy) - - this._proxy = null - } -} -Signals.addSignalMethods(DBusClient.prototype) - -////////////////////////////////////////////////////////////////////////// + Signals._disconnectAll.apply(this._proxy); + + this._proxy = null; + } +}; +Signals.addSignalMethods(DBusClient.prototype); + +// //////////////////////////////////////////////////////////////////////// // PART TWO: "View" frontend implementation. -////////////////////////////////////////////////////////////////////////// +// //////////////////////////////////////////////////////////////////////// // https://bugzilla.gnome.org/show_bug.cgi?id=731514 // GNOME 3.10 and 3.12 can't open a nested submenu. // Patches have been written, but it's not clear when (if?) they will be applied. // We also don't know whether they will be backported to 3.10, so we will work around // it in the meantime. Offending versions can be clearly identified: -const NEED_NESTED_SUBMENU_FIX = '_setOpenedSubMenu' in PopupMenu.PopupMenu.prototype +const NEED_NESTED_SUBMENU_FIX = '_setOpenedSubMenu' in PopupMenu.PopupMenu.prototype; /** * Creates new wrapper menu items and injects methods for managing them at runtime. @@ -495,50 +489,51 @@ * handlers, so any `this` will refer to a menu item create in createItem */ const MenuItemFactory = { - createItem: function(client, dbusItem) { + createItem(client, dbusItem) { // first, decide whether it's a submenu or not - if (dbusItem.property_get("children-display") == "submenu") - var shellItem = new PopupMenu.PopupSubMenuMenuItem("FIXME") - else if (dbusItem.property_get("type") == "separator") - var shellItem = new PopupMenu.PopupSeparatorMenuItem('') + let shellItem; + if (dbusItem.propertyGet('children-display') === 'submenu') + shellItem = new PopupMenu.PopupSubMenuMenuItem('FIXME'); + else if (dbusItem.propertyGet('type') === 'separator') + shellItem = new PopupMenu.PopupSeparatorMenuItem(''); else - var shellItem = new PopupMenu.PopupMenuItem("FIXME") - - shellItem._dbusItem = dbusItem - shellItem._dbusClient = client + shellItem = new PopupMenu.PopupMenuItem('FIXME'); + + shellItem._dbusItem = dbusItem; + shellItem._dbusClient = client; if (shellItem instanceof PopupMenu.PopupMenuItem) { - shellItem._icon = new St.Icon({ style_class: 'popup-menu-icon', x_align: St.Align.END }) + shellItem._icon = new St.Icon({ style_class: 'popup-menu-icon', x_align: St.Align.END }); shellItem.add_child(shellItem._icon); shellItem.label.x_expand = true; } // initialize our state - MenuItemFactory._updateLabel.call(shellItem) - MenuItemFactory._updateOrnament.call(shellItem) - MenuItemFactory._updateImage.call(shellItem) - MenuItemFactory._updateVisible.call(shellItem) - MenuItemFactory._updateSensitive.call(shellItem) + MenuItemFactory._updateLabel.call(shellItem); + MenuItemFactory._updateOrnament.call(shellItem); + MenuItemFactory._updateImage.call(shellItem); + MenuItemFactory._updateVisible.call(shellItem); + MenuItemFactory._updateSensitive.call(shellItem); // initially create children if (shellItem instanceof PopupMenu.PopupSubMenuMenuItem) { - let children = dbusItem.get_children() - for (let i = 0; i < children.length; ++i) { - shellItem.menu.addMenuItem(MenuItemFactory.createItem(client, children[i])) - } + let children = dbusItem.getChildren(); + for (let i = 0; i < children.length; ++i) + shellItem.menu.addMenuItem(MenuItemFactory.createItem(client, children[i])); + } // now, connect various events - Util.connectSmart(dbusItem, 'property-changed', shellItem, MenuItemFactory._onPropertyChanged) - Util.connectSmart(dbusItem, 'child-added', shellItem, MenuItemFactory._onChildAdded) - Util.connectSmart(dbusItem, 'child-removed', shellItem, MenuItemFactory._onChildRemoved) - Util.connectSmart(dbusItem, 'child-moved', shellItem, MenuItemFactory._onChildMoved) - Util.connectSmart(shellItem, 'activate', shellItem, MenuItemFactory._onActivate) + Util.connectSmart(dbusItem, 'property-changed', shellItem, MenuItemFactory._onPropertyChanged); + Util.connectSmart(dbusItem, 'child-added', shellItem, MenuItemFactory._onChildAdded); + Util.connectSmart(dbusItem, 'child-removed', shellItem, MenuItemFactory._onChildRemoved); + Util.connectSmart(dbusItem, 'child-moved', shellItem, MenuItemFactory._onChildMoved); + Util.connectSmart(shellItem, 'activate', shellItem, MenuItemFactory._onActivate); if (shellItem.menu) - Util.connectSmart(shellItem.menu, "open-state-changed", shellItem, MenuItemFactory._onOpenStateChanged) - - return shellItem + Util.connectSmart(shellItem.menu, 'open-state-changed', shellItem, MenuItemFactory._onOpenStateChanged); + + return shellItem; }, _onOpenStateChanged(menu, open) { @@ -546,168 +541,170 @@ if (NEED_NESTED_SUBMENU_FIX) { // close our own submenus if (menu._openedSubMenu) - menu._openedSubMenu.close(false) + menu._openedSubMenu.close(false); // register ourselves and close sibling submenus if (menu._parent._openedSubMenu && menu._parent._openedSubMenu !== menu) - menu._parent._openedSubMenu.close(true) - - menu._parent._openedSubMenu = menu + menu._parent._openedSubMenu.close(true); + + menu._parent._openedSubMenu = menu; } - this._dbusItem.handle_event("opened", null, 0) - this._dbusItem.send_about_to_show() + this._dbusItem.handleEvent('opened', null, 0); + this._dbusItem.sendAboutToShow(); } else { if (NEED_NESTED_SUBMENU_FIX) { // close our own submenus if (menu._openedSubMenu) - menu._openedSubMenu.close(false) + menu._openedSubMenu.close(false); } - this._dbusItem.handle_event("closed", null, 0) + this._dbusItem.handleEvent('closed', null, 0); } }, _onActivate() { - this._dbusItem.handle_event("clicked", GLib.Variant.new("i", 0), 0) - }, - - _onPropertyChanged(dbusItem, prop, value) { - if (prop == "toggle-type" || prop == "toggle-state") - MenuItemFactory._updateOrnament.call(this) - else if (prop == "label") - MenuItemFactory._updateLabel.call(this) - else if (prop == "enabled") - MenuItemFactory._updateSensitive.call(this) - else if (prop == "visible") - MenuItemFactory._updateVisible.call(this) - else if (prop == "icon-name" || prop == "icon-data") - MenuItemFactory._updateImage.call(this) - else if (prop == "type" || prop == "children-display") - MenuItemFactory._replaceSelf.call(this) - //else - // Util.Logger.debug("Unhandled property change: "+prop) + this._dbusItem.handleEvent('clicked', GLib.Variant.new('i', 0), 0); + }, + + _onPropertyChanged(dbusItem, prop, _value) { + if (prop === 'toggle-type' || prop === 'toggle-state') + MenuItemFactory._updateOrnament.call(this); + else if (prop === 'label') + MenuItemFactory._updateLabel.call(this); + else if (prop === 'enabled') + MenuItemFactory._updateSensitive.call(this); + else if (prop === 'visible') + MenuItemFactory._updateVisible.call(this); + else if (prop === 'icon-name' || prop === 'icon-data') + MenuItemFactory._updateImage.call(this); + else if (prop === 'type' || prop === 'children-display') + MenuItemFactory._replaceSelf.call(this); + else + Util.Logger.debug(`Unhandled property change: ${prop}`); }, _onChildAdded(dbusItem, child, position) { if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) { - Util.Logger.warn("Tried to add a child to non-submenu item. Better recreate it as whole") - MenuItemFactory._replaceSelf.call(this) + Util.Logger.warn('Tried to add a child to non-submenu item. Better recreate it as whole'); + MenuItemFactory._replaceSelf.call(this); } else { - this.menu.addMenuItem(MenuItemFactory.createItem(this._dbusClient, child), position) + this.menu.addMenuItem(MenuItemFactory.createItem(this._dbusClient, child), position); } }, _onChildRemoved(dbusItem, child) { if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) { - Util.Logger.warn("Tried to remove a child from non-submenu item. Better recreate it as whole") - MenuItemFactory._replaceSelf.call(this) + Util.Logger.warn('Tried to remove a child from non-submenu item. Better recreate it as whole'); + MenuItemFactory._replaceSelf.call(this); } else { // find it! - this.menu._getMenuItems().forEach((item) => { - if (item._dbusItem == child) - item.destroy() - }) + this.menu._getMenuItems().forEach(item => { + if (item._dbusItem === child) + item.destroy(); + }); } }, _onChildMoved(dbusItem, child, oldpos, newpos) { if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) { - Util.Logger.warn("Tried to move a child in non-submenu item. Better recreate it as whole") - MenuItemFactory._replaceSelf.call(this) + Util.Logger.warn('Tried to move a child in non-submenu item. Better recreate it as whole'); + MenuItemFactory._replaceSelf.call(this); } else { - MenuUtils.moveItemInMenu(this.menu, child, newpos) + MenuUtils.moveItemInMenu(this.menu, child, newpos); } }, _updateLabel() { - let label = this._dbusItem.property_get("label").replace(/_([^_])/, "$1") + let label = this._dbusItem.propertyGet('label').replace(/_([^_])/, '$1'); if (this.label) // especially on GS3.8, the separator item might not even have a hidden label - this.label.set_text(label) + this.label.set_text(label); }, _updateOrnament() { - if (!this.setOrnament) return // separators and alike might not have gotten the polyfill - - if (this._dbusItem.property_get("toggle-type") == "checkmark" && this._dbusItem.property_get_int("toggle-state")) - this.setOrnament(PopupMenu.Ornament.CHECK) - else if (this._dbusItem.property_get("toggle-type") == "radio" && this._dbusItem.property_get_int("toggle-state")) - this.setOrnament(PopupMenu.Ornament.DOT) + if (!this.setOrnament) + return; // separators and alike might not have gotten the polyfill + + if (this._dbusItem.propertyGet('toggle-type') === 'checkmark' && this._dbusItem.propertyGetInt('toggle-state')) + this.setOrnament(PopupMenu.Ornament.CHECK); + else if (this._dbusItem.propertyGet('toggle-type') === 'radio' && this._dbusItem.propertyGetInt('toggle-state')) + this.setOrnament(PopupMenu.Ornament.DOT); else - this.setOrnament(PopupMenu.Ornament.NONE) + this.setOrnament(PopupMenu.Ornament.NONE); }, _updateImage() { - if (!this._icon) return // might be missing on submenus / separators - - let iconName = this._dbusItem.property_get("icon-name") - let iconData = this._dbusItem.property_get_variant("icon-data") + if (!this._icon) + return; // might be missing on submenus / separators + + let iconName = this._dbusItem.propertyGet('icon-name'); + let iconData = this._dbusItem.propertyGetVariant('icon-data'); if (iconName) - this._icon.icon_name = iconName + this._icon.icon_name = iconName; else if (iconData) - this._icon.gicon = GdkPixbuf.Pixbuf.new_from_stream(Gio.MemoryInputStream.new_from_bytes(iconData.get_data_as_bytes()), null) + this._icon.gicon = GdkPixbuf.Pixbuf.new_from_stream(Gio.MemoryInputStream.new_from_bytes(iconData.get_data_as_bytes()), null); }, _updateVisible() { - this.visible = this._dbusItem.property_get_bool("visible") + this.visible = this._dbusItem.propertyGetBool('visible'); }, _updateSensitive() { - this.setSensitive(this._dbusItem.property_get_bool("enabled")) + this.setSensitive(this._dbusItem.propertyGetBool('enabled')); }, _replaceSelf(newSelf) { // create our new self if needed if (!newSelf) - newSelf = MenuItemFactory.createItem(this._dbusClient, this._dbusItem) + newSelf = MenuItemFactory.createItem(this._dbusClient, this._dbusItem); // first, we need to find our old position - let pos = -1 - let family = this._parent._getMenuItems() + let pos = -1; + let family = this._parent._getMenuItems(); for (let i = 0; i < family.length; ++i) { if (family[i] === this) - pos = i + pos = i; } if (pos < 0) - throw new Error("DBusMenu: can't replace non existing menu item") + throw new Error("DBusMenu: can't replace non existing menu item"); // add our new self while we're still alive - this._parent.addMenuItem(newSelf, pos) + this._parent.addMenuItem(newSelf, pos); // now destroy our old self - this.destroy() - } -} + this.destroy(); + }, +}; /** * Utility functions not necessarily belonging into the item factory */ const MenuUtils = { moveItemInMenu(menu, dbusItem, newpos) { - //HACK: we're really getting into the internals of the PopupMenu implementation + // HACK: we're really getting into the internals of the PopupMenu implementation // First, find our wrapper. Children tend to lie. We do not trust the old positioning. - let family = menu._getMenuItems() + let family = menu._getMenuItems(); for (let i = 0; i < family.length; ++i) { - if (family[i]._dbusItem == dbusItem) { + if (family[i]._dbusItem === dbusItem) { // now, remove it - menu.box.remove_child(family[i]) + menu.box.remove_child(family[i]); // and add it again somewhere else - if (newpos < family.length && family[newpos] != family[i]) - menu.box.insert_child_below(family[i], family[newpos]) + if (newpos < family.length && family[newpos] !== family[i]) + menu.box.insert_child_below(family[i], family[newpos]); else - menu.box.add(family[i]) + menu.box.add(family[i]); // skip the rest - return + return; } } - } -} + }, +}; /** @@ -715,14 +712,14 @@ * * Something like a mini-god-object */ -var Client = class AppIndicators_Client { +var Client = class AppIndicatorsClient { constructor(busName, path) { - this._busName = busName - this._busPath = path - this._client = new DBusClient(busName, path) - this._rootMenu = null // the shell menu - this._rootItem = null // the DbusMenuItem for the root + this._busName = busName; + this._busPath = path; + this._client = new DBusClient(busName, path); + this._rootMenu = null; // the shell menu + this._rootItem = null; // the DbusMenuItem for the root } get isReady() { @@ -732,88 +729,89 @@ // this will attach the client to an already existing menu that will be used as the root menu. // it will also connect the client to be automatically destroyed when the menu dies. attachToMenu(menu) { - this._rootMenu = menu - this._rootItem = this._client.get_root() + this._rootMenu = menu; + this._rootItem = this._client.getRoot(); // cleanup: remove existing children (just in case) - this._rootMenu.removeAll() + this._rootMenu.removeAll(); if (NEED_NESTED_SUBMENU_FIX) - menu._setOpenedSubMenu = this._setOpenedSubmenu.bind(this) + menu._setOpenedSubMenu = this._setOpenedSubmenu.bind(this); // connect handlers - Util.connectSmart(menu, 'open-state-changed', this, '_onMenuOpened') - Util.connectSmart(menu, 'destroy', this, 'destroy') - - Util.connectSmart(this._rootItem, 'child-added', this, '_onRootChildAdded') - Util.connectSmart(this._rootItem, 'child-removed', this, '_onRootChildRemoved') - Util.connectSmart(this._rootItem, 'child-moved', this, '_onRootChildMoved') + Util.connectSmart(menu, 'open-state-changed', this, this._onMenuOpened); + Util.connectSmart(menu, 'destroy', this, this.destroy); + + Util.connectSmart(this._rootItem, 'child-added', this, this._onRootChildAdded); + Util.connectSmart(this._rootItem, 'child-removed', this, this._onRootChildRemoved); + Util.connectSmart(this._rootItem, 'child-moved', this, this._onRootChildMoved); // Dropbox requires us to call AboutToShow(0) first - this._rootItem.send_about_to_show() + this._rootItem.sendAboutToShow(); // fill the menu for the first time - this._rootItem.get_children().forEach(child => - this._rootMenu.addMenuItem(MenuItemFactory.createItem(this, child)) + this._rootItem.getChildren().forEach(child => + this._rootMenu.addMenuItem(MenuItemFactory.createItem(this, child)), ); } _setOpenedSubmenu(submenu) { if (!submenu) - return - - if (submenu._parent != this._rootMenu) - return + return; + + if (submenu._parent !== this._rootMenu) + return; if (submenu === this._openedSubMenu) - return + return; if (this._openedSubMenu && this._openedSubMenu.isOpen) - this._openedSubMenu.close(true) - - this._openedSubMenu = submenu + this._openedSubMenu.close(true); + + this._openedSubMenu = submenu; } _onRootChildAdded(dbusItem, child, position) { - this._rootMenu.addMenuItem(MenuItemFactory.createItem(this, child), position) + this._rootMenu.addMenuItem(MenuItemFactory.createItem(this, child), position); } _onRootChildRemoved(dbusItem, child) { // children like to play hide and seek // but we know how to find it for sure! - this._rootMenu._getMenuItems().forEach((item) => { - if (item._dbusItem == child) - item.destroy() - }) + this._rootMenu._getMenuItems().forEach(item => { + if (item._dbusItem === child) + item.destroy(); + }); } _onRootChildMoved(dbusItem, child, oldpos, newpos) { - MenuUtils.moveItemInMenu(this._rootMenu, dbusItem, newpos) + MenuUtils.moveItemInMenu(this._rootMenu, dbusItem, newpos); } _onMenuOpened(menu, state) { - if (!this._rootItem) return + if (!this._rootItem) + return; if (state) { if (this._openedSubMenu && this._openedSubMenu.isOpen) - this._openedSubMenu.close() - - this._rootItem.handle_event("opened", null, 0) - this._rootItem.send_about_to_show() + this._openedSubMenu.close(); + + this._rootItem.handleEvent('opened', null, 0); + this._rootItem.sendAboutToShow(); } else { - this._rootItem.handle_event("closed", null, 0) + this._rootItem.handleEvent('closed', null, 0); } } destroy() { - this.emit('destroy') + this.emit('destroy'); if (this._client) - this._client.destroy() - - this._client = null - this._rootItem = null - this._rootMenu = null - } -} -Signals.addSignalMethods(Client.prototype) + this._client.destroy(); + + this._client = null; + this._rootItem = null; + this._rootMenu = null; + } +}; +Signals.addSignalMethods(Client.prototype); diff --git a/extension.js b/extension.js index 0cc72fa..0b5ee60 100644 --- a/extension.js +++ b/extension.js @@ -13,82 +13,49 @@ // 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 Gtk = imports.gi.Gtk; -const Gdk = imports.gi.Gdk; -const Main = imports.ui.main; -const Mainloop = imports.mainloop; -const Extension = imports.misc.extensionUtils.getCurrentExtension() +/* exported init, enable, disable */ -const StatusNotifierWatcher = Extension.imports.statusNotifierWatcher -const Util = Extension.imports.util +const Extension = imports.misc.extensionUtils.getCurrentExtension(); + +const StatusNotifierWatcher = Extension.imports.statusNotifierWatcher; +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(); - watchDog.onVanished = maybe_enable_after_name_available; + watchDog = new Util.NameWatcher(StatusNotifierWatcher.WATCHER_BUS_NAME); + watchDog.connect('vanished', () => maybeEnableAfterNameAvailable()); - //HACK: we want to leave the watchdog alive when disabling the extension, + // HACK: we want to leave the watchdog alive when disabling the extension, // but if we are being reloaded, we destroy it since it could be considered // a leak and spams our log, too. - if (typeof global['--appindicator-extension-on-reload'] == 'function') - global['--appindicator-extension-on-reload']() + /* eslint-disable no-undef */ + if (typeof global['--appindicator-extension-on-reload'] === 'function') + global['--appindicator-extension-on-reload'](); global['--appindicator-extension-on-reload'] = () => { - Util.Logger.debug("Reload detected, destroying old watchdog") + Util.Logger.debug('Reload detected, destroying old watchdog'); watchDog.destroy(); - } + }; + /* eslint-enable no-undef */ } -//FIXME: when entering/leaving the lock screen, the extension might be enabled/disabled rapidly. +// FIXME: when entering/leaving the lock screen, the extension might be enabled/disabled rapidly. // This will create very bad side effects in case we were not done unowning the name while trying // to own it again. Since g_bus_unown_name doesn't fire any callback when it's done, we need to // monitor the bus manually to find out when the name vanished so we can reclaim it again. -function maybe_enable_after_name_available() { +function maybeEnableAfterNameAvailable() { // by the time we get called whe might not be enabled - if (isEnabled && (!watchDog.nameAcquired || !watchDog.isPresent) && statusNotifierWatcher === null) + if (isEnabled && (!watchDog.nameAcquired || !watchDog.nameOnBus) && statusNotifierWatcher === null) statusNotifierWatcher = new StatusNotifierWatcher.StatusNotifierWatcher(watchDog); } -function inner_enable() { - if (startupComplete && displayAvailable) { - isEnabled = true; - maybe_enable_after_name_available(); - } -} - function enable() { - // 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(); + isEnabled = true; + maybeEnableAfterNameAvailable(); } function disable() { @@ -98,34 +65,3 @@ statusNotifierWatcher = null; } } - -/** - * NameWatchdog will monitor the ork.kde.StatusNotifierWatcher bus name for us - */ -var NameWatchdog = class AppIndicators_NameWatchdog { - - constructor() { - this.onAppeared = null; - this.onVanished = null; - - // will be set in the handlers which are guaranteed to be called at least once - this.isPresent = false; - - this._watcher_id = Gio.DBus.session.watch_name("org.kde.StatusNotifierWatcher", 0, - this._appeared_handler.bind(this), this._vanished_handler.bind(this)); - } - - destroy() { - Gio.DBus.session.unwatch_name(this._watcher_id); - } - - _appeared_handler() { - this.isPresent = true; - if (this.onAppeared) this.onAppeared(); - } - - _vanished_handler() { - this.isPresent = false; - if (this.onVanished) this.onVanished(); - } -} diff --git a/iconCache.js b/iconCache.js index a715cfb..7074edd 100644 --- a/iconCache.js +++ b/iconCache.js @@ -14,10 +14,14 @@ // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -const GLib = imports.gi.GLib -const Gio = imports.gi.Gio +/* exported IconCache */ -const Util = imports.misc.extensionUtils.getCurrentExtension().imports.util; +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; + +const Extension = imports.misc.extensionUtils.getCurrentExtension(); +const PromiseUtils = Extension.imports.promiseUtils; +const Util = Extension.imports.util; // The icon cache caches icon objects in case they're reused shortly aftwerwards. // This is necessary for some indicators like skype which rapidly switch between serveral icons. @@ -29,10 +33,10 @@ const LIFETIME_TIMESPAN = 10; // seconds // how to use: see IconCache.add, IconCache.get -var IconCache = class AppIndicators_IconCache { +var IconCache = class AppIndicatorsIconCache { constructor() { this._cache = new Map(); - this._lifetime = new Map(); //we don't want to attach lifetime to the object + this._lifetime = new Map(); // we don't want to attach lifetime to the object } add(id, icon) { @@ -81,7 +85,7 @@ // marks all the icons as removable, if something doesn't claim them before weakClear() { - this._cache.forEach((icon) => icon.inUse = false); + this._cache.forEach(icon => (icon.inUse = false)); this._checkGC(); } @@ -103,18 +107,17 @@ return null; } - _checkGC() { - let cacheIsEmpty = this._cache.size == 0; + async _checkGC() { + let cacheIsEmpty = this._cache.size === 0; if (!cacheIsEmpty && !this._gcTimeout) { - Util.Logger.debug("IconCache: garbage collector started"); - this._gcTimeout = GLib.timeout_add_seconds( - GLib.PRIORITY_LOW, - GC_INTERVAL, - () => this._gc()); + Util.Logger.debug('IconCache: garbage collector started'); + this._gcTimeout = new PromiseUtils.TimeoutSecondsPromise(GC_INTERVAL, + GLib.PRIORITY_LOW); + await this._gcTimeout; } else if (cacheIsEmpty && this._gcTimeout) { - Util.Logger.debug("IconCache: garbage collector stopped"); - GLib.Source.remove(this._gcTimeout); + Util.Logger.debug('IconCache: garbage collector stopped'); + this._gcTimeout.cancel(); delete this._gcTimeout; } } @@ -122,13 +125,12 @@ _gc() { let time = new Date().getTime(); this._cache.forEach((icon, id) => { - if (icon.inUse) { + if (icon.inUse) Util.Logger.debug(`IconCache: ${id} is in use.`); - } else if (this._lifetime.get(id) < time) { + else if (this._lifetime.get(id) < time) this._remove(id); - } else { + else Util.Logger.debug(`IconCache: ${id} survived this round.`); - } }); return true; diff --git a/indicator-test-tool/testTool.js b/indicator-test-tool/testTool.js index 365b17d..1daa9fe 100755 --- a/indicator-test-tool/testTool.js +++ b/indicator-test-tool/testTool.js @@ -34,256 +34,256 @@ (() => { -var app = new Gtk.Application({ - application_id: null -}); - -var window = null; - -app.connect("activate", () => { - window.present(); -}); - -app.connect("startup", () => { - window = new Gtk.ApplicationWindow({ - title: "test", - application: app + var app = new Gtk.Application({ + application_id: null, }); - let getRandomIcon = () => - iconsPool[Math.floor(Math.random() * (iconsPool.length - 1))]; - - let setRandomIconPath = () => { - let iconName = getRandomIcon(); - let iconInfo = Gtk.IconTheme.get_default().lookup_icon(iconName, - 16, Gtk.IconLookupFlags.GENERIC_FALLBACK); - let iconFile = Gio.File.new_for_path(iconInfo.get_filename()); - let [, extension] = iconFile.get_basename().split('.'); - let newName = `${iconName}-${Math.floor(Math.random() * 100)}.${extension}`; - let newFile = Gio.File.new_for_path( - `${GLib.dir_make_tmp('indicator-test-XXXXXX')}/${newName}`); - iconFile.copy(newFile, Gio.FileCopyFlagsOVERWRITE, null, null); - - indicator.set_icon_theme_path(newFile.get_parent().get_path()); - indicator.set_icon(newFile.get_basename()); - } - - var menu = new Gtk.Menu(); - - var item = Gtk.MenuItem.new_with_label("A standard item"); - menu.append(item); - - item = Gtk.MenuItem.new_with_label("Foo"); - menu.append(item); - - item = Gtk.ImageMenuItem.new_with_label("Calculator"); - item.image = Gtk.Image.new_from_icon_name("gnome-calculator", Gtk.IconSize.MENU); - menu.append(item); - - item = Gtk.CheckMenuItem.new_with_label("Check me!"); - menu.append(item); - - item = Gtk.MenuItem.new_with_label("Blub"); - let sub = new Gtk.Menu(); - item.set_submenu(sub); - menu.append(item); - - item = Gtk.MenuItem.new_with_label("Blubdablub"); - sub.append(item); - - item = new Gtk.SeparatorMenuItem(); - menu.append(item); - - item = Gtk.MenuItem.new_with_label("Foo"); - menu.append(item); - - let submenu = new Gtk.Menu(); - item.set_submenu(submenu); - - item = Gtk.MenuItem.new_with_label("Hello"); - submenu.append(item); - - item = Gtk.MenuItem.new_with_label("Nested"); - submenu.append(item); - - let submenu1 = new Gtk.Menu(); - item.set_submenu(submenu1); - - item = Gtk.MenuItem.new_with_label("Another nested"); - submenu.append(item); - - let submenu2 = new Gtk.Menu(); - item.set_submenu(submenu2); - - item = Gtk.MenuItem.new_with_label("Some other item"); - submenu1.append(item); - - item = Gtk.MenuItem.new_with_label("abcdefg"); - submenu2.append(item); - - item = new Gtk.SeparatorMenuItem(); - menu.append(item); - - var group = []; - - for (let i = 0; i < 5; ++i) { - item = Gtk.RadioMenuItem.new_with_label(group, "Example Radio "+i); - group = Gtk.RadioMenuItem.prototype.get_group.apply(item)//.get_group(); - if (i == 1) - item.set_active(true); - menu.append(item); - } - - item = new Gtk.SeparatorMenuItem(); - menu.append(item); - - item = Gtk.MenuItem.new_with_label("Set Label"); - item.connect('activate', () => { - indicator.set_label(''+new Date().getTime(), 'Blub'); + var window = null; + + app.connect('activate', () => { + window.present(); }); - menu.append(item); - - item = Gtk.MenuItem.new_with_label("Unset Label"); - item.connect('activate', () => { - indicator.set_label('', ''); - }) - menu.append(item); - - item = Gtk.MenuItem.new_with_label("Autodestroy Label"); - item.connect('activate', () => { - let i = 30; - GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => { - indicator.set_label(i > 0 ? `Label timeout ${i--}` : '', ''); - return (i >= 0); - }); - }) - menu.append(item); - - item = Gtk.MenuItem.new_with_label('Set Random icon'); - item.connect('activate', () => indicator.set_icon(getRandomIcon())); - menu.append(item); - - item = Gtk.MenuItem.new_with_label('Set Random custom theme icon'); - item.connect('activate', setRandomIconPath); - menu.append(item); - - item = Gtk.CheckMenuItem.new_with_label('Toggle Label and Icon'); - item.connect('activate', (item) => { - if (item.get_active()) { + + app.connect('startup', () => { + window = new Gtk.ApplicationWindow({ + title: 'test', + application: app, + }); + + let getRandomIcon = () => + iconsPool[Math.floor(Math.random() * (iconsPool.length - 1))]; + + let setRandomIconPath = () => { + let iconName = getRandomIcon(); + let iconInfo = Gtk.IconTheme.get_default().lookup_icon(iconName, + 16, Gtk.IconLookupFlags.GENERIC_FALLBACK); + let iconFile = Gio.File.new_for_path(iconInfo.get_filename()); + let [, extension] = iconFile.get_basename().split('.'); + let newName = `${iconName}-${Math.floor(Math.random() * 100)}.${extension}`; + let newFile = Gio.File.new_for_path( + `${GLib.dir_make_tmp('indicator-test-XXXXXX')}/${newName}`); + iconFile.copy(newFile, Gio.FileCopyFlagsOVERWRITE, null, null); + + indicator.set_icon_theme_path(newFile.get_parent().get_path()); + indicator.set_icon(newFile.get_basename()); + }; + + var menu = new Gtk.Menu(); + + var item = Gtk.MenuItem.new_with_label('A standard item'); + menu.append(item); + + item = Gtk.MenuItem.new_with_label('Foo'); + menu.append(item); + + item = Gtk.ImageMenuItem.new_with_label('Calculator'); + item.image = Gtk.Image.new_from_icon_name('gnome-calculator', Gtk.IconSize.MENU); + menu.append(item); + + item = Gtk.CheckMenuItem.new_with_label('Check me!'); + menu.append(item); + + item = Gtk.MenuItem.new_with_label('Blub'); + let sub = new Gtk.Menu(); + item.set_submenu(sub); + menu.append(item); + + item = Gtk.MenuItem.new_with_label('Blubdablub'); + sub.append(item); + + item = new Gtk.SeparatorMenuItem(); + menu.append(item); + + item = Gtk.MenuItem.new_with_label('Foo'); + menu.append(item); + + let submenu = new Gtk.Menu(); + item.set_submenu(submenu); + + item = Gtk.MenuItem.new_with_label('Hello'); + submenu.append(item); + + item = Gtk.MenuItem.new_with_label('Nested'); + submenu.append(item); + + let submenu1 = new Gtk.Menu(); + item.set_submenu(submenu1); + + item = Gtk.MenuItem.new_with_label('Another nested'); + submenu.append(item); + + let submenu2 = new Gtk.Menu(); + item.set_submenu(submenu2); + + item = Gtk.MenuItem.new_with_label('Some other item'); + submenu1.append(item); + + item = Gtk.MenuItem.new_with_label('abcdefg'); + submenu2.append(item); + + item = new Gtk.SeparatorMenuItem(); + menu.append(item); + + var group = []; + + for (let i = 0; i < 5; ++i) { + item = Gtk.RadioMenuItem.new_with_label(group, `Example Radio ${i}`); + group = Gtk.RadioMenuItem.prototype.get_group.apply(item);// .get_group(); + if (i === 1) + item.set_active(true); + menu.append(item); + } + + item = new Gtk.SeparatorMenuItem(); + menu.append(item); + + item = Gtk.MenuItem.new_with_label('Set Label'); + item.connect('activate', () => { indicator.set_label(`${new Date().getTime()}`, 'Blub'); - item.connect('activate', () => indicator.set_icon(getRandomIcon())); - } else { + }); + menu.append(item); + + item = Gtk.MenuItem.new_with_label('Unset Label'); + item.connect('activate', () => { indicator.set_label('', ''); - indicator.set_icon(DEFAULT_ICON); - } - }) - menu.append(item); - let toggleBrandingItem = item; - - item = Gtk.CheckMenuItem.new_with_label('Toggle Attention'); - let toggleAttentionId = item.connect('activate', () => { - indicator.set_status(indicator.get_status() != AppIndicator.IndicatorStatus.ATTENTION ? - AppIndicator.IndicatorStatus.ATTENTION : - AppIndicator.IndicatorStatus.ACTIVE); + }); + menu.append(item); + + item = Gtk.MenuItem.new_with_label('Autodestroy Label'); + item.connect('activate', () => { + let i = 30; + GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => { + indicator.set_label(i > 0 ? `Label timeout ${i--}` : '', ''); + return i >= 0; + }); + }); + menu.append(item); + + item = Gtk.MenuItem.new_with_label('Set Random icon'); + item.connect('activate', () => indicator.set_icon(getRandomIcon())); + menu.append(item); + + item = Gtk.MenuItem.new_with_label('Set Random custom theme icon'); + item.connect('activate', setRandomIconPath); + menu.append(item); + + item = Gtk.CheckMenuItem.new_with_label('Toggle Label and Icon'); + item.connect('activate', it => { + if (it.get_active()) { + indicator.set_label(`${new Date().getTime()}`, 'Blub'); + item.connect('activate', () => indicator.set_icon(getRandomIcon())); + } else { + indicator.set_label('', ''); + indicator.set_icon(DEFAULT_ICON); + } + }); + menu.append(item); + let toggleBrandingItem = item; + + item = Gtk.CheckMenuItem.new_with_label('Toggle Attention'); + let toggleAttentionId = item.connect('activate', () => { + indicator.set_status(indicator.get_status() !== AppIndicator.IndicatorStatus.ATTENTION + ? AppIndicator.IndicatorStatus.ATTENTION + : AppIndicator.IndicatorStatus.ACTIVE); + }); + menu.append(item); + let toggleAttentionItem = item; + + item = new Gtk.SeparatorMenuItem(); + menu.append(item); + + /* Double separaptors test */ + + item = new Gtk.SeparatorMenuItem(); + menu.append(item); + + /* Simulate similar behavior of #226 and #236 */ + item = Gtk.CheckMenuItem.new_with_label('Crazy icons updates'); + item.connect('activate', it => { + if (it.get_active()) { + item._timeoutID = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 16, () => { + setRandomIconPath(); + indicator.set_label(`${new Date().getSeconds()}`, ''); + return GLib.SOURCE_CONTINUE; + }); + } else { + GLib.source_remove(item._timeoutID); + delete item._timeoutID; + } + }); + menu.append(item); + + item = Gtk.MenuItem.new_with_label('Hide for some time'); + item.connect('activate', () => { + indicator.set_status(AppIndicator.IndicatorStatus.PASSIVE); + GLib.timeout_add(0, 5000, () => { + indicator.set_status(AppIndicator.IndicatorStatus.ACTIVE); + return false; + }); + }); + menu.append(item); + + item = Gtk.MenuItem.new_with_label('Close in 5 seconds'); + item.connect('activate', () => { + GLib.timeout_add(0, 5000, () => { + app.quit(); + return false; + }); + }); + menu.append(item); + + menu.show_all(); + + var indicator = AppIndicator.Indicator.new('Hello', 'indicator-test', AppIndicator.IndicatorCategory.APPLICATION_STATUS); + + indicator.set_status(AppIndicator.IndicatorStatus.ACTIVE); + indicator.set_icon(DEFAULT_ICON); + indicator.set_attention_icon(ATTENTION_ICON); + indicator.set_menu(menu); + indicator.set_secondary_activate_target(toggleBrandingItem); + + indicator.connect('connection-changed', (_indicator, connected) => { + print(`Signal "connection-changed" emitted. Connected: ${connected}`); + }); + indicator.connect('new-attention-icon', () => { + print('Signal "new-attention-icon" emitted.'); + }); + indicator.connect('new-icon', () => { + let icon = ''; + if (indicator.get_status() === AppIndicator.IndicatorStatus.ATTENTION) + icon = indicator.get_attention_icon(); + else if (indicator.get_status() === AppIndicator.IndicatorStatus.ACTIVE) + icon = indicator.get_icon(); + + print(`Signal "new-icon" emitted. Icon: ${icon}`); + }); + indicator.connect('new-icon-theme-path', (_indicator, path) => { + print(`Signal "new-icon-theme-path" emitted. Path: ${path}`); + }); + indicator.connect('new-label', (_indicator, label, guide) => { + print(`Signal "new-label" emitted. Label: ${label}, Guide: ${guide}`); + }); + indicator.connect('new-status', (_indicator, status) => { + print(`Signal "new-status" emitted. Status: ${status}`); + + toggleAttentionItem.block_signal_handler(toggleAttentionId); + toggleAttentionItem.set_active(status === 'NeedsAttention'); + toggleAttentionItem.unblock_signal_handler(toggleAttentionId); + }); + indicator.connect('scroll-event', (_indicator, steps, direction) => { + print(`Signal "scroll-event" emitted. Steps: ${steps}, Direction: ${direction}`); + let currentIndex = iconsPool.indexOf(indicator.get_icon()); + let iconIndex; + + if (direction === ScrollType.UP) + iconIndex = (currentIndex + 1) % iconsPool.length; + else + iconIndex = (currentIndex <= 0 ? iconsPool.length : currentIndex) - 1; + + + indicator.set_icon(iconsPool[iconIndex]); + }); }); - menu.append(item); - let toggleAttentionItem = item; - - item = new Gtk.SeparatorMenuItem(); - menu.append(item); - - /* Double separaptors test */ - - item = new Gtk.SeparatorMenuItem(); - menu.append(item); - - /* Simulate similar behavior of #226 and #236 */ - item = Gtk.CheckMenuItem.new_with_label('Crazy icons updates'); - item.connect('activate', (item) => { - if (item.get_active()) { - item._timeoutID = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 16, () => { - setRandomIconPath(); - indicator.set_label(`${new Date().getSeconds()}`, ''); - return GLib.SOURCE_CONTINUE; - }); - } else { - GLib.source_remove(item._timeoutID); - delete item._timeoutID; - } - }); - menu.append(item); - - item = Gtk.MenuItem.new_with_label("Hide for some time"); - item.connect('activate', () => { - indicator.set_status(AppIndicator.IndicatorStatus.PASSIVE); - GLib.timeout_add(0, 5000, () => { - indicator.set_status(AppIndicator.IndicatorStatus.ACTIVE); - return false; - }); - }); - menu.append(item); - - item = Gtk.MenuItem.new_with_label("Close in 5 seconds"); - item.connect('activate', () => { - GLib.timeout_add(0, 5000, () => { - app.quit(); - return false; - }); - }); - menu.append(item); - - menu.show_all(); - - var indicator = AppIndicator.Indicator.new("Hello", "indicator-test", AppIndicator.IndicatorCategory.APPLICATION_STATUS); - - indicator.set_status(AppIndicator.IndicatorStatus.ACTIVE); - indicator.set_icon(DEFAULT_ICON); - indicator.set_attention_icon(ATTENTION_ICON); - indicator.set_menu(menu); - indicator.set_secondary_activate_target(toggleBrandingItem); - - indicator.connect("connection-changed", (indicator, connected) => { - print(`Signal "connection-changed" emitted. Connected: ${connected}`); - }); - indicator.connect("new-attention-icon", (indicator) => { - print(`Signal "new-attention-icon" emitted.`); - }); - indicator.connect("new-icon", (indicator) => { - let icon = ""; - if (indicator.get_status() == AppIndicator.IndicatorStatus.ATTENTION) - icon = indicator.get_attention_icon(); - else if (indicator.get_status() == AppIndicator.IndicatorStatus.ACTIVE) - icon = indicator.get_icon(); - - print(`Signal "new-icon" emitted. Icon: ${icon}`); - }); - indicator.connect("new-icon-theme-path", (indicator, path) => { - print(`Signal "new-icon-theme-path" emitted. Path: ${path}`); - }); - indicator.connect("new-label", (indicator, label, guide) => { - print(`Signal "new-label" emitted. Label: ${label}, Guide: ${guide}`); - }); - indicator.connect("new-status", (indicator, status) => { - print(`Signal "new-status" emitted. Status: ${status}`); - - toggleAttentionItem.block_signal_handler(toggleAttentionId); - toggleAttentionItem.set_active(status == 'NeedsAttention'); - toggleAttentionItem.unblock_signal_handler(toggleAttentionId); - }); - indicator.connect("scroll-event", (indicator, steps, direction) => { - print(`Signal "scroll-event" emitted. Steps: ${steps}, Direction: ${direction}`); - let currentIndex = iconsPool.indexOf(indicator.get_icon()); - let iconIndex; - - if (direction == ScrollType.UP) { - iconIndex = (currentIndex + 1) % iconsPool.length; - } else { - iconIndex = (currentIndex <= 0 ? iconsPool.length : currentIndex) - 1; - } - - indicator.set_icon(iconsPool[iconIndex]); - }); -}); -app.run(ARGV); + app.run(ARGV); })(); diff --git a/indicatorStatusIcon.js b/indicatorStatusIcon.js index 38f9f27..9807235 100644 --- a/indicatorStatusIcon.js +++ b/indicatorStatusIcon.js @@ -13,6 +13,9 @@ // 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. + +/* exported IndicatorStatusIcon */ + const Clutter = imports.gi.Clutter; const GObject = imports.gi.GObject; const St = imports.gi.St; @@ -20,13 +23,12 @@ const Main = imports.ui.main; const Panel = imports.ui.panel; const PanelMenu = imports.ui.panelMenu; -const PopupMenu = imports.ui.popupMenu; const Config = imports.misc.config; const ExtensionUtils = imports.misc.extensionUtils; const Extension = ExtensionUtils.getCurrentExtension(); -const AppIndicator = Extension.imports.appIndicator +const AppIndicator = Extension.imports.appIndicator; const DBusMenu = Extension.imports.dbusMenu; const Util = Extension.imports.util; @@ -34,9 +36,9 @@ * IndicatorStatusIcon implements an icon in the system status area */ var IndicatorStatusIcon = GObject.registerClass( -class AppIndicators_IndicatorStatusIcon extends PanelMenu.Button { +class AppIndicatorsIndicatorStatusIcon extends PanelMenu.Button { _init(indicator) { - super._init(0.5, indicator._uniqueId); + super._init(0.5, indicator.uniqueId); this._indicator = indicator; this._iconBox = new AppIndicator.IconActor(indicator, Panel.PANEL_ICON_SIZE); @@ -45,12 +47,11 @@ this.add_child(this._box); this._box.add_child(this._iconBox); - Util.connectSmart(this, 'button-press-event', this, '_boxClicked') - Util.connectSmart(this._indicator, 'ready', this, '_display') - Util.connectSmart(this._indicator, 'menu', this, '_updateMenu') - Util.connectSmart(this._indicator, 'label', this, '_updateLabel') - Util.connectSmart(this._indicator, 'status', this, '_updateStatus') + Util.connectSmart(this._indicator, 'ready', this, this._display); + Util.connectSmart(this._indicator, 'menu', this, this._updateMenu); + Util.connectSmart(this._indicator, 'label', this, this._updateLabel); + Util.connectSmart(this._indicator, 'status', this, this._updateStatus); Util.connectSmart(this._indicator, 'reset', this, () => { this._updateStatus(); this._updateLabel(); @@ -61,10 +62,10 @@ this._menuClient.destroy(); this._menuClient = null; } - }) + }); if (this._indicator.isReady) - this._display() + this._display(); } _updateLabel() { @@ -72,28 +73,27 @@ if (label) { if (!this._label || !this._labelBin) { this._labelBin = new St.Bin({ - y_align: ExtensionUtils.versionCheck(['3.34'], Config.PACKAGE_VERSION) ? - St.Align.MIDDLE : Clutter.ActorAlign.CENTER, + y_align: ExtensionUtils.versionCheck(['3.34'], Config.PACKAGE_VERSION) + ? St.Align.MIDDLE : Clutter.ActorAlign.CENTER, }); this._label = new St.Label(); this._labelBin.add_actor(this._label); this._box.add_actor(this._labelBin); } this._label.set_text(label); - if (!this._box.contains(this._labelBin)) this._box.add_actor(this._labelBin); //FIXME: why is it suddenly necessary? - } else { - if (this._label) { - this._labelBin.destroy_all_children(); - this._box.remove_actor(this._labelBin); - this._labelBin.destroy(); - delete this._labelBin; - delete this._label; - } + if (!this._box.contains(this._labelBin)) + this._box.add_actor(this._labelBin); // FIXME: why is it suddenly necessary? + } else if (this._label) { + this._labelBin.destroy_all_children(); + this._box.remove_actor(this._labelBin); + this._labelBin.destroy(); + delete this._labelBin; + delete this._label; } } _updateStatus() { - this.visible = this._indicator.status != AppIndicator.SNIStatus.PASSIVE; + this.visible = this._indicator.status !== AppIndicator.SNIStatus.PASSIVE; } _updateMenu() { @@ -105,7 +105,7 @@ if (this._indicator.menuPath) { this._menuClient = new DBusMenu.Client(this._indicator.busName, - this._indicator.menuPath); + this._indicator.menuPath); this._menuClient.attachToMenu(this.menu); } } @@ -115,26 +115,38 @@ this._updateStatus(); this._updateMenu(); - Main.panel.addToStatusArea("appindicator-"+this._indicator.uniqueId, this, 1, 'right') + Main.panel.addToStatusArea(`appindicator-${this._indicator.uniqueId}`, this, 1, 'right'); } - _boxClicked(actor, event) { + vfunc_button_press_event(buttonEvent) { // if middle mouse button clicked send SecondaryActivate dbus event and do not show appindicator menu - if (event.get_button() == 2) { + if (buttonEvent.button === 2) { Main.panel.menuManager._closeMenu(true, Main.panel.menuManager.activeMenu); this._indicator.secondaryActivate(); - return; + return Clutter.EVENT_STOP; } - //HACK: event should be a ClutterButtonEvent but we get only a ClutterEvent (why?) - // because we can't access click_count, we'll create our own double click detector. - var treshold = Clutter.Settings.get_default().double_click_time; - var now = new Date().getTime(); - if (this._lastClicked && (now - this._lastClicked) < treshold) { - this._lastClicked = null; //reset double click detector + if (buttonEvent.button === 1 && buttonEvent.click_count === 2) { this._indicator.open(); - } else { - this._lastClicked = now; + return Clutter.EVENT_STOP; } + + return Clutter.EVENT_PROPAGATE; + } + + vfunc_scroll_event(scrollEvent) { + // Since Clutter 1.10, clutter will always send a smooth scrolling event + // with explicit deltas, no matter what input device is used + // In fact, for every scroll there will be a smooth and non-smooth scroll + // event, and we can choose which one we interpret. + if (scrollEvent.direction === Clutter.ScrollDirection.SMOOTH) { + const event = Clutter.get_current_event(); + let [dx, dy] = event.get_scroll_delta(); + + this._indicator.scroll(dx, dy); + return Clutter.EVENT_STOP; + } + + return Clutter.EVENT_PROPAGATE; } }); diff --git a/interfaces-xml/Properties.xml b/interfaces-xml/Properties.xml deleted file mode 100644 index ddaefc2..0000000 --- a/interfaces-xml/Properties.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/interfaces.js b/interfaces.js index 4372a16..a1a8c84 100644 --- a/interfaces.js +++ b/interfaces.js @@ -14,31 +14,29 @@ // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -var StatusNotifierItem = loadInterfaceXml("StatusNotifierItem.xml") -const Properties = loadInterfaceXml("Properties.xml") -var StatusNotifierWatcher = loadInterfaceXml("StatusNotifierWatcher.xml") -var DBusMenu = loadInterfaceXml("DBusMenu.xml") +/* exported StatusNotifierItem, StatusNotifierWatcher, DBusMenu */ + +var StatusNotifierItem = loadInterfaceXml('StatusNotifierItem.xml'); +var StatusNotifierWatcher = loadInterfaceXml('StatusNotifierWatcher.xml'); +var DBusMenu = loadInterfaceXml('DBusMenu.xml'); // loads a xml file into an in-memory string function loadInterfaceXml(filename) { - let extension = imports.misc.extensionUtils.getCurrentExtension() - - let interfaces_dir = extension.dir.get_child("interfaces-xml") - - let file = interfaces_dir.get_child(filename) - - let [ result, contents ] = imports.gi.GLib.file_get_contents(file.get_path()) + const extension = imports.misc.extensionUtils.getCurrentExtension(); + const interfacesDir = extension.dir.get_child('interfaces-xml'); + const file = interfacesDir.get_child(filename); + let [result, contents] = imports.gi.GLib.file_get_contents(file.get_path()); if (result) { - //HACK: The "" + trick is important as hell because file_get_contents returns - // an object (WTF?) but Gio.makeProxyWrapper requires `typeof() == "string"` + // HACK: The "" + trick is important as hell because file_get_contents returns + // an object (WTF?) but Gio.makeProxyWrapper requires `typeof() === "string"` // Otherwise, it will try to check `instanceof XML` and fail miserably because there // is no `XML` on very recent SpiderMonkey releases (or, if SpiderMonkey is old enough, // will spit out a TypeError soon). if (contents instanceof Uint8Array) - contents = imports.byteArray.toString(contents); - return "" + contents + "" + contents = imports.byteArray.toString(contents); + return `${contents}`; } else { - throw new Error("AppIndicatorSupport: Could not load file: "+filename) + throw new Error(`AppIndicatorSupport: Could not load file: ${filename}`); } } diff --git a/lint/eslintrc-gjs.yml b/lint/eslintrc-gjs.yml new file mode 100644 index 0000000..58324e3 --- /dev/null +++ b/lint/eslintrc-gjs.yml @@ -0,0 +1,229 @@ +--- +env: + es6: true +extends: 'eslint:recommended' +rules: + array-bracket-newline: + - error + - consistent + array-bracket-spacing: + - error + - never + array-callback-return: error + arrow-parens: + - error + - as-needed + arrow-spacing: error + block-scoped-var: error + block-spacing: error + brace-style: error + # Waiting for this to have matured a bit in eslint + # camelcase: + # - error + # - properties: never + # allow: [^vfunc_, ^on_, _instance_init] + comma-dangle: + - error + - always-multiline + comma-spacing: + - error + - before: false + after: true + comma-style: + - error + - last + computed-property-spacing: error + curly: + - error + - multi-or-nest + - consistent + dot-location: + - error + - property + eol-last: error + eqeqeq: error + func-call-spacing: error + func-name-matching: error + func-style: + - error + - declaration + - allowArrowFunctions: true + indent: + - error + - 4 + - ignoredNodes: + # Allow not indenting the body of GObject.registerClass, since in the + # future it's intended to be a decorator + - 'CallExpression[callee.object.name=GObject][callee.property.name=registerClass] > ClassExpression:first-child' + # Allow dedenting chained member expressions + MemberExpression: 'off' + key-spacing: + - error + - beforeColon: false + afterColon: true + keyword-spacing: + - error + - before: true + after: true + linebreak-style: + - error + - unix + lines-between-class-members: error + max-nested-callbacks: error + max-statements-per-line: error + new-parens: error + no-array-constructor: error + no-await-in-loop: error + no-caller: error + no-constant-condition: + - error + - checkLoops: false + no-div-regex: error + no-empty: + - error + - allowEmptyCatch: true + no-extra-bind: error + no-extra-parens: + - error + - all + - conditionalAssign: false + nestedBinaryExpressions: false + returnAssign: false + no-implicit-coercion: + - error + - allow: + - '!!' + no-invalid-this: error + no-iterator: error + no-label-var: error + no-lonely-if: error + no-loop-func: error + no-nested-ternary: error + no-new-object: error + no-new-wrappers: error + no-octal-escape: error + no-proto: error + no-prototype-builtins: 'off' + no-restricted-properties: + - error + - object: Lang + property: bind + message: Use arrow notation or Function.prototype.bind() + - object: Lang + property: Class + message: Use ES6 classes + - object: imports + property: mainloop + message: Use GLib main loops and timeouts + no-restricted-syntax: + - error + - selector: >- + MethodDefinition[key.name="_init"] > + FunctionExpression[params.length=1] > + BlockStatement[body.length=1] + CallExpression[arguments.length=1][callee.object.type="Super"][callee.property.name="_init"] > + Identifier:first-child + message: _init() that only calls super._init() is unnecessary + - selector: >- + MethodDefinition[key.name="_init"] > + FunctionExpression[params.length=0] > + BlockStatement[body.length=1] + CallExpression[arguments.length=0][callee.object.type="Super"][callee.property.name="_init"] + message: _init() that only calls super._init() is unnecessary + no-return-assign: error + no-return-await: error + no-self-compare: error + no-shadow: error + no-shadow-restricted-names: error + no-spaced-func: error + no-tabs: error + no-template-curly-in-string: error + no-throw-literal: error + no-trailing-spaces: error + no-undef-init: error + no-unneeded-ternary: error + no-unused-expressions: error + no-unused-vars: + - error + # Vars use a suffix _ instead of a prefix because of file-scope private vars + - varsIgnorePattern: (^unused|_$) + argsIgnorePattern: ^(unused|_) + no-useless-call: error + no-useless-computed-key: error + no-useless-concat: error + no-useless-constructor: error + no-useless-rename: error + no-useless-return: error + no-whitespace-before-property: error + no-with: error + nonblock-statement-body-position: + - error + - below + object-curly-newline: + - error + - consistent: true + object-curly-spacing: error + object-shorthand: error + operator-assignment: error + operator-linebreak: error + # These may be a bit controversial, we can try them out and enable them later + # prefer-const: error + # prefer-destructuring: error + prefer-numeric-literals: error + prefer-promise-reject-errors: error + prefer-rest-params: error + prefer-spread: error + prefer-template: error + quotes: + - error + - single + - avoidEscape: true + require-await: error + rest-spread-spacing: error + semi: + - error + - always + semi-spacing: + - error + - before: false + after: true + semi-style: error + space-before-blocks: error + space-before-function-paren: + - error + - named: never + # for `function ()` and `async () =>`, preserve space around keywords + anonymous: always + asyncArrow: always + space-in-parens: error + space-infix-ops: + - error + - int32Hint: false + space-unary-ops: error + spaced-comment: error + switch-colon-spacing: error + symbol-description: error + template-curly-spacing: error + template-tag-spacing: error + unicode-bom: error + valid-jsdoc: + - error + - requireReturn: false + wrap-iife: + - error + - inside + yield-star-spacing: error + yoda: error +globals: + ARGV: readonly + Debugger: readonly + GIRepositoryGType: readonly + globalThis: readonly + imports: readonly + Intl: readonly + log: readonly + logError: readonly + print: readonly + printerr: readonly +parserOptions: + ecmaVersion: 2020 diff --git a/lint/eslintrc-legacy.yml b/lint/eslintrc-legacy.yml new file mode 100644 index 0000000..55e9a2b --- /dev/null +++ b/lint/eslintrc-legacy.yml @@ -0,0 +1,14 @@ +rules: + eqeqeq: off + indent: + - error + - 4 + - ignoredNodes: + - 'CallExpression[callee.object.name=GObject][callee.property.name=registerClass] > ClassExpression:first-child' + CallExpression: + arguments: first + ArrayExpression: first + ObjectExpression: first + MemberExpression: off + prefer-template: off + quotes: off diff --git a/lint/eslintrc-shell.yml b/lint/eslintrc-shell.yml new file mode 100644 index 0000000..bb0636f --- /dev/null +++ b/lint/eslintrc-shell.yml @@ -0,0 +1,31 @@ +rules: + camelcase: + - error + - properties: never + allow: [^vfunc_, ^on_] + consistent-return: error + key-spacing: + - error + - mode: minimum + beforeColon: false + afterColon: true + object-curly-spacing: + - error + - always + prefer-arrow-callback: error + +overrides: + - files: js/** + excludedFiles: + - js/portalHelper/* + globals: + global: readonly + _: readonly + C_: readonly + N_: readonly + ngettext: readonly + - files: subprojects/extensions-app/js/** + globals: + _: readonly + C_: readonly + N_: readonly diff --git a/metadata.json b/metadata.json index 7afc59c..9983bd7 100644 --- a/metadata.json +++ b/metadata.json @@ -3,7 +3,9 @@ "3.33", "3.34", "3.36", - "3.38" + "3.38", + "40.beta", + "40" ], "uuid": "appindicatorsupport@rgcjonas.gmail.com", "name": "KStatusNotifierItem/AppIndicator Support", diff --git a/promiseUtils.js b/promiseUtils.js new file mode 100644 index 0000000..95c7e17 --- /dev/null +++ b/promiseUtils.js @@ -0,0 +1,339 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported CancellablePromise, SignalConnectionPromise, IdlePromise, + TimeoutPromise, TimeoutSecondsPromise, MetaLaterPromise, _promisify, + _promisifySignals */ + +const { Gio, GLib, GObject, Meta } = imports.gi; +const Signals = imports.signals; + +var CancellablePromise = class extends Promise { + constructor(executor, cancellable) { + if (!(executor instanceof Function)) + throw TypeError('executor is not a function'); + + if (cancellable && !(cancellable instanceof Gio.Cancellable)) + throw TypeError('cancellable parameter is not a Gio.Cancellable'); + + let rejector; + let cancelled; + super((resolve, reject) => { + rejector = reject; + if (cancellable && cancellable.is_cancelled()) { + cancelled = true; + reject(new GLib.Error(Gio.IOErrorEnum, + Gio.IOErrorEnum.CANCELLED, 'Promise cancelled')); + } else { + executor(resolve, reject); + } + }); + + this._cancelled = cancelled; + this._rejector = rejector; + + this._cancellable = cancellable || null; + if (this._cancellable) + this._cancellable.connect(() => this.cancel()); + } + + get cancellable() { + return this._cancellable; + } + + then(...args) { + const ret = super.then(...args); + + /* Every time we call then() on this promise we'd get a new + * CancellablePromise however that won't have the properties that the + * root one has set, and then it won't be possible to cancel a promise + * chain from the last one. + * To allow this we keep track of the root promise, make sure that + * the same method on the root object is called during cancellation + * or any destruction method if you want this to work. */ + if (ret instanceof CancellablePromise) + ret._root = this._root || this; + + return ret; + } + + resolved() { + return !this.cancelled() && !!(this._root || this)._resolved; + } + + cancelled() { + return !!(this._root || this)._cancelled; + } + + pending() { + return !this.resolved() && !this.cancelled(); + } + + cancel() { + if (this._root) { + this._root.cancel(); + return this; + } + + if (!this._rejector) + throw new GObject.NotImplementedError(); + + this._cancelled = !this._resolved; + this._rejector(new GLib.Error(Gio.IOErrorEnum, + Gio.IOErrorEnum.CANCELLED, 'Promise cancelled')); + + return this; + } +}; + +var SignalConnectionPromise = class extends CancellablePromise { + constructor(object, signal, cancellable) { + if (arguments.length === 1 && object instanceof Function) { + super(object); + return; + } + + if (!(object.connect instanceof Function)) + throw new TypeError('Not a valid object'); + + if (object instanceof GObject.Object && + !GObject.signal_lookup(signal.split(':')[0], object.constructor.$gtype)) + throw new TypeError(`Signal ${signal} not found on object ${object}`); + + let id; + let destroyId; + super(resolve => { + id = object.connect(signal, (_obj, ...args) => { + this._resolved = !this.cancelled(); + this.disconnect(); + resolve(args.length === 1 ? args[0] : args); + }); + + if (!(object instanceof GObject.Object) || + GObject.signal_lookup('destroy', object.constructor.$gtype)) + destroyId = object.connect('destroy', () => this.cancel()); + }, cancellable); + + this._object = object; + this._id = id; + this._destroyId = destroyId; + } + + disconnect() { + if (this._root) { + this._root.disconnect(); + return this; + } + + if (this._id) { + this._object.disconnect(this._id); + if (this._destroyId) { + this._object.disconnect(this._destroyId); + this._destroyId = 0; + } + this._object = null; + this._id = 0; + } + return this; + } + + cancel() { + this.disconnect(); + return super.cancel(); + } +}; + +var GSourcePromise = class extends CancellablePromise { + constructor(gsource, priority, cancellable) { + if (arguments.length === 1 && gsource instanceof Function) { + super(gsource); + return; + } + + if (gsource.constructor.$gtype !== GLib.Source.$gtype) + throw new TypeError(`gsource ${gsource} is not of type GLib.Source`); + + if (!priority) + priority = GLib.PRIORITY_DEFAULT; + + super(resolve => { + gsource.set_callback(() => { + this._resolved = !this.cancelled(); + this.remove(); + resolve(); + return GLib.SOURCE_REMOVE; + }); + gsource.set_name(`[gnome-shell] Source promise ${ + new Error().stack.split('\n').filter(line => + !line.match(/promiseUtils\.js/))[0]}`); + gsource.attach(null); + }, cancellable); + + this._gsource = gsource; + } + + remove() { + if (this._root) { + this._root.remove(); + return this; + } + + if (this._gsource) { + this._gsource.destroy(); + this._gsource = null; + } + + return this; + } + + cancel() { + this.remove(); + return super.cancel(); + } +}; + +var IdlePromise = class extends GSourcePromise { + constructor(priority, cancellable) { + if (arguments.length === 1 && priority instanceof Function) { + super(priority); + return; + } + + if (priority === undefined) + priority = GLib.PRIORITY_DEFAULT_IDLE; + else if (!Number.isInteger(priority)) + throw TypeError('Invalid priority'); + + super(GLib.idle_source_new(), priority, cancellable); + } +}; + +var TimeoutPromise = class extends GSourcePromise { + constructor(interval, priority, cancellable) { + if (arguments.length === 1 && interval instanceof Function) { + super(interval); + return; + } + + if (!Number.isInteger(interval) || interval < 0) + throw TypeError('Invalid interval'); + + super(GLib.timeout_source_new(interval), priority, cancellable); + } +}; + +var TimeoutSecondsPromise = class extends GSourcePromise { + constructor(interval, priority, cancellable) { + if (arguments.length === 1 && interval instanceof Function) { + super(interval); + return; + } + + if (!Number.isInteger(interval) || interval < 0) + throw TypeError('Invalid interval'); + + super(GLib.timeout_source_new_seconds(interval), priority, cancellable); + } +}; + +var MetaLaterPromise = class extends CancellablePromise { + constructor(laterType, cancellable) { + if (arguments.length === 1 && laterType instanceof Function) { + super(laterType); + return; + } + + if (laterType && laterType.constructor.$gtype !== Meta.LaterType.$gtype) + throw new TypeError(`laterType ${laterType} is not of type Meta.LaterType`); + else if (!laterType) + laterType = Meta.LaterType.BEFORE_REDRAW; + + let id; + super(resolve => { + id = Meta.later_add(laterType, () => { + this._resolved = !this.cancelled(); + this.remove(); + resolve(); + return GLib.SOURCE_REMOVE; + }); + }, cancellable); + + this._id = id; + } + + remove() { + if (this._root) { + this._root.remove(); + return this; + } + + if (this._id) { + Meta.later_remove(this._id); + this._id = 0; + } + return this; + } + + cancel() { + this.remove(); + return super.cancel(); + } +}; + +function _promisifySignals(proto) { + if (proto.connect_once) + return; + + proto.connect_once = function (signal, cancellable) { + return new SignalConnectionPromise(this, signal, cancellable); + }; +} + +const addSignalMethods = Signals.addSignalMethods; +Signals.addSignalMethods = proto => { + addSignalMethods(proto); + _promisifySignals(proto); +}; + +_promisifySignals(GObject.Object.prototype); + +var _promisify = Gio._promisify; +if (imports.system.version < 16501) { + /* This is backported from upstream gjs, so that all the features are available */ + _promisify = function (proto, asyncFunc, finishFunc) { + if (proto[`_original_${asyncFunc}`] !== undefined) + return; + proto[`_original_${asyncFunc}`] = proto[asyncFunc]; + proto[asyncFunc] = function (...args) { + if (!args.every(arg => typeof arg !== 'function')) + return this[`_original_${asyncFunc}`](...args); + return new Promise((resolve, reject) => { + const callStack = new Error().stack.split('\n').filter(line => !line.match(/promisify/)).join('\n'); + this[`_original_${asyncFunc}`](...args, (source, res) => { + try { + const result = source !== null && source[finishFunc] !== undefined + ? source[finishFunc](res) + : proto[finishFunc](res); + if (Array.isArray(result) && result.length > 1 && result[0] === true) + result.shift(); + resolve(result); + } catch (error) { + if (error.stack) + error.stack += `### Promise created here: ###\n${callStack}`; + else + error.stack = callStack; + reject(error); + } + }); + }); + }; + }; +} + +if (!Promise.allSettled) { + Promise.allSettled = function (promises) { + let wrappedPromises = promises.map(p => Promise.resolve(p) + .then( + val => ({ status: 'fulfilled', value: val }), + err => ({ status: 'rejected', reason: err }))); + return Promise.all(wrappedPromises); + }; +} diff --git a/statusNotifierWatcher.js b/statusNotifierWatcher.js index d43e5f6..50194fe 100644 --- a/statusNotifierWatcher.js +++ b/statusNotifierWatcher.js @@ -14,51 +14,44 @@ // 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 Gtk = imports.gi.Gtk - -const Mainloop = imports.mainloop -const ShellConfig = imports.misc.config -const Signals = imports.signals - -const Extension = imports.misc.extensionUtils.getCurrentExtension() - -const AppIndicator = Extension.imports.appIndicator -const IndicatorStatusIcon = Extension.imports.indicatorStatusIcon -const Interfaces = Extension.imports.interfaces -const Util = Extension.imports.util +/* exported StatusNotifierWatcher */ + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; + +const Extension = imports.misc.extensionUtils.getCurrentExtension(); + +const AppIndicator = Extension.imports.appIndicator; +const IndicatorStatusIcon = Extension.imports.indicatorStatusIcon; +const Interfaces = Extension.imports.interfaces; +const PromiseUtils = Extension.imports.promiseUtils; +const Util = Extension.imports.util; // TODO: replace with org.freedesktop and /org/freedesktop when approved const KDE_PREFIX = 'org.kde'; -const WATCHER_BUS_NAME = KDE_PREFIX + '.StatusNotifierWatcher'; -const WATCHER_INTERFACE = WATCHER_BUS_NAME; +var WATCHER_BUS_NAME = `${KDE_PREFIX}.StatusNotifierWatcher`; const WATCHER_OBJECT = '/StatusNotifierWatcher'; const DEFAULT_ITEM_OBJECT_PATH = '/StatusNotifierItem'; - -const BUS_ADDRESS_REGEX = /([a-zA-Z0-9._-]+\.[a-zA-Z0-9.-]+)|(:[0-9]+\.[0-9]+)$/ /* * The StatusNotifierWatcher class implements the StatusNotifierWatcher dbus object */ -var StatusNotifierWatcher = class AppIndicators_StatusNotifierWatcher { +var StatusNotifierWatcher = class AppIndicatorsStatusNotifierWatcher { constructor(watchDog) { this._watchDog = watchDog; this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(Interfaces.StatusNotifierWatcher, this); this._dbusImpl.export(Gio.DBus.session, WATCHER_OBJECT); - this._cancellable = new Gio.Cancellable; + this._cancellable = new Gio.Cancellable(); this._everAcquiredName = false; this._ownName = Gio.DBus.session.own_name(WATCHER_BUS_NAME, - Gio.BusNameOwnerFlags.NONE, - this._acquiredName.bind(this), - this._lostName.bind(this)); + Gio.BusNameOwnerFlags.NONE, + this._acquiredName.bind(this), + this._lostName.bind(this)); this._items = new Map(); - this._nameWatcher = new Map(); - this._serviceWatcher = new Map(); this._seekStatusNotifierItems(); } @@ -69,20 +62,20 @@ _lostName() { if (this._everAcquiredName) - Util.Logger.debug('Lost name' + WATCHER_BUS_NAME); + Util.Logger.debug(`Lost name${WATCHER_BUS_NAME}`); else - Util.Logger.warn('Failed to acquire ' + WATCHER_BUS_NAME); + Util.Logger.warn(`Failed to acquire ${WATCHER_BUS_NAME}`); this._watchDog.nameAcquired = false; } // create a unique index for the _items dictionary - _getItemId(bus_name, obj_path) { - return bus_name + obj_path; - } - - _registerItem(service, bus_name, obj_path) { - let id = this._getItemId(bus_name, obj_path); + _getItemId(busName, objPath) { + return busName + objPath; + } + + async _registerItem(service, busName, objPath) { + let id = this._getItemId(busName, objPath); if (this._items.has(id)) { Util.Logger.warn(`Item ${id} is already registered`); @@ -91,107 +84,126 @@ Util.Logger.debug(`Registering StatusNotifierItem ${id}`); - let indicator = new AppIndicator.AppIndicator(bus_name, obj_path); - let visual = new IndicatorStatusIcon.IndicatorStatusIcon(indicator); - indicator.connect('destroy', () => visual.destroy()); - - this._items.set(id, indicator); - - this._dbusImpl.emit_signal('StatusNotifierItemRegistered', GLib.Variant.new('(s)', service)); - this._nameWatcher.set(id, Gio.DBus.session.watch_name(bus_name, - Gio.BusNameWatcherFlags.NONE, null, - () => this._itemVanished(id))); - - if (service != bus_name && service.match(BUS_ADDRESS_REGEX)) { - this._serviceWatcher.set(id, Gio.DBus.session.watch_name(service, - Gio.BusNameWatcherFlags.NONE, null, - () => this._itemVanished(id))); - } - - this._dbusImpl.emit_property_changed('RegisteredStatusNotifierItems', GLib.Variant.new('as', this.RegisteredStatusNotifierItems)); - } - - _ensureItemRegistered(service, bus_name, obj_path) { - let id = this._getItemId(bus_name, obj_path); + try { + const indicator = new AppIndicator.AppIndicator(service, busName, objPath); + this._items.set(id, 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); + } + }); + + // if the desktop is not ready delay the icon creation and signal emissions + await Util.waitForStartupCompletion(indicator.cancellable); + const statusIcon = new IndicatorStatusIcon.IndicatorStatusIcon(indicator); + indicator.connect('destroy', () => statusIcon.destroy()); + + this._dbusImpl.emit_signal('StatusNotifierItemRegistered', + GLib.Variant.new('(s)', [indicator.uniqueId])); + this._dbusImpl.emit_property_changed('RegisteredStatusNotifierItems', + GLib.Variant.new('as', this.RegisteredStatusNotifierItems)); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + throw e; + } + } + + _ensureItemRegistered(service, busName, objPath) { + let id = this._getItemId(busName, objPath); let item = this._items.get(id); if (item) { - //delete the old one and add the new indicator + // delete the old one and add the new indicator Util.Logger.debug(`Attempting to re-register ${id}; resetting instead`); item.reset(); return; } - this._registerItem(service, bus_name, obj_path) - } - - _seekStatusNotifierItems() { + this._registerItem(service, busName, objPath); + } + + async _seekStatusNotifierItems() { // Some indicators (*coff*, dropbox, *coff*) do not re-register again // when the plugin is enabled/disabled, thus we need to manually look // for the objects in the session bus that implements the - // StatusNotifierItem interface... - Util.traverseBusNames(Gio.DBus.session, this._cancellable, (bus, name, cancellable) => { - Util.introspectBusObject(bus, name, cancellable, (node_info) => { - return Util.dbusNodeImplementsInterfaces(node_info, ['org.kde.StatusNotifierItem']); - }, (name, path) => { - let id = this._getItemId(name, path); - if (!this._items.has(id)) { - Util.Logger.debug(`Using Brute-force mode for StatusNotifierItem ${id}`); - this._registerItem(path, name, path); + // StatusNotifierItem interface... However let's do it after a low + // priority idle, so that it won't affect startup. + const cancellable = this._cancellable; + await new PromiseUtils.IdlePromise(GLib.PRIORITY_LOW, cancellable); + const bus = Gio.DBus.session; + const uniqueNames = await Util.getBusNames(bus, cancellable); + uniqueNames.forEach(async name => { + const nodes = await Util.introspectBusObject(bus, name, cancellable); + 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)) { + Util.Logger.warn(`Using Brute-force mode for StatusNotifierItem ${id}`); + this._registerItem(path, name, path); + } } - }) + }); }); } - RegisterStatusNotifierItemAsync(params, invocation) { + async RegisterStatusNotifierItemAsync(params, invocation) { // it would be too easy if all application behaved the same // instead, ayatana patched gnome apps to send a path // while kde apps send a bus name let [service] = params; - let bus_name = null, obj_path = null; - - if (service.charAt(0) == '/') { // looks like a path - bus_name = invocation.get_sender(); - obj_path = service; - } else if (service.match(BUS_ADDRESS_REGEX)) { - bus_name = Util.getUniqueBusNameSync(invocation.get_connection(), service); - obj_path = DEFAULT_ITEM_OBJECT_PATH; - } - - if (!bus_name || !obj_path) { - let error = "Impossible to register an indicator for parameters '"+ - service.toString()+"'"; + let busName, objPath; + + if (service.charAt(0) === '/') { // looks like a path + busName = invocation.get_sender(); + objPath = service; + } else if (service.match(Util.BUS_ADDRESS_REGEX)) { + try { + busName = await Util.getUniqueBusName(invocation.get_connection(), + service, this._cancellable); + } catch (e) { + logError(e); + } + objPath = DEFAULT_ITEM_OBJECT_PATH; + } + + if (!busName || !objPath) { + let error = `Impossible to register an indicator for parameters '${ + service.toString()}'`; Util.Logger.warn(error); invocation.return_dbus_error('org.gnome.gjs.JSError.ValueError', - error); + error); return; } - this._ensureItemRegistered(service, bus_name, obj_path); + 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)) { + if (this._items.has(id)) this._remove(id); - } + } _remove(id) { - this._items.get(id).destroy(); + const indicator = this._items.get(id); + const { uniqueId } = indicator; + indicator.destroy(); this._items.delete(id); - Gio.DBus.session.unwatch_name(this._nameWatcher.get(id)); - this._nameWatcher.delete(id); - - if (this._serviceWatcher.has(id)) { - Gio.DBus.session.unwatch_name(this._serviceWatcher.get(id)); - this._serviceWatcher.delete(id); - } - this._dbusImpl.emit_signal('StatusNotifierItemUnregistered', GLib.Variant.new('(s)', id)); - this._dbusImpl.emit_property_changed('RegisteredStatusNotifierItems', GLib.Variant.new('as', this.RegisteredStatusNotifierItems)); + + this._dbusImpl.emit_signal('StatusNotifierItemUnregistered', + GLib.Variant.new('(s)', [uniqueId])); + this._dbusImpl.emit_property_changed('RegisteredStatusNotifierItems', + GLib.Variant.new('as', this.RegisteredStatusNotifierItems)); } RegisterStatusNotifierHostAsync(_service, invocation) { @@ -206,7 +218,7 @@ } get RegisteredStatusNotifierItems() { - return Array.from(this._items.keys()); + return Array.from(this._items.values()).map(i => i.uniqueId); } get IsStatusNotifierHostRegistered() { @@ -221,14 +233,10 @@ if (!this._isDestroyed) { // 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)); Gio.DBus.session.unown_name(this._ownName); this._cancellable.cancel(); this._dbusImpl.unexport(); - this._nameWatcher.forEach(n => Gio.DBus.session.unwatch_name(n)); - delete this._nameWatcher; - this._serviceWatcher.forEach(s => Gio.DBus.session.unwatch_name(s)); - delete this._serviceWatcher; - this._items.forEach(i => i.destroy()); delete this._items; this._isDestroyed = true; } diff --git a/util.js b/util.js index ccf765e..5a77c65 100644 --- a/util.js +++ b/util.js @@ -13,79 +13,80 @@ // 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 GObject = imports.gi.GObject + +/* exported refreshPropertyOnProxy, getUniqueBusName, getBusNames, + introspectBusObject, dbusNodeImplementsInterfaces, waitForStartupCompletion, + connectSmart, BUS_ADDRESS_REGEX */ + +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 GObject = imports.gi.GObject; const Extension = imports.misc.extensionUtils.getCurrentExtension(); const Params = imports.misc.params; - -const Signals = imports.signals - -var refreshPropertyOnProxy = function(proxy, propertyName, params) { +const PromiseUtils = Extension.imports.promiseUtils; +const Signals = imports.signals; + +var BUS_ADDRESS_REGEX = /([a-zA-Z0-9._-]+\.[a-zA-Z0-9.-]+)|(:[0-9]+\.[0-9]+)$/; + +PromiseUtils._promisify(Gio.DBusConnection.prototype, 'call', 'call_finish'); + +async function refreshPropertyOnProxy(proxy, propertyName, params) { if (!proxy._proxyCancellables) proxy._proxyCancellables = new Map(); params = Params.parse(params, { - skipEqualtyCheck: false, + skipEqualityCheck: false, }); let cancellable = cancelRefreshPropertyOnProxy(proxy, { propertyName, - addNew: true + addNew: true, }); - 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, - (conn, result) => { - try { - let valueVariant = conn.call_finish(result).deep_unpack()[0]; + 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(); + + 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; + } + } 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); - - if (!params.skipEqualtyCheck && - 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._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, params) { + delete proxy._proxyChangedProperties[propertyName]; + } + } +} + +function cancelRefreshPropertyOnProxy(proxy, params) { if (!proxy._proxyCancellables) - return; + return null; params = Params.parse(params, { propertyName: undefined, @@ -107,130 +108,152 @@ return cancellable; } } else { - if (proxy._proxyPropertiesEmitId) { - GLib.source_remove(proxy._proxyPropertiesEmitId); - delete proxy._proxyPropertiesEmitId; - } proxy._proxyCancellables.forEach(c => c.cancel()); delete proxy._proxyChangedProperties; delete proxy._proxyCancellables; } -} - -var getUniqueBusNameSync = function(bus, name) { - if (name[0] == ':') + + return null; +} + +async function getUniqueBusName(bus, name, cancellable) { + if (name[0] === ':') return name; if (!bus) bus = Gio.DBus.session; - let variant_name = new GLib.Variant("(s)", [name]); - let [unique] = bus.call_sync("org.freedesktop.DBus", "/", "org.freedesktop.DBus", - "GetNameOwner", variant_name, null, - Gio.DBusCallFlags.NONE, -1, null).deep_unpack(); + const variantName = new GLib.Variant('(s)', [name]); + const [unique] = (await bus.call('org.freedesktop.DBus', '/', 'org.freedesktop.DBus', + 'GetNameOwner', variantName, new GLib.VariantType('(s)'), + Gio.DBusCallFlags.NONE, -1, cancellable)).deep_unpack(); return unique; } -var traverseBusNames = function(bus, cancellable, callback) { +async function getBusNames(bus, cancellable) { if (!bus) bus = Gio.DBus.session; - if (typeof(callback) !== "function") - throw new Error("No traversal callback provided"); - - bus.call("org.freedesktop.DBus", "/", "org.freedesktop.DBus", - "ListNames", null, new GLib.VariantType("(as)"), 0, -1, cancellable, - function (bus, task) { - if (task.had_error()) - return; - - let [names] = bus.call_finish(task).deep_unpack(); - let unique_names = new Set(); - - for (let name of names) { - unique_names.add(getUniqueBusNameSync(bus, name)); - } - - unique_names.forEach((name) => callback(bus, name, cancellable)); + const [names] = (await bus.call('org.freedesktop.DBus', '/', 'org.freedesktop.DBus', + 'ListNames', null, new GLib.VariantType('(as)'), Gio.DBusCallFlags.NONE, + -1, cancellable)).deep_unpack(); + + const uniqueNames = new Set(); + 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)) + Logger.debug(`Impossible to get the unique name of ${names[i]}: ${result.reason}`); + } + + return uniqueNames; +} + +async function introspectBusObject(bus, name, cancellable, path = undefined) { + if (!path) + path = '/'; + + const [introspection] = (await bus.call(name, path, 'org.freedesktop.DBus.Introspectable', + 'Introspect', null, new GLib.VariantType('(s)'), Gio.DBusCallFlags.NONE, + -1, cancellable)).deep_unpack(); + + const nodeInfo = Gio.DBusNodeInfo.new_for_xml(introspection); + const nodes = [{ nodeInfo, path }]; + + if (path === '/') + path = ''; + + const requests = []; + for (const subNodes of nodeInfo.nodes) { + const subPath = `${path}/${subNodes.path}`; + requests.push(introspectBusObject(bus, name, cancellable, subPath)); + } + + for (const result of await Promise.allSettled(requests)) { + if (result.status === 'fulfilled') + result.value.forEach(n => nodes.push(n)); + else if (!result.reason.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + Logger.debug(`Impossible to get node info: ${result.reason}`); + } + + return nodes; +} + +function dbusNodeImplementsInterfaces(nodeInfo, interfaces) { + if (!(nodeInfo instanceof Gio.DBusNodeInfo) || !Array.isArray(interfaces)) + return false; + + return interfaces.some(iface => nodeInfo.lookup_interface(iface)); +} + +var NameWatcher = class AppIndicatorsNameWatcher { + constructor(name) { + this._watcherId = Gio.DBus.session.watch_name(name, + Gio.BusNameWatcherFlags.NONE, () => { + this._nameOnBus = true; + Logger.debug(`Name ${name} appeared`); + this.emit('changed'); + this.emit('appeared'); + }, () => { + this._nameOnBus = false; + Logger.debug(`Name ${name} vanished`); + this.emit('changed'); + this.emit('vanished'); }); -} - -var introspectBusObject = function(bus, name, cancellable, filterFunction, targetCallback, path) { - if (!path) - path = "/"; - - if (typeof targetCallback !== "function") - throw new Error("No introspection callback defined"); - - bus.call (name, path, "org.freedesktop.DBus.Introspectable", "Introspect", - null, new GLib.VariantType("(s)"), Gio.DBusCallFlags.NONE, -1, - cancellable, function (bus, task) { - if (task.had_error()) - return; - - let introspection = bus.call_finish(task).deep_unpack().toString(); - let node_info = Gio.DBusNodeInfo.new_for_xml(introspection); - - if ((typeof filterFunction === "function" && filterFunction(node_info) === true) || - !filterFunction) { - targetCallback(name, path); - } - - if (path === "/") - path = "" - - for (let sub_nodes of node_info.nodes) { - let sub_path = path+"/"+sub_nodes.path; - introspectBusObject (bus, name, cancellable, filterFunction, - targetCallback, sub_path); - } - }); -} - -var dbusNodeImplementsInterfaces = function(node_info, interfaces) { - if (!(node_info instanceof Gio.DBusNodeInfo) || !Array.isArray(interfaces)) - return false; - - for (let iface of interfaces) { - if (node_info.lookup_interface(iface) !== null) - return true; - } - - return false; -} - -const connectSmart3A = function(src, signal, handler) { - let id = src.connect(signal, handler) + } + + destroy() { + this.emit('destroy'); + + Gio.DBus.session.unwatch_name(this._watcherId); + delete this._watcherId; + } + + get nameOnBus() { + return !!this._nameOnBus; + } +}; +Signals.addSignalMethods(NameWatcher.prototype); + +function connectSmart3A(src, signal, handler) { + let id = src.connect(signal, handler); if (src.connect && (!(src instanceof GObject.Object) || GObject.signal_lookup('destroy', src))) { - let destroy_id = src.connect('destroy', () => { - src.disconnect(id) - src.disconnect(destroy_id) - }) - } -} - -const connectSmart4A = function(src, signal, target, method) { - if (typeof method === 'string') - method = target[method].bind(target) - if (typeof method === 'function') - method = method.bind(target) - - let signal_id = src.connect(signal, method) + let destroyId = src.connect('destroy', () => { + src.disconnect(id); + src.disconnect(destroyId); + }); + } +} + +function connectSmart4A(src, signal, target, method) { + if (typeof method !== 'function') + throw new TypeError('Unsupported function'); + + method = method.bind(target); + const signalId = src.connect(signal, method); + const onDestroy = () => { + src.disconnect(signalId); + if (srcDestroyId) + src.disconnect(srcDestroyId); + if (tgtDestroyId) + target.disconnect(tgtDestroyId); + }; // GObject classes might or might not have a destroy signal // JS Classes will not complain when connecting to non-existent signals - let src_destroy_id = src.connect && (!(src instanceof GObject.Object) || GObject.signal_lookup('destroy', src)) ? src.connect('destroy', on_destroy) : 0 - let tgt_destroy_id = target.connect && (!(target instanceof GObject.Object) || GObject.signal_lookup('destroy', target)) ? target.connect('destroy', on_destroy) : 0 - - function on_destroy() { - src.disconnect(signal_id) - if (src_destroy_id) src.disconnect(src_destroy_id) - if (tgt_destroy_id) target.disconnect(tgt_destroy_id) - } -} - + const srcDestroyId = src.connect && (!(src instanceof GObject.Object) || + GObject.signal_lookup('destroy', src)) ? src.connect('destroy', onDestroy) : 0; + const tgtDestroyId = target.connect && (!(target instanceof GObject.Object) || + GObject.signal_lookup('destroy', target)) ? target.connect('destroy', onDestroy) : 0; +} + +// eslint-disable-next-line valid-jsdoc /** * Connect signals to slots, and remove the connection when either source or * target are destroyed @@ -240,20 +263,33 @@ * or * Util.connectSmart(srcOb, 'signal', () => { ... }) */ -var connectSmart = function() { - if (arguments.length == 4) - return connectSmart4A.apply(null, arguments) +function connectSmart(...args) { + if (arguments.length === 4) + return connectSmart4A(...args); else - return connectSmart3A.apply(null, arguments) + return connectSmart3A(...args); +} + +// eslint-disable-next-line valid-jsdoc +/** + * Helper function to wait for the system startup to be completed. + * Adding widgets before the desktop is ready to accept them can result in errors. + */ +async function waitForStartupCompletion(cancellable) { + if (Main.layoutManager._startingUp) + await Main.layoutManager.connect_once('startup-complete', cancellable); + + if (Gtk.IconTheme.get_default() === null) + await Gdk.DisplayManager.get().connect_once('display-opened', cancellable); } /** * Helper class for logging stuff */ -var Logger = class AppIndicators_Logger { +var Logger = class AppIndicatorsLogger { static _logStructured(logLevel, message, extraFields = {}) { if (!Object.values(GLib.LogLevelFlags).includes(logLevel)) { - _logStructured(GLib.LogLevelFlags.LEVEL_WARNING, + Logger._logStructured(GLib.LogLevelFlags.LEVEL_WARNING, 'logLevel is not a valid GLib.LogLevelFlags'); return; }