Linux puskom-ProLiant-DL385-Gen10 5.4.0-150-generic #167~18.04.1-Ubuntu SMP Wed May 24 00:51:42 UTC 2023 x86_64
/
usr
/
share
/
gnome-shell
/
extensions
/
ubuntu-appindicators@ubuntu.com
/
//usr/share/gnome-shell/extensions/ubuntu-appindicators@ubuntu.com/dbusMenu.js
// 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. 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 Lang = imports.lang 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 ////////////////////////////////////////////////////////////////////////// // PART ONE: "ViewModel" backend implementation. // Both code and design are inspired by libdbusmenu ////////////////////////////////////////////////////////////////////////// /** * Saves menu property values and handles type checking and defaults */ const PropertyStore = new Lang.Class({ Name: 'DbusMenuPropertyStore', _init: function(initial_properties) { this._props = {} if (initial_properties) { for (let i in initial_properties) { this.set(i, initial_properties[i]) } } }, set: function(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[name] = value else delete this._props[name] }, get: function(name) { if (name in this._props) return this._props[name] 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 */ const DbusMenuItem = new Lang.Class({ Name: 'DbusMenuItem', // will steal the properties object _init: function(client, id, properties, children_ids) { this._client = client this._id = id this._propStore = new PropertyStore(properties) this._children_ids = children_ids }, property_get: function(prop_name) { let prop = this.property_get_variant(prop_name) return prop ? prop.get_string()[0] : null }, property_get_variant: function(prop_name) { return this._propStore.get(prop_name) }, property_get_bool: function(prop_name) { let prop = this.property_get_variant(prop_name) return prop ? prop.get_boolean() : false }, property_get_int: function(prop_name) { let prop = this.property_get_variant(prop_name) return prop ? prop.get_int32() : 0 }, property_set: function(prop, value) { this._propStore.set(prop, value) this.emit('property-changed', prop, this.property_get_variant(prop)) }, get_children_ids: function() { return this._children_ids.concat() // clone it! }, add_child: function(pos, child_id) { this._children_ids.splice(pos, 0, child_id) this.emit('child-added', this._client.get_item(child_id), pos) }, remove_child: function(child_id) { // find it let pos = -1 for (let i = 0; i < this._children_ids.length; ++i) { if (this._children_ids[i] == child_id) { pos = i break } } if (pos < 0) { Util.Logger.fatal("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: function(child_id, newpos) { // find the old position let oldpos = -1 for (let i = 0; i < this._children_ids.length; ++i) { if (this._children_ids[i] == child_id) { oldpos = i break } } if (oldpos < 0) { Util.Logger.fatal("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: function() { return this._children_ids.map(function(el) { return this._client.get_item(el) }, this) }, handle_event: function(event, data, timestamp) { if (!data) data = GLib.Variant.new_int32(0) this._client.send_event(this._id, event, data, timestamp) }, get_id: function() { return this._id }, send_about_to_show: function() { this._client.send_about_to_show(this._id) } }) Signals.addSignalMethods(DbusMenuItem.prototype) const BusClientProxy = Gio.DBusProxy.makeProxyWrapper(DBusInterfaces.DBusMenu); /** * The client does the heavy lifting of actually reading layouts and distributing events */ const DBusClient = new Lang.Class({ Name: 'DbusMenuBusClient', _init: function(busName, busPath) { this._proxy = new BusClientProxy(Gio.DBus.session, busName, busPath, this._clientReady.bind(this)) this._items = { 0: new DbusMenuItem(this, 0, { 'children-display': GLib.Variant.new_string('submenu') }, []) } // will be set to true if a layout update is requested while one is already in progress // then the handler that completes the layout update will request another update this._flagLayoutUpdateRequired = false this._flagLayoutUpdateInProgress = false // property requests are queued this._propertiesRequestedFor = [ /* ids */ ] }, get_root: function() { return this._items[0] }, _requestLayoutUpdate: function() { if (this._flagLayoutUpdateInProgress) this._flagLayoutUpdateRequired = true else this._beginLayoutUpdate() }, _requestProperties: function(id) { // if we don't have any requests queued, we'll need to add one if (this._propertiesRequestedFor.length < 1) GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, this._beginRequestProperties.bind(this)) if (this._propertiesRequestedFor.filter(function(e) { return e === id }).length == 0) this._propertiesRequestedFor.push(id) }, _beginRequestProperties: function() { this._proxy.GetGroupPropertiesRemote(this._propertiesRequestedFor, [], this._endRequestProperties.bind(this)) this._propertiesRequestedFor = [] return false }, _endRequestProperties: function(result, error) { if (error) { Util.Logger.warn("Could not retrieve properties: "+error) return } // for some funny reason, the result array is hidden in an array result[0].forEach(function([id, properties]) { if (!(id in this._items)) return for (let prop in properties) this._items[id].property_set(prop, properties[prop]) }, this) }, // 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: function() { 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()) } for (let i in this._items) if (this._items[i]._dbusClientGcTag != tag) delete this._items[i] }, // 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: function() { // we only read the type property, because if the type changes after reading all properties, // the view would have to replace the item completely which we try to avoid this._proxy.GetLayoutRemote(0, -1, [ 'type', 'children-display' ], this._endLayoutUpdate.bind(this)) this._flagLayoutUpdateRequired = false this._flagLayoutUpdateInProgress = true }, _endLayoutUpdate: function(result, error) { if (error) { Util.Logger.warn("While reading menu layout: "+error) return } let [ revision, root ] = result this._doLayoutUpdate(root) this._gcItems() if (this._flagLayoutUpdateRequired) this._beginLayoutUpdate() else this._flagLayoutUpdateInProgress = false }, _doLayoutUpdate: function(item) { let [ id, properties, children ] = item let children_unpacked = children.map(function(child) { return child.deep_unpack() }) let children_ids = children_unpacked.map(function(child) { return child[0] }) // make sure all our children exist children_unpacked.forEach(this._doLayoutUpdate, this) // make sure we exist if (id in this._items) { // we do, update our properties if necessary for (let prop in properties) { this._items[id].property_set(prop, properties[prop]) } // make sure our children are all at the right place, and exist let old_children_ids = this._items[id].get_children_ids() for (let i = 0; i < children_ids.length; ++i) { // try to recycle an old child let old_child = -1 for (let j = 0; j < old_children_ids.length; ++j) { if (old_children_ids[j] == children_ids[i]) { old_child = old_children_ids.splice(j, 1)[0] break } } if (old_child < 0) { // no old child found, so create a new one! this._items[id].add_child(i, children_ids[i]) } else { // old child found, reuse it! this._items[id].move_child(children_ids[i], i) } } // remove any old children that weren't reused old_children_ids.forEach(function(child_id) { this._items[id].remove_child(child_id) }, this) } else { // we don't, so let's create us this._items[id] = new DbusMenuItem(this, id, properties, children_ids) this._requestProperties(id) } return id }, _clientReady: function(result, error) { if (error) { Util.Logger.warn("Could not initialize menu proxy: "+error) //FIXME: show message to the user? } 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: function(id) { if (id in this._items) return this._items[id] Util.Logger.warn("trying to retrieve item for non-existing id "+id+" !?") return null }, // we don't need to cache and burst-send that since it will not happen that frequently send_about_to_show: function(id) { this._proxy.AboutToShowRemote(id, (function(result, error) { if (error) Util.Logger.warn("while calling AboutToShow: "+error) else if (result && result[0]) this._requestLayoutUpdate() }).bind(this)) }, send_event: function(id, event, params, timestamp) { if (!this._proxy) return this._proxy.EventRemote(id, event, params, timestamp, function(result, error) { /* we don't care */ }) }, _onLayoutUpdated: function() { this._requestLayoutUpdate() }, _onPropertiesUpdated: function(proxy, name, [changed, removed]) { changed.forEach(function([id, props]) { if (!(id in this._items)) return for (let prop in props) this._items[id].property_set(prop, props[prop]) }, this) removed.forEach(function([id, propNames]) { if (!(id in this._items)) return propNames.forEach(function(propName) { this._items[id].property_set(propName, null) }, this) }, this) }, destroy: function() { this.emit('destroy') 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 /** * 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: function(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('') else var 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.actor.add(shellItem._icon, { x_align: St.Align.END }) shellItem.label.get_parent().child_set(shellItem.label, { 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) { let children = dbusItem.get_children() 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) if (shellItem.menu) Util.connectSmart(shellItem.menu, "open-state-changed", shellItem, MenuItemFactory._onOpenStateChanged) return shellItem }, _onOpenStateChanged: function(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.handle_event("opened", null, 0) this._dbusItem.send_about_to_show() } else { if (NEED_NESTED_SUBMENU_FIX) { // close our own submenus if (menu._openedSubMenu) menu._openedSubMenu.close(false) } this._dbusItem.handle_event("closed", null, 0) } }, _onActivate: function() { this._dbusItem.handle_event("clicked", GLib.Variant.new("i", 0), 0) }, _onPropertyChanged: function(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: function(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: function(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(function(item) { if (item._dbusItem == child) item.destroy() }) } }, _onChildMoved: function(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: function() { let label = this._dbusItem.property_get("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: function() { 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) else this.setOrnament(PopupMenu.Ornament.NONE) }, _updateImage: function() { 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 (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) }, _updateVisible: function() { this.actor.visible = this._dbusItem.property_get_bool("visible") }, _updateSensitive: function() { this.setSensitive(this._dbusItem.property_get_bool("enabled")) }, _replaceSelf: function(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 let 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: function(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. let 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].actor) // and add it again somewhere else if (newpos < family.length && family[newpos] != family[i]) menu.box.insert_child_below(family[i].actor, family[newpos].actor) else menu.box.add(family[i].actor) // skip the rest return } } } } /** * Processes DBus events, creates the menu items and handles the actions * * Something like a mini-god-object */ var Client = new Lang.Class({ Name: 'DbusMenuClient', _init: function(busName, path) { this.parent() 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 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: function(menu) { this._rootMenu = menu this._rootItem = this._client.get_root() // 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, '_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') // Dropbox requires us to call AboutToShow(0) first this._rootItem.send_about_to_show() // fill the menu for the first time this._rootItem.get_children().forEach(function(child) { this._rootMenu.addMenuItem(MenuItemFactory.createItem(this, child)) }, this) }, _setOpenedSubmenu: function(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: function(dbusItem, child, position) { this._rootMenu.addMenuItem(MenuItemFactory.createItem(this, child), position) }, _onRootChildRemoved: function(dbusItem, child) { // children like to play hide and seek // but we know how to find it for sure! this._rootMenu._getMenuItems().forEach(function(item) { if (item._dbusItem == child) item.destroy() }) }, _onRootChildMoved: function(dbusItem, child, oldpos, newpos) { MenuUtils.moveItemInMenu(this._rootMenu, dbusItem, newpos) }, _onMenuOpened: function(menu, state) { 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() } else { this._rootItem.handle_event("closed", null, 0) } }, destroy: function() { this.emit('destroy') if (this._client) this._client.destroy() this._client = null this._rootItem = null this._rootMenu = null } }) Signals.addSignalMethods(Client.prototype)