From 70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 Mon Sep 17 00:00:00 2001 From: AlexanderCurl Date: Sat, 18 Apr 2026 17:46:06 +0100 Subject: Replaced file structures for themes in the correct format --- .../dbusMenu.js | 990 +++++++++++++++++++++ 1 file changed, 990 insertions(+) create mode 100644 raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusMenu.js (limited to 'raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusMenu.js') diff --git a/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusMenu.js b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusMenu.js new file mode 100644 index 0000000..52c812d --- /dev/null +++ b/raveos-gnome-theme/theme-data/extensions/installed/appindicatorsupport@rgcjonas.gmail.com/dbusMenu.js @@ -0,0 +1,990 @@ +// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import Clutter from 'gi://Clutter'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import GdkPixbuf from 'gi://GdkPixbuf'; +import Gio from 'gi://Gio'; +import St from 'gi://St'; + +import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; +import * as Signals from 'resource:///org/gnome/shell/misc/signals.js'; + +import * as DBusInterfaces from './interfaces.js'; +import * as PromiseUtils from './promiseUtils.js'; +import * as Util from './util.js'; +import {DBusProxy} from './dbusProxy.js'; + +Gio._promisify(GdkPixbuf.Pixbuf, 'new_from_stream_async'); + +// //////////////////////////////////////////////////////////////////////// +// PART ONE: "ViewModel" backend implementation. +// Both code and design are inspired by libdbusmenu +// //////////////////////////////////////////////////////////////////////// + +/** + * Saves menu property values and handles type checking and defaults + */ +export class PropertyStore { + constructor(initialProperties) { + this._props = new Map(); + + if (initialProperties) { + for (const [prop, value] of Object.entries(initialProperties)) + this.set(prop, value); + } + } + + set(name, value) { + if (name in PropertyStore.MandatedTypes && value && + !value.is_of_type(PropertyStore.MandatedTypes[name])) + Util.Logger.warn(`Cannot set property ${name}: type mismatch!`); + else if (value) + this._props.set(name, value); + else + this._props.delete(name); + } + + get(name) { + const prop = this._props.get(name); + if (prop) + return prop; + else if (name in PropertyStore.DefaultValues) + return PropertyStore.DefaultValues[name]; + else + return null; + } +} + +// we list all the properties we know and use here, so we won' have to deal with unexpected type mismatches +PropertyStore.MandatedTypes = { + 'visible': GLib.VariantType.new('b'), + 'enabled': GLib.VariantType.new('b'), + 'label': GLib.VariantType.new('s'), + 'type': GLib.VariantType.new('s'), + 'children-display': GLib.VariantType.new('s'), + 'icon-name': GLib.VariantType.new('s'), + 'icon-data': GLib.VariantType.new('ay'), + 'toggle-type': GLib.VariantType.new('s'), + 'toggle-state': GLib.VariantType.new('i'), +}; + +PropertyStore.DefaultValues = { + 'visible': GLib.Variant.new_boolean(true), + 'enabled': GLib.Variant.new_boolean(true), + 'label': GLib.Variant.new_string(''), + 'type': GLib.Variant.new_string('standard'), + // elements not in here must return null +}; + +/** + * Represents a single menu item + */ +export class DbusMenuItem extends Signals.EventEmitter { + // will steal the properties object + constructor(client, id, properties, childrenIds) { + super(); + + this._client = client; + this._id = id; + this._propStore = new PropertyStore(properties); + this._children_ids = childrenIds; + } + + propertyGet(propName) { + const prop = this.propertyGetVariant(propName); + return prop ? prop.get_string()[0] : null; + } + + propertyGetVariant(propName) { + return this._propStore.get(propName); + } + + propertyGetBool(propName) { + const prop = this.propertyGetVariant(propName); + return prop ? prop.get_boolean() : false; + } + + propertyGetInt(propName) { + const prop = this.propertyGetVariant(propName); + return prop ? prop.get_int32() : 0; + } + + propertySet(prop, value) { + this._propStore.set(prop, value); + + this.emit('property-changed', prop, this.propertyGetVariant(prop)); + } + + resetProperties() { + Object.entries(PropertyStore.DefaultValues).forEach(([prop, value]) => + this.propertySet(prop, value)); + } + + getChildrenIds() { + return this._children_ids.concat(); // clone it! + } + + addChild(pos, childId) { + this._children_ids.splice(pos, 0, childId); + this.emit('child-added', this._client.getItem(childId), pos); + } + + removeChild(childId) { + // find it + let pos = -1; + for (let i = 0; i < this._children_ids.length; ++i) { + if (this._children_ids[i] === childId) { + pos = i; + break; + } + } + + if (pos < 0) { + Util.Logger.critical("Trying to remove child which doesn't exist"); + } else { + this._children_ids.splice(pos, 1); + this.emit('child-removed', this._client.getItem(childId)); + } + } + + moveChild(childId, newPos) { + // find the old position + let oldPos = -1; + for (let i = 0; i < this._children_ids.length; ++i) { + if (this._children_ids[i] === childId) { + oldPos = i; + break; + } + } + + if (oldPos < 0) { + Util.Logger.critical("tried to move child which wasn't in the list"); + return; + } + + if (oldPos !== newPos) { + this._children_ids.splice(oldPos, 1); + this._children_ids.splice(newPos, 0, childId); + this.emit('child-moved', oldPos, newPos, this._client.getItem(childId)); + } + } + + getChildren() { + return this._children_ids.map(el => this._client.getItem(el)); + } + + hasChildren() { + return this._children_ids.length > 0; + } + + handleEvent(event, data, timestamp) { + if (!data) + data = GLib.Variant.new_int32(0); + + return this._client.sendEvent(this._id, event, data, timestamp); + } + + getId() { + return this._id; + } + + sendAboutToShow() { + this._client.sendAboutToShow(this._id); + } +} + + +/** + * The client does the heavy lifting of actually reading layouts and distributing events + */ + +export const DBusClient = GObject.registerClass({ + Signals: {'ready-changed': {}}, +}, class AppIndicatorsDBusClient extends DBusProxy { + static get interfaceInfo() { + if (!this._interfaceInfo) { + this._interfaceInfo = Gio.DBusInterfaceInfo.new_for_xml( + DBusInterfaces.DBusMenu); + } + return this._interfaceInfo; + } + + static get baseItems() { + if (!this._baseItems) { + this._baseItems = { + 'children-display': GLib.Variant.new_string('submenu'), + }; + } + return this._baseItems; + } + + static destroy() { + delete this._interfaceInfo; + } + + _init(busName, objectPath) { + const {interfaceInfo} = AppIndicatorsDBusClient; + + super._init(busName, objectPath, interfaceInfo, + Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES); + + this._items = new Map(); + this._items.set(0, new DbusMenuItem(this, 0, DBusClient.baseItems, [])); + this._flagItemsUpdateRequired = false; + + // will be set to true if a layout update is needed once active + this._flagLayoutUpdateRequired = false; + + // property requests are queued + this._propertiesRequestedFor = new Set(/* ids */); + + this._layoutUpdated = false; + this._active = false; + } + + async initAsync(cancellable) { + await super.initAsync(cancellable); + + this._requestLayoutUpdate(); + } + + _onNameOwnerChanged() { + if (this.isReady) + this._requestLayoutUpdate(); + } + + get isReady() { + return this._layoutUpdated && !!this.gNameOwner; + } + + get cancellable() { + return this._cancellable; + } + + getRoot() { + return this._items.get(0); + } + + _requestLayoutUpdate() { + const cancellable = new Util.CancellableChild(this._cancellable); + this._beginLayoutUpdate(cancellable); + } + + async _requestProperties(propertyId, cancellable) { + this._propertiesRequestedFor.add(propertyId); + + if (this._propertiesRequest && this._propertiesRequest.pending()) + return; + + // if we don't have any requests queued, we'll need to add one + this._propertiesRequest = new PromiseUtils.IdlePromise( + GLib.PRIORITY_DEFAULT_IDLE, cancellable); + await this._propertiesRequest; + + const requestedProperties = Array.from(this._propertiesRequestedFor); + this._propertiesRequestedFor.clear(); + const [result] = await this.GetGroupPropertiesAsync(requestedProperties, + [], cancellable); + + result.forEach(([id, properties]) => { + const item = this._items.get(id); + if (!item) + return; + + item.resetProperties(); + + for (const [prop, value] of Object.entries(properties)) + item.propertySet(prop, value); + }); + } + + // Traverses the list of cached menu items and removes everyone that is not in the list + // so we don't keep alive unused items + _gcItems() { + const tag = new Date().getTime(); + + const toTraverse = [0]; + while (toTraverse.length > 0) { + const item = this.getItem(toTraverse.shift()); + item._dbusClientGcTag = tag; + Array.prototype.push.apply(toTraverse, item.getChildrenIds()); + } + + this._items.forEach((i, id) => { + if (i._dbusClientGcTag !== tag) + this._items.delete(id); + }); + } + + // the original implementation will only request partial layouts if somehow possible + // we try to save us from multiple kinds of race conditions by always requesting a full layout + _beginLayoutUpdate(cancellable) { + this._layoutUpdateUpdateAsync(cancellable).catch(e => { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + }); + } + + // the original implementation will only request partial layouts if somehow possible + // we try to save us from multiple kinds of race conditions by always requesting a full layout + async _layoutUpdateUpdateAsync(cancellable) { + // we only read the type property, because if the type changes after reading all properties, + // the view would have to replace the item completely which we try to avoid + if (this._layoutUpdateCancellable) + this._layoutUpdateCancellable.cancel(); + + this._layoutUpdateCancellable = cancellable; + + try { + const [revision_, root] = await this.GetLayoutAsync(0, -1, + ['type', 'children-display'], cancellable); + + this._updateLayoutState(true); + this._doLayoutUpdate(root, cancellable); + this._gcItems(); + this._flagLayoutUpdateRequired = false; + this._flagItemsUpdateRequired = false; + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + this._updateLayoutState(false); + throw e; + } finally { + if (this._layoutUpdateCancellable === cancellable) + this._layoutUpdateCancellable = null; + } + } + + _updateLayoutState(state) { + const wasReady = this.isReady; + this._layoutUpdated = state; + if (this.isReady !== wasReady) + this.emit('ready-changed'); + } + + _doLayoutUpdate(item, cancellable) { + const [id, properties, children] = item; + + const childrenUnpacked = children.map(c => c.deep_unpack()); + const childrenIds = childrenUnpacked.map(([c]) => c); + + // make sure all our children exist + childrenUnpacked.forEach(c => this._doLayoutUpdate(c, cancellable)); + + // make sure we exist + const menuItem = this._items.get(id); + + if (menuItem) { + // we do, update our properties if necessary + for (const [prop, value] of Object.entries(properties)) + menuItem.propertySet(prop, value); + + // make sure our children are all at the right place, and exist + const oldChildrenIds = menuItem.getChildrenIds(); + for (let i = 0; i < childrenIds.length; ++i) { + // try to recycle an old child + let oldChild = -1; + for (let j = 0; j < oldChildrenIds.length; ++j) { + if (oldChildrenIds[j] === childrenIds[i]) { + [oldChild] = oldChildrenIds.splice(j, 1); + break; + } + } + + if (oldChild < 0) { + // no old child found, so create a new one! + menuItem.addChild(i, childrenIds[i]); + } else { + // old child found, reuse it! + menuItem.moveChild(childrenIds[i], i); + } + } + + // remove any old children that weren't reused + oldChildrenIds.forEach(c => menuItem.removeChild(c)); + + if (!this._flagItemsUpdateRequired) + return id; + } + + // we don't, so let's create us + let newMenuItem = menuItem; + + if (!newMenuItem) { + newMenuItem = new DbusMenuItem(this, id, properties, childrenIds); + this._items.set(id, newMenuItem); + } + + this._requestProperties(id, cancellable).catch(e => { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + Util.Logger.warn(`Could not get menu properties menu proxy: ${e}`); + }); + + return id; + } + + async _doPropertiesUpdateAsync(cancellable) { + if (this._propertiesUpdateCancellable) + this._propertiesUpdateCancellable.cancel(); + + this._propertiesUpdateCancellable = cancellable; + + try { + const requests = []; + + this._items.forEach((_, id) => + requests.push(this._requestProperties(id, cancellable))); + + await Promise.all(requests); + } finally { + if (this._propertiesUpdateCancellable === cancellable) + this._propertiesUpdateCancellable = null; + } + } + + _doPropertiesUpdate() { + const cancellable = new Util.CancellableChild(this._cancellable); + this._doPropertiesUpdateAsync(cancellable).catch(e => { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + Util.Logger.warn(`Could not get menu properties menu proxy: ${e}`); + }); + } + + + set active(active) { + const wasActive = this._active; + this._active = active; + + if (active && wasActive !== active) { + if (this._flagLayoutUpdateRequired) { + this._requestLayoutUpdate(); + } else if (this._flagItemsUpdateRequired) { + this._doPropertiesUpdate(); + this._flagItemsUpdateRequired = false; + } + } + } + + _onSignal(_sender, signal, params) { + if (signal === 'LayoutUpdated') { + if (!this._active && this.getRoot()?.hasChildren()) { + this._flagLayoutUpdateRequired = true; + return; + } + + this._requestLayoutUpdate(); + } else if (signal === 'ItemsPropertiesUpdated') { + if (!this._active && this.getRoot()?.hasChildren()) { + this._flagItemsUpdateRequired = true; + return; + } + + this._onPropertiesUpdated(params.deep_unpack()); + } + } + + getItem(id) { + const item = this._items.get(id); + if (!item) + Util.Logger.warn(`trying to retrieve item for non-existing id ${id} !?`); + return item || null; + } + + // we don't need to cache and burst-send that since it will not happen that frequently + async sendAboutToShow(id) { + if (this._hasAboutToShow === false) + return; + + /* Some indicators (you, dropbox!) don't use the right signature + * and don't return a boolean, so we need to support both cases */ + try { + const ret = await this.gConnection.call(this.gName, this.gObjectPath, + this.gInterfaceName, 'AboutToShow', new GLib.Variant('(i)', [id]), + null, Gio.DBusCallFlags.NONE, -1, this._cancellable); + + if ((ret.is_of_type(new GLib.VariantType('(b)')) && + ret.get_child_value(0).get_boolean()) || + ret.is_of_type(new GLib.VariantType('()'))) + this._requestLayoutUpdate(); + } catch (e) { + Util.Logger.debug('Error when calling \'AboutToShow()\' in ' + + `${this.gName}, ${this.gObjectPath}, ${this.gInterfaceName}`); + if (e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_METHOD) || + e.matches(Gio.DBusError, Gio.DBusError.FAILED)) { + this._hasAboutToShow = false; + return; + } + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + } + } + + async sendEvent(id, event, params, timestamp) { + if (!this.gNameOwner) + return; + + try { + await this.EventAsync(id, event, params, timestamp, this._cancellable); + } catch (e) { + if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + return; + + throw e; + } + } + + _onPropertiesUpdated([changed, removed]) { + changed.forEach(([id, props]) => { + const item = this._items.get(id); + if (!item) + return; + + for (const [prop, value] of Object.entries(props)) + item.propertySet(prop, value); + }); + removed.forEach(([id, propNames]) => { + const item = this._items.get(id); + if (!item) + return; + + propNames.forEach(propName => item.propertySet(propName, null)); + }); + } +}); + +// //////////////////////////////////////////////////////////////////////// +// PART TWO: "View" frontend implementation. +// //////////////////////////////////////////////////////////////////////// + +// https://bugzilla.gnome.org/show_bug.cgi?id=731514 +// GNOME 3.10 and 3.12 can't open a nested submenu. +// Patches have been written, but it's not clear when (if?) they will be applied. +// We also don't know whether they will be backported to 3.10, so we will work around +// it in the meantime. Offending versions can be clearly identified: +const NEED_NESTED_SUBMENU_FIX = '_setOpenedSubMenu' in PopupMenu.PopupMenu.prototype; + +/** + * Creates new wrapper menu items and injects methods for managing them at runtime. + * + * Many functions in this object will be bound to the created item and executed as event + * handlers, so any `this` will refer to a menu item create in createItem + */ +const MenuItemFactory = { + createItem(client, dbusItem) { + // first, decide whether it's a submenu or not + let shellItem; + if (dbusItem.propertyGet('children-display') === 'submenu') + shellItem = new PopupMenu.PopupSubMenuMenuItem('FIXME'); + else if (dbusItem.propertyGet('type') === 'separator') + shellItem = new PopupMenu.PopupSeparatorMenuItem(''); + else + shellItem = new PopupMenu.PopupMenuItem('FIXME'); + + shellItem._dbusItem = dbusItem; + shellItem._dbusClient = client; + + if (shellItem instanceof PopupMenu.PopupMenuItem) { + shellItem._icon = new St.Icon({ + style_class: 'popup-menu-icon', + xAlign: Clutter.ActorAlign.END, + }); + shellItem.add_child(shellItem._icon); + shellItem.label.x_expand = true; + } + + // initialize our state + MenuItemFactory._updateLabel.call(shellItem); + MenuItemFactory._updateOrnament.call(shellItem); + MenuItemFactory._updateImage.call(shellItem); + MenuItemFactory._updateVisible.call(shellItem); + MenuItemFactory._updateSensitive.call(shellItem); + + // initially create children + if (shellItem instanceof PopupMenu.PopupSubMenuMenuItem) { + dbusItem.getChildren().forEach(c => + shellItem.menu.addMenuItem(MenuItemFactory.createItem(client, c))); + } + + // now, connect various events + Util.connectSmart(dbusItem, 'property-changed', + shellItem, MenuItemFactory._onPropertyChanged); + Util.connectSmart(dbusItem, 'child-added', + shellItem, MenuItemFactory._onChildAdded); + Util.connectSmart(dbusItem, 'child-removed', + shellItem, MenuItemFactory._onChildRemoved); + Util.connectSmart(dbusItem, 'child-moved', + shellItem, MenuItemFactory._onChildMoved); + Util.connectSmart(shellItem, 'activate', + shellItem, MenuItemFactory._onActivate); + + shellItem.connect('destroy', () => { + shellItem._dbusItemCancellable?.cancel(); + shellItem._dbusItemCancellable = null; + shellItem._dbusItem = null; + shellItem._dbusClient = null; + shellItem._icon = null; + }); + + if (shellItem.menu) { + Util.connectSmart(shellItem.menu, 'open-state-changed', + shellItem, MenuItemFactory._onOpenStateChanged); + } + + return shellItem; + }, + + _onOpenStateChanged(menu, open) { + if (open) { + if (NEED_NESTED_SUBMENU_FIX) { + // close our own submenus + if (menu._openedSubMenu) + menu._openedSubMenu.close(false); + + // register ourselves and close sibling submenus + if (menu._parent._openedSubMenu && menu._parent._openedSubMenu !== menu) + menu._parent._openedSubMenu.close(true); + + menu._parent._openedSubMenu = menu; + } + + this._dbusItem.handleEvent('opened', null, 0).catch(logError); + this._dbusItem.sendAboutToShow(); + } else { + if (NEED_NESTED_SUBMENU_FIX) { + // close our own submenus + if (menu._openedSubMenu) + menu._openedSubMenu.close(false); + } + + this._dbusItem.handleEvent('closed', null, 0).catch(logError); + } + }, + + _onActivate(_item, event) { + const timestamp = event.get_time(); + if (timestamp && this._dbusClient.indicator) + this._dbusClient.indicator.provideActivationToken(timestamp); + + this._dbusItem.handleEvent('clicked', GLib.Variant.new('i', 0), + timestamp).catch(logError); + }, + + _onPropertyChanged(dbusItem, prop, _value) { + if (prop === 'toggle-type' || prop === 'toggle-state') + MenuItemFactory._updateOrnament.call(this); + else if (prop === 'label') + MenuItemFactory._updateLabel.call(this); + else if (prop === 'enabled') + MenuItemFactory._updateSensitive.call(this); + else if (prop === 'visible') + MenuItemFactory._updateVisible.call(this); + else if (prop === 'icon-name' || prop === 'icon-data') + MenuItemFactory._updateImage.call(this); + else if (prop === 'type' || prop === 'children-display') + MenuItemFactory._replaceSelf.call(this); + else + Util.Logger.debug(`Unhandled property change: ${prop}`); + }, + + _onChildAdded(dbusItem, child, position) { + if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) { + Util.Logger.warn('Tried to add a child to non-submenu item. Better recreate it as whole'); + MenuItemFactory._replaceSelf.call(this); + } else { + this.menu.addMenuItem(MenuItemFactory.createItem(this._dbusClient, child), position); + } + }, + + _onChildRemoved(dbusItem, child) { + if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) { + Util.Logger.warn('Tried to remove a child from non-submenu item. Better recreate it as whole'); + MenuItemFactory._replaceSelf.call(this); + } else { + // find it! + this.menu._getMenuItems().forEach(item => { + if (item._dbusItem === child) + item.destroy(); + }); + } + }, + + _onChildMoved(dbusItem, child, oldpos, newpos) { + if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) { + Util.Logger.warn('Tried to move a child in non-submenu item. Better recreate it as whole'); + MenuItemFactory._replaceSelf.call(this); + } else { + MenuUtils.moveItemInMenu(this.menu, child, newpos); + } + }, + + _updateLabel() { + const label = this._dbusItem.propertyGet('label').replace(/_([^_])/, '$1'); + + if (this.label) // especially on GS3.8, the separator item might not even have a hidden label + this.label.set_text(label); + }, + + _updateOrnament() { + if (!this.setOrnament) + return; // separators and alike might not have gotten the polyfill + + if (this._dbusItem.propertyGet('toggle-type') === 'checkmark' && + this._dbusItem.propertyGetInt('toggle-state')) + this.setOrnament(PopupMenu.Ornament.CHECK); + else if (this._dbusItem.propertyGet('toggle-type') === 'radio' && + this._dbusItem.propertyGetInt('toggle-state')) + this.setOrnament(PopupMenu.Ornament.DOT); + else + this.setOrnament(PopupMenu.Ornament.NONE); + }, + + async _updateImage() { + if (!this._icon) + return; // might be missing on submenus / separators + + const iconName = this._dbusItem.propertyGet('icon-name'); + const iconData = this._dbusItem.propertyGetVariant('icon-data'); + if (iconName) { + this._icon.icon_name = iconName; + } else if (iconData) { + try { + if (!this._dbusItemCancellable) { + this._dbusItemCancellable = new Util.CancellableChild( + this._dbusClient.cancellable); + } + + const inputStream = Gio.MemoryInputStream.new_from_bytes( + iconData.get_data_as_bytes()); + this._icon.gicon = await GdkPixbuf.Pixbuf.new_from_stream_async( + inputStream, this._dbusItemCancellable); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + } + } + }, + + _updateVisible() { + this.visible = this._dbusItem.propertyGetBool('visible'); + }, + + _updateSensitive() { + this.setSensitive(this._dbusItem.propertyGetBool('enabled')); + }, + + _replaceSelf(newSelf) { + // create our new self if needed + if (!newSelf) + newSelf = MenuItemFactory.createItem(this._dbusClient, this._dbusItem); + + // first, we need to find our old position + let pos = -1; + const family = this._parent._getMenuItems(); + for (let i = 0; i < family.length; ++i) { + if (family[i] === this) + pos = i; + } + + if (pos < 0) + throw new Error("DBusMenu: can't replace non existing menu item"); + + + // add our new self while we're still alive + this._parent.addMenuItem(newSelf, pos); + + // now destroy our old self + this.destroy(); + }, +}; + +/** + * Utility functions not necessarily belonging into the item factory + */ +const MenuUtils = { + moveItemInMenu(menu, dbusItem, newpos) { + // HACK: we're really getting into the internals of the PopupMenu implementation + + // First, find our wrapper. Children tend to lie. We do not trust the old positioning. + const family = menu._getMenuItems(); + for (let i = 0; i < family.length; ++i) { + if (family[i]._dbusItem === dbusItem) { + // now, remove it + menu.box.remove_child(family[i]); + + // and add it again somewhere else + if (newpos < family.length && family[newpos] !== family[i]) + menu.box.insert_child_below(family[i], family[newpos]); + else + menu.box.add(family[i]); + + // skip the rest + return; + } + } + }, +}; + + +/** + * Processes DBus events, creates the menu items and handles the actions + * + * Something like a mini-god-object + */ +export class Client extends Signals.EventEmitter { + constructor(busName, path, indicator) { + super(); + + this._busName = busName; + this._busPath = path; + this._client = new DBusClient(busName, path); + this._rootMenu = null; // the shell menu + this._rootItem = null; // the DbusMenuItem for the root + this.indicator = indicator; + this.cancellable = new Util.CancellableChild(this.indicator.cancellable); + + this._client.initAsync(this.cancellable).catch(e => { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + }); + + Util.connectSmart(this._client, 'ready-changed', this, + () => this.emit('ready-changed')); + } + + get isReady() { + return this._client.isReady; + } + + // this will attach the client to an already existing menu that will be used as the root menu. + // it will also connect the client to be automatically destroyed when the menu dies. + attachToMenu(menu) { + this._rootMenu = menu; + this._rootItem = this._client.getRoot(); + this._itemsBeingAdded = new Set(); + + // cleanup: remove existing children (just in case) + this._rootMenu.removeAll(); + + if (NEED_NESTED_SUBMENU_FIX) + menu._setOpenedSubMenu = this._setOpenedSubmenu.bind(this); + + // connect handlers + Util.connectSmart(menu, 'open-state-changed', this, this._onMenuOpenStateChanged); + Util.connectSmart(menu, 'destroy', this, this.destroy); + + Util.connectSmart(this._rootItem, 'child-added', this, this._onRootChildAdded); + Util.connectSmart(this._rootItem, 'child-removed', this, this._onRootChildRemoved); + Util.connectSmart(this._rootItem, 'child-moved', this, this._onRootChildMoved); + + // Dropbox requires us to call AboutToShow(0) first + this._rootItem.sendAboutToShow(); + + // fill the menu for the first time + const children = this._rootItem.getChildren(); + children.forEach(child => + this._onRootChildAdded(this._rootItem, child)); + } + + _setOpenedSubmenu(submenu) { + if (!submenu) + return; + + if (submenu._parent !== this._rootMenu) + return; + + if (submenu === this._openedSubMenu) + return; + + if (this._openedSubMenu && this._openedSubMenu.isOpen) + this._openedSubMenu.close(true); + + this._openedSubMenu = submenu; + } + + _onRootChildAdded(dbusItem, child, position) { + // Menu additions can be expensive, so let's do it in different chunks + const basePriority = this.isOpen ? GLib.PRIORITY_DEFAULT : GLib.PRIORITY_LOW; + const idlePromise = new PromiseUtils.IdlePromise( + basePriority + this._itemsBeingAdded.size, this.cancellable); + this._itemsBeingAdded.add(child); + + idlePromise.then(() => { + if (!this._itemsBeingAdded.has(child)) + return; + + this._rootMenu.addMenuItem( + MenuItemFactory.createItem(this, child), position); + }).catch(e => { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + }).finally(() => this._itemsBeingAdded.delete(child)); + } + + _onRootChildRemoved(dbusItem, child) { + // children like to play hide and seek + // but we know how to find it for sure! + const item = this._rootMenu._getMenuItems().find(it => + it._dbusItem === child); + + if (item) + item.destroy(); + else + this._itemsBeingAdded.delete(child); + } + + _onRootChildMoved(dbusItem, child, oldpos, newpos) { + MenuUtils.moveItemInMenu(this._rootMenu, dbusItem, newpos); + } + + _onMenuOpenStateChanged(menu, state) { + if (!this._rootItem) + return; + + this._client.active = state; + + if (state) { + if (this._openedSubMenu && this._openedSubMenu.isOpen) + this._openedSubMenu.close(); + + this._rootItem.handleEvent('opened', null, 0).catch(logError); + this._rootItem.sendAboutToShow(); + } else { + this._rootItem.handleEvent('closed', null, 0).catch(e => { + if (e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_OBJECT)) { + // The menu hay have been removed at this point, thus do not + // spam the users about this if it happens. + return; + } + + logError(e); + }); + } + } + + destroy() { + this.emit('destroy'); + + if (this._client) + this._client.destroy(); + + this._client = null; + this._rootItem = null; + this._rootMenu = null; + this.indicator = null; + this._itemsBeingAdded = null; + } +} -- cgit v1.3