Add base gnome extensions
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,950 @@
|
||||
// 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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
handleEvent(event, data, timestamp) {
|
||||
if (!data)
|
||||
data = GLib.Variant.new_int32(0);
|
||||
|
||||
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;
|
||||
|
||||
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._flagLayoutUpdateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this._requestLayoutUpdate();
|
||||
} else if (signal === 'ItemsPropertiesUpdated') {
|
||||
if (!this._active) {
|
||||
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) {
|
||||
/* 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) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
sendEvent(id, event, params, timestamp) {
|
||||
if (!this.gNameOwner)
|
||||
return;
|
||||
|
||||
this.EventAsync(id, event, params, timestamp, this._cancellable).catch(e => {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(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._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);
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
_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);
|
||||
},
|
||||
|
||||
_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 {
|
||||
const inputStream = Gio.MemoryInputStream.new_from_bytes(
|
||||
iconData.get_data_as_bytes());
|
||||
this._icon.gicon = await GdkPixbuf.Pixbuf.new_from_stream_async(
|
||||
inputStream, this._dbusClient.cancellable);
|
||||
} 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._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.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);
|
||||
}
|
||||
|
||||
_onMenuOpened(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);
|
||||
this._rootItem.sendAboutToShow();
|
||||
} else {
|
||||
this._rootItem.handleEvent('closed', null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
|
||||
import {CancellableChild, Logger} from './util.js';
|
||||
|
||||
Gio._promisify(Gio.DBusProxy.prototype, 'init_async');
|
||||
|
||||
export const DBusProxy = GObject.registerClass({
|
||||
Signals: {'destroy': {}},
|
||||
}, class DBusProxy extends Gio.DBusProxy {
|
||||
static get TUPLE_VARIANT_TYPE() {
|
||||
if (!this._tupleVariantType)
|
||||
this._tupleVariantType = new GLib.VariantType('(v)');
|
||||
|
||||
return this._tupleVariantType;
|
||||
}
|
||||
|
||||
static destroy() {
|
||||
delete this._tupleType;
|
||||
}
|
||||
|
||||
_init(busName, objectPath, interfaceInfo, flags = Gio.DBusProxyFlags.NONE) {
|
||||
if (interfaceInfo.signals.length)
|
||||
Logger.warn('Avoid exposing signals to gjs!');
|
||||
|
||||
super._init({
|
||||
gConnection: Gio.DBus.session,
|
||||
gInterfaceName: interfaceInfo.name,
|
||||
gInterfaceInfo: interfaceInfo,
|
||||
gName: busName,
|
||||
gObjectPath: objectPath,
|
||||
gFlags: flags,
|
||||
});
|
||||
|
||||
this._signalIds = [];
|
||||
|
||||
if (!(flags & Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS)) {
|
||||
this._signalIds.push(this.connect('g-signal',
|
||||
(_proxy, ...args) => this._onSignal(...args)));
|
||||
}
|
||||
|
||||
this._signalIds.push(this.connect('notify::g-name-owner', () =>
|
||||
this._onNameOwnerChanged()));
|
||||
}
|
||||
|
||||
async initAsync(cancellable) {
|
||||
cancellable = new CancellableChild(cancellable);
|
||||
await this.init_async(GLib.PRIORITY_DEFAULT, cancellable);
|
||||
this._cancellable = cancellable;
|
||||
|
||||
this.gInterfaceInfo.methods.map(m => m.name).forEach(method =>
|
||||
this._ensureAsyncMethod(method));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.emit('destroy');
|
||||
|
||||
this._signalIds.forEach(id => this.disconnect(id));
|
||||
|
||||
if (this._cancellable)
|
||||
this._cancellable.cancel();
|
||||
}
|
||||
|
||||
// This can be removed when we will have GNOME 43 as minimum version
|
||||
_ensureAsyncMethod(method) {
|
||||
if (this[`${method}Async`])
|
||||
return;
|
||||
|
||||
if (!this[`${method}Remote`])
|
||||
throw new Error(`Missing remote method '${method}'`);
|
||||
|
||||
this[`${method}Async`] = function (...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this[`${method}Remote`](...args, (ret, e) => {
|
||||
if (e)
|
||||
reject(e);
|
||||
else
|
||||
resolve(ret);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
_onSignal() {
|
||||
}
|
||||
|
||||
getProperty(propertyName, cancellable) {
|
||||
return this.gConnection.call(this.gName,
|
||||
this.gObjectPath, 'org.freedesktop.DBus.Properties', 'Get',
|
||||
GLib.Variant.new('(ss)', [this.gInterfaceName, propertyName]),
|
||||
DBusProxy.TUPLE_VARIANT_TYPE, Gio.DBusCallFlags.NONE, -1,
|
||||
cancellable);
|
||||
}
|
||||
|
||||
getProperties(cancellable) {
|
||||
return this.gConnection.call(this.gName,
|
||||
this.gObjectPath, 'org.freedesktop.DBus.Properties', 'GetAll',
|
||||
GLib.Variant.new('(s)', [this.gInterfaceName]),
|
||||
GLib.VariantType.new('(a{sv})'), Gio.DBusCallFlags.NONE, -1,
|
||||
cancellable);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,89 @@
|
||||
// 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 * as Extension from 'resource:///org/gnome/shell/extensions/extension.js';
|
||||
|
||||
import * as StatusNotifierWatcher from './statusNotifierWatcher.js';
|
||||
import * as Interfaces from './interfaces.js';
|
||||
import * as TrayIconsManager from './trayIconsManager.js';
|
||||
import * as Util from './util.js';
|
||||
import {SettingsManager} from './settingsManager.js';
|
||||
|
||||
export default class DashToDockExtension extends Extension.Extension {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
Util.Logger.init(this);
|
||||
Interfaces.initialize(this);
|
||||
|
||||
this._isEnabled = false;
|
||||
this._statusNotifierWatcher = null;
|
||||
this._watchDog = new Util.NameWatcher(StatusNotifierWatcher.WATCHER_BUS_NAME);
|
||||
this._watchDog.connect('vanished', () => this._maybeEnableAfterNameAvailable());
|
||||
|
||||
// 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.
|
||||
/* 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');
|
||||
this._watchDog.destroy();
|
||||
this._watchDog = null;
|
||||
};
|
||||
/* eslint-enable no-undef */
|
||||
}
|
||||
|
||||
enable() {
|
||||
this._isEnabled = true;
|
||||
SettingsManager.initialize(this);
|
||||
Util.tryCleanupOldIndicators();
|
||||
this._maybeEnableAfterNameAvailable();
|
||||
TrayIconsManager.TrayIconsManager.initialize();
|
||||
}
|
||||
|
||||
disable() {
|
||||
this._isEnabled = false;
|
||||
TrayIconsManager.TrayIconsManager.destroy();
|
||||
|
||||
if (this._statusNotifierWatcher !== null) {
|
||||
this._statusNotifierWatcher.destroy();
|
||||
this._statusNotifierWatcher = null;
|
||||
}
|
||||
|
||||
SettingsManager.destroy();
|
||||
}
|
||||
|
||||
// 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.
|
||||
_maybeEnableAfterNameAvailable() {
|
||||
// by the time we get called whe might not be enabled
|
||||
if (!this._isEnabled || this._statusNotifierWatcher)
|
||||
return;
|
||||
|
||||
if (this._watchDog.nameAcquired && this._watchDog.nameOnBus)
|
||||
return;
|
||||
|
||||
this._statusNotifierWatcher = new StatusNotifierWatcher.StatusNotifierWatcher(
|
||||
this._watchDog);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,179 @@
|
||||
// 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 GLib from 'gi://GLib';
|
||||
import Gio from 'gi://Gio';
|
||||
|
||||
import * as PromiseUtils from './promiseUtils.js';
|
||||
import * as Util from './util.js';
|
||||
|
||||
// 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.
|
||||
// Without caching, the garbage collection would never be able to handle the amount of new icon data.
|
||||
// If the lifetime of an icon is over, the cache will destroy the icon. (!)
|
||||
// The presence of active icons will extend the lifetime.
|
||||
|
||||
const GC_INTERVAL = 100; // seconds
|
||||
const LIFETIME_TIMESPAN = 120; // seconds
|
||||
|
||||
// how to use: see IconCache.add, IconCache.get
|
||||
export class IconCache {
|
||||
constructor() {
|
||||
this._cache = new Map();
|
||||
this._activeIcons = Object.create(null);
|
||||
this._lifetime = new Map(); // we don't want to attach lifetime to the object
|
||||
}
|
||||
|
||||
add(id, icon) {
|
||||
if (!(icon instanceof Gio.Icon)) {
|
||||
Util.Logger.critical('IconCache: Only Gio.Icons are supported');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
Util.Logger.critical('IconCache: Invalid ID provided');
|
||||
return null;
|
||||
}
|
||||
|
||||
const oldIcon = this._cache.get(id);
|
||||
if (!oldIcon || !oldIcon.equals(icon)) {
|
||||
Util.Logger.debug(`IconCache: adding ${id}: ${icon}`);
|
||||
this._cache.set(id, icon);
|
||||
} else {
|
||||
icon = oldIcon;
|
||||
}
|
||||
|
||||
this._renewLifetime(id);
|
||||
this._checkGC();
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
updateActive(iconType, gicon, isActive) {
|
||||
if (!gicon)
|
||||
return;
|
||||
|
||||
const previousActive = this._activeIcons[iconType];
|
||||
|
||||
if (isActive && [...this._cache.values()].some(icon => icon === gicon))
|
||||
this._activeIcons[iconType] = gicon;
|
||||
else if (previousActive === gicon)
|
||||
delete this._activeIcons[iconType];
|
||||
else
|
||||
return;
|
||||
|
||||
if (previousActive) {
|
||||
this._cache.forEach((icon, id) => {
|
||||
if (icon === previousActive)
|
||||
this._renewLifetime(id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_remove(id) {
|
||||
Util.Logger.debug(`IconCache: removing ${id}`);
|
||||
|
||||
this._cache.delete(id);
|
||||
this._lifetime.delete(id);
|
||||
}
|
||||
|
||||
_renewLifetime(id) {
|
||||
this._lifetime.set(id, new Date().getTime() + LIFETIME_TIMESPAN * 1000);
|
||||
}
|
||||
|
||||
forceDestroy(id) {
|
||||
const gicon = this._cache.has(id);
|
||||
if (gicon) {
|
||||
Object.keys(this._activeIcons).forEach(iconType =>
|
||||
this.updateActive(iconType, gicon, false));
|
||||
this._remove(id);
|
||||
this._checkGC();
|
||||
}
|
||||
}
|
||||
|
||||
// marks all the icons as removable, if something doesn't claim them before
|
||||
weakClear() {
|
||||
this._activeIcons = Object.create(null);
|
||||
this._checkGC();
|
||||
}
|
||||
|
||||
// removes everything from the cache
|
||||
clear() {
|
||||
this._activeIcons = Object.create(null);
|
||||
this._cache.forEach((_icon, id) => this._remove(id));
|
||||
this._checkGC();
|
||||
}
|
||||
|
||||
// returns an object from the cache, or null if it can't be found.
|
||||
get(id) {
|
||||
const icon = this._cache.get(id);
|
||||
if (icon) {
|
||||
Util.Logger.debug(`IconCache: retrieving ${id}: ${icon}`);
|
||||
this._renewLifetime(id);
|
||||
return icon;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async _checkGC() {
|
||||
const cacheIsEmpty = this._cache.size === 0;
|
||||
|
||||
if (!cacheIsEmpty && !this._gcTimeout) {
|
||||
Util.Logger.debug('IconCache: garbage collector started');
|
||||
let anyUnusedInCache = false;
|
||||
this._gcTimeout = new PromiseUtils.TimeoutSecondsPromise(GC_INTERVAL,
|
||||
GLib.PRIORITY_LOW);
|
||||
try {
|
||||
await this._gcTimeout;
|
||||
anyUnusedInCache = this._gc();
|
||||
} catch (e) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e, 'IconCache: garbage collector');
|
||||
} finally {
|
||||
delete this._gcTimeout;
|
||||
}
|
||||
|
||||
if (anyUnusedInCache)
|
||||
this._checkGC();
|
||||
} else if (cacheIsEmpty && this._gcTimeout) {
|
||||
Util.Logger.debug('IconCache: garbage collector stopped');
|
||||
this._gcTimeout.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
_gc() {
|
||||
const time = new Date().getTime();
|
||||
let anyUnused = false;
|
||||
|
||||
this._cache.forEach((icon, id) => {
|
||||
if (Object.values(this._activeIcons).includes(icon)) {
|
||||
Util.Logger.debug(`IconCache: ${id} is in use.`);
|
||||
} else if (this._lifetime.get(id) < time) {
|
||||
this._remove(id);
|
||||
} else {
|
||||
anyUnused = true;
|
||||
Util.Logger.debug(`IconCache: ${id} survived this round.`);
|
||||
}
|
||||
});
|
||||
|
||||
return anyUnused;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,592 @@
|
||||
// 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 Gio from 'gi://Gio';
|
||||
import GObject from 'gi://GObject';
|
||||
import St from 'gi://St';
|
||||
|
||||
import * as AppDisplay from 'resource:///org/gnome/shell/ui/appDisplay.js';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
import * as Panel from 'resource:///org/gnome/shell/ui/panel.js';
|
||||
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
|
||||
|
||||
import * as AppIndicator from './appIndicator.js';
|
||||
import * as PromiseUtils from './promiseUtils.js';
|
||||
import * as SettingsManager from './settingsManager.js';
|
||||
import * as Util from './util.js';
|
||||
import * as DBusMenu from './dbusMenu.js';
|
||||
|
||||
const DEFAULT_ICON_SIZE = Panel.PANEL_ICON_SIZE || 16;
|
||||
|
||||
export function addIconToPanel(statusIcon) {
|
||||
if (!(statusIcon instanceof BaseStatusIcon))
|
||||
throw TypeError(`Unexpected icon type: ${statusIcon}`);
|
||||
|
||||
const settings = SettingsManager.getDefaultGSettings();
|
||||
const indicatorId = `appindicator-${statusIcon.uniqueId}`;
|
||||
|
||||
const currentIcon = Main.panel.statusArea[indicatorId];
|
||||
if (currentIcon) {
|
||||
if (currentIcon !== statusIcon)
|
||||
currentIcon.destroy();
|
||||
|
||||
Main.panel.statusArea[indicatorId] = null;
|
||||
}
|
||||
|
||||
Main.panel.addToStatusArea(indicatorId, statusIcon, 1,
|
||||
settings.get_string('tray-pos'));
|
||||
|
||||
Util.connectSmart(settings, 'changed::tray-pos', statusIcon, () =>
|
||||
addIconToPanel(statusIcon));
|
||||
}
|
||||
|
||||
export function getTrayIcons() {
|
||||
return Object.values(Main.panel.statusArea).filter(
|
||||
i => i instanceof IndicatorStatusTrayIcon);
|
||||
}
|
||||
|
||||
export function getAppIndicatorIcons() {
|
||||
return Object.values(Main.panel.statusArea).filter(
|
||||
i => i instanceof IndicatorStatusIcon);
|
||||
}
|
||||
|
||||
export const BaseStatusIcon = GObject.registerClass(
|
||||
class IndicatorBaseStatusIcon extends PanelMenu.Button {
|
||||
_init(menuAlignment, nameText, iconActor, dontCreateMenu) {
|
||||
super._init(menuAlignment, nameText, dontCreateMenu);
|
||||
|
||||
const settings = SettingsManager.getDefaultGSettings();
|
||||
Util.connectSmart(settings, 'changed::icon-opacity', this, this._updateOpacity);
|
||||
this.connect('notify::hover', () => this._onHoverChanged());
|
||||
|
||||
if (!super._onDestroy)
|
||||
this.connect('destroy', () => this._onDestroy());
|
||||
|
||||
this._box = new St.BoxLayout({style_class: 'panel-status-indicators-box'});
|
||||
this.add_child(this._box);
|
||||
|
||||
this._setIconActor(iconActor);
|
||||
this._showIfReady();
|
||||
}
|
||||
|
||||
_setIconActor(icon) {
|
||||
if (!(icon instanceof Clutter.Actor))
|
||||
throw new Error(`${icon} is not a valid actor`);
|
||||
|
||||
if (this._icon && this._icon !== icon)
|
||||
this._icon.destroy();
|
||||
|
||||
this._icon = icon;
|
||||
this._updateEffects();
|
||||
this._monitorIconEffects();
|
||||
|
||||
if (this._icon) {
|
||||
this._box.add_child(this._icon);
|
||||
const id = this._icon.connect('destroy', () => {
|
||||
this._icon.disconnect(id);
|
||||
this._icon = null;
|
||||
this._monitorIconEffects();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onDestroy() {
|
||||
if (this._icon)
|
||||
this._icon.destroy();
|
||||
|
||||
if (super._onDestroy)
|
||||
super._onDestroy();
|
||||
}
|
||||
|
||||
isReady() {
|
||||
throw new GObject.NotImplementedError('isReady() in %s'.format(this.constructor.name));
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return this._icon;
|
||||
}
|
||||
|
||||
get uniqueId() {
|
||||
throw new GObject.NotImplementedError('uniqueId in %s'.format(this.constructor.name));
|
||||
}
|
||||
|
||||
_showIfReady() {
|
||||
this.visible = this.isReady();
|
||||
}
|
||||
|
||||
_onHoverChanged() {
|
||||
if (this.hover) {
|
||||
this.opacity = 255;
|
||||
if (this._icon)
|
||||
this._icon.remove_effect_by_name('desaturate');
|
||||
} else {
|
||||
this._updateEffects();
|
||||
}
|
||||
}
|
||||
|
||||
_updateOpacity() {
|
||||
const settings = SettingsManager.getDefaultGSettings();
|
||||
const userValue = settings.get_user_value('icon-opacity');
|
||||
if (userValue)
|
||||
this.opacity = userValue.unpack();
|
||||
else
|
||||
this.opacity = 255;
|
||||
}
|
||||
|
||||
_updateEffects() {
|
||||
this._updateOpacity();
|
||||
|
||||
if (this._icon) {
|
||||
this._updateSaturation();
|
||||
this._updateBrightnessContrast();
|
||||
}
|
||||
}
|
||||
|
||||
_monitorIconEffects() {
|
||||
const settings = SettingsManager.getDefaultGSettings();
|
||||
const monitoring = !!this._iconSaturationIds;
|
||||
|
||||
if (!this._icon && monitoring) {
|
||||
Util.disconnectSmart(settings, this, this._iconSaturationIds);
|
||||
delete this._iconSaturationIds;
|
||||
|
||||
Util.disconnectSmart(settings, this, this._iconBrightnessIds);
|
||||
delete this._iconBrightnessIds;
|
||||
|
||||
Util.disconnectSmart(settings, this, this._iconContrastIds);
|
||||
delete this._iconContrastIds;
|
||||
} else if (this._icon && !monitoring) {
|
||||
this._iconSaturationIds =
|
||||
Util.connectSmart(settings, 'changed::icon-saturation', this,
|
||||
this._updateSaturation);
|
||||
this._iconBrightnessIds =
|
||||
Util.connectSmart(settings, 'changed::icon-brightness', this,
|
||||
this._updateBrightnessContrast);
|
||||
this._iconContrastIds =
|
||||
Util.connectSmart(settings, 'changed::icon-contrast', this,
|
||||
this._updateBrightnessContrast);
|
||||
}
|
||||
}
|
||||
|
||||
_updateSaturation() {
|
||||
const settings = SettingsManager.getDefaultGSettings();
|
||||
const desaturationValue = settings.get_double('icon-saturation');
|
||||
let desaturateEffect = this._icon.get_effect('desaturate');
|
||||
|
||||
if (desaturationValue > 0) {
|
||||
if (!desaturateEffect) {
|
||||
desaturateEffect = new Clutter.DesaturateEffect();
|
||||
this._icon.add_effect_with_name('desaturate', desaturateEffect);
|
||||
}
|
||||
desaturateEffect.set_factor(desaturationValue);
|
||||
} else if (desaturateEffect) {
|
||||
this._icon.remove_effect(desaturateEffect);
|
||||
}
|
||||
}
|
||||
|
||||
_updateBrightnessContrast() {
|
||||
const settings = SettingsManager.getDefaultGSettings();
|
||||
const brightnessValue = settings.get_double('icon-brightness');
|
||||
const contrastValue = settings.get_double('icon-contrast');
|
||||
let brightnessContrastEffect = this._icon.get_effect('brightness-contrast');
|
||||
|
||||
if (brightnessValue !== 0 | contrastValue !== 0) {
|
||||
if (!brightnessContrastEffect) {
|
||||
brightnessContrastEffect = new Clutter.BrightnessContrastEffect();
|
||||
this._icon.add_effect_with_name('brightness-contrast', brightnessContrastEffect);
|
||||
}
|
||||
brightnessContrastEffect.set_brightness(brightnessValue);
|
||||
brightnessContrastEffect.set_contrast(contrastValue);
|
||||
} else if (brightnessContrastEffect) {
|
||||
this._icon.remove_effect(brightnessContrastEffect);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* IndicatorStatusIcon implements an icon in the system status area
|
||||
*/
|
||||
export const IndicatorStatusIcon = GObject.registerClass(
|
||||
class IndicatorStatusIcon extends BaseStatusIcon {
|
||||
_init(indicator) {
|
||||
super._init(0.5, indicator.accessibleName,
|
||||
new AppIndicator.IconActor(indicator, DEFAULT_ICON_SIZE));
|
||||
this._indicator = indicator;
|
||||
|
||||
this._lastClickTime = -1;
|
||||
this._lastClickX = -1;
|
||||
this._lastClickY = -1;
|
||||
|
||||
this._box.add_style_class_name('appindicator-box');
|
||||
|
||||
Util.connectSmart(this._indicator, 'ready', this, this._showIfReady);
|
||||
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();
|
||||
});
|
||||
Util.connectSmart(this._indicator, 'accessible-name', this, () =>
|
||||
this.set_accessible_name(this._indicator.accessibleName));
|
||||
Util.connectSmart(this._indicator, 'destroy', this, () => this.destroy());
|
||||
|
||||
this.connect('notify::visible', () => this._updateMenu());
|
||||
|
||||
this._showIfReady();
|
||||
}
|
||||
|
||||
_onDestroy() {
|
||||
if (this._menuClient) {
|
||||
this._menuClient.disconnect(this._menuReadyId);
|
||||
this._menuClient.destroy();
|
||||
this._menuClient = null;
|
||||
}
|
||||
|
||||
super._onDestroy();
|
||||
}
|
||||
|
||||
get uniqueId() {
|
||||
return this._indicator.uniqueId;
|
||||
}
|
||||
|
||||
isReady() {
|
||||
return this._indicator && this._indicator.isReady;
|
||||
}
|
||||
|
||||
_updateLabel() {
|
||||
const {label} = this._indicator;
|
||||
if (label) {
|
||||
if (!this._label || !this._labelBin) {
|
||||
this._labelBin = new St.Bin({
|
||||
yAlign: 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;
|
||||
}
|
||||
}
|
||||
|
||||
_updateStatus() {
|
||||
const wasVisible = this.visible;
|
||||
this.visible = this._indicator.status !== AppIndicator.SNIStatus.PASSIVE;
|
||||
|
||||
if (this.visible !== wasVisible)
|
||||
this._indicator.checkAlive().catch(logError);
|
||||
}
|
||||
|
||||
_updateMenu() {
|
||||
if (this._menuClient) {
|
||||
this._menuClient.disconnect(this._menuReadyId);
|
||||
this._menuClient.destroy();
|
||||
this._menuClient = null;
|
||||
this.menu.removeAll();
|
||||
}
|
||||
|
||||
if (this.visible && this._indicator.menuPath) {
|
||||
this._menuClient = new DBusMenu.Client(this._indicator.busName,
|
||||
this._indicator.menuPath, this._indicator);
|
||||
|
||||
if (this._menuClient.isReady)
|
||||
this._menuClient.attachToMenu(this.menu);
|
||||
|
||||
this._menuReadyId = this._menuClient.connect('ready-changed', () => {
|
||||
if (this._menuClient.isReady)
|
||||
this._menuClient.attachToMenu(this.menu);
|
||||
else
|
||||
this._updateMenu();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_showIfReady() {
|
||||
if (!this.isReady())
|
||||
return;
|
||||
|
||||
this._updateLabel();
|
||||
this._updateStatus();
|
||||
this._updateMenu();
|
||||
}
|
||||
|
||||
_updateClickCount(event) {
|
||||
const [x, y] = event.get_coords();
|
||||
const time = event.get_time();
|
||||
const {doubleClickDistance, doubleClickTime} =
|
||||
Clutter.Settings.get_default();
|
||||
|
||||
if (time > (this._lastClickTime + doubleClickTime) ||
|
||||
(Math.abs(x - this._lastClickX) > doubleClickDistance) ||
|
||||
(Math.abs(y - this._lastClickY) > doubleClickDistance))
|
||||
this._clickCount = 0;
|
||||
|
||||
this._lastClickTime = time;
|
||||
this._lastClickX = x;
|
||||
this._lastClickY = y;
|
||||
|
||||
this._clickCount = (this._clickCount % 2) + 1;
|
||||
|
||||
return this._clickCount;
|
||||
}
|
||||
|
||||
_maybeHandleDoubleClick(event) {
|
||||
if (this._indicator.supportsActivation === false)
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
|
||||
if (event.get_button() !== Clutter.BUTTON_PRIMARY)
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
|
||||
if (this._updateClickCount(event) === 2) {
|
||||
this._indicator.open(...event.get_coords(), event.get_time());
|
||||
return Clutter.EVENT_STOP;
|
||||
}
|
||||
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
async _waitForDoubleClick() {
|
||||
const {doubleClickTime} = Clutter.Settings.get_default();
|
||||
this._waitDoubleClickPromise = new PromiseUtils.TimeoutPromise(
|
||||
doubleClickTime);
|
||||
|
||||
try {
|
||||
await this._waitDoubleClickPromise;
|
||||
this.menu.toggle();
|
||||
} catch (e) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
throw e;
|
||||
} finally {
|
||||
delete this._waitDoubleClickPromise;
|
||||
}
|
||||
}
|
||||
|
||||
vfunc_event(event) {
|
||||
if (this.menu.numMenuItems && event.type() === Clutter.EventType.TOUCH_BEGIN)
|
||||
this.menu.toggle();
|
||||
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
vfunc_button_press_event(event) {
|
||||
if (this._waitDoubleClickPromise)
|
||||
this._waitDoubleClickPromise.cancel();
|
||||
|
||||
// if middle mouse button clicked send SecondaryActivate dbus event and do not show appindicator menu
|
||||
if (event.get_button() === Clutter.BUTTON_MIDDLE) {
|
||||
if (Main.panel.menuManager.activeMenu)
|
||||
Main.panel.menuManager._closeMenu(true, Main.panel.menuManager.activeMenu);
|
||||
this._indicator.secondaryActivate(event.get_time(), ...event.get_coords());
|
||||
return Clutter.EVENT_STOP;
|
||||
}
|
||||
|
||||
if (event.get_button() === Clutter.BUTTON_SECONDARY) {
|
||||
this.menu.toggle();
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
const doubleClickHandled = this._maybeHandleDoubleClick(event);
|
||||
if (doubleClickHandled === Clutter.EVENT_PROPAGATE &&
|
||||
event.get_button() === Clutter.BUTTON_PRIMARY &&
|
||||
this.menu.numMenuItems) {
|
||||
if (this._indicator.supportsActivation !== false)
|
||||
this._waitForDoubleClick().catch(logError);
|
||||
else
|
||||
this.menu.toggle();
|
||||
}
|
||||
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
vfunc_button_release_event(event) {
|
||||
if (!this._indicator.supportsActivation)
|
||||
return this._maybeHandleDoubleClick(event);
|
||||
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
vfunc_scroll_event(event) {
|
||||
// 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) {
|
||||
const [dx, dy] = event.get_scroll_delta();
|
||||
|
||||
this._indicator.scroll(dx, dy);
|
||||
return Clutter.EVENT_STOP;
|
||||
}
|
||||
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
}
|
||||
});
|
||||
|
||||
export const IndicatorStatusTrayIcon = GObject.registerClass(
|
||||
class IndicatorTrayIcon extends BaseStatusIcon {
|
||||
_init(icon) {
|
||||
super._init(0.5, icon.wm_class, icon, {dontCreateMenu: true});
|
||||
Util.Logger.debug(`Adding legacy tray icon ${this.uniqueId}`);
|
||||
this._box.add_style_class_name('appindicator-trayicons-box');
|
||||
this.add_style_class_name('appindicator-icon');
|
||||
this.add_style_class_name('tray-icon');
|
||||
|
||||
this.connect('button-press-event', (_actor, _event) => {
|
||||
this.add_style_pseudo_class('active');
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
});
|
||||
this.connect('button-release-event', (_actor, event) => {
|
||||
this._icon.click(event);
|
||||
this.remove_style_pseudo_class('active');
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
});
|
||||
this.connect('key-press-event', (_actor, event) => {
|
||||
this.add_style_pseudo_class('active');
|
||||
this._icon.click(event);
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
});
|
||||
this.connect('key-release-event', (_actor, event) => {
|
||||
this._icon.click(event);
|
||||
this.remove_style_pseudo_class('active');
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
});
|
||||
|
||||
Util.connectSmart(this._icon, 'destroy', this, () => {
|
||||
icon.clear_effects();
|
||||
this.destroy();
|
||||
});
|
||||
|
||||
const settings = SettingsManager.getDefaultGSettings();
|
||||
Util.connectSmart(settings, 'changed::icon-size', this, this._updateIconSize);
|
||||
|
||||
const themeContext = St.ThemeContext.get_for_stage(global.stage);
|
||||
Util.connectSmart(themeContext, 'notify::scale-factor', this, () =>
|
||||
this._updateIconSize());
|
||||
|
||||
this._updateIconSize();
|
||||
}
|
||||
|
||||
_onDestroy() {
|
||||
Util.Logger.debug(`Destroying legacy tray icon ${this.uniqueId}`);
|
||||
|
||||
if (this._waitDoubleClickPromise)
|
||||
this._waitDoubleClickPromise.cancel();
|
||||
|
||||
super._onDestroy();
|
||||
}
|
||||
|
||||
isReady() {
|
||||
return !!this._icon;
|
||||
}
|
||||
|
||||
get uniqueId() {
|
||||
return `legacy:${this._icon.wm_class}:${this._icon.pid}`;
|
||||
}
|
||||
|
||||
vfunc_navigate_focus(from, direction) {
|
||||
this.grab_key_focus();
|
||||
return super.vfunc_navigate_focus(from, direction);
|
||||
}
|
||||
|
||||
_getSimulatedButtonEvent(touchEvent) {
|
||||
const event = Clutter.Event.new(Clutter.EventType.BUTTON_RELEASE);
|
||||
event.set_button(1);
|
||||
event.set_time(touchEvent.get_time());
|
||||
event.set_flags(touchEvent.get_flags());
|
||||
event.set_stage(global.stage);
|
||||
event.set_source(touchEvent.get_source());
|
||||
event.set_coords(...touchEvent.get_coords());
|
||||
event.set_state(touchEvent.get_state());
|
||||
return event;
|
||||
}
|
||||
|
||||
vfunc_touch_event(event) {
|
||||
// Under X11 we rely on emulated pointer events
|
||||
if (!imports.gi.Meta.is_wayland_compositor())
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
|
||||
const slot = event.get_event_sequence().get_slot();
|
||||
|
||||
if (!this._touchPressSlot &&
|
||||
event.get_type() === Clutter.EventType.TOUCH_BEGIN) {
|
||||
this.add_style_pseudo_class('active');
|
||||
this._touchButtonEvent = this._getSimulatedButtonEvent(event);
|
||||
this._touchPressSlot = slot;
|
||||
this._touchDelayPromise = new PromiseUtils.TimeoutPromise(
|
||||
AppDisplay.MENU_POPUP_TIMEOUT);
|
||||
this._touchDelayPromise.then(() => {
|
||||
delete this._touchDelayPromise;
|
||||
delete this._touchPressSlot;
|
||||
this._touchButtonEvent.set_button(3);
|
||||
this._icon.click(this._touchButtonEvent);
|
||||
this.remove_style_pseudo_class('active');
|
||||
});
|
||||
} else if (event.get_type() === Clutter.EventType.TOUCH_END &&
|
||||
this._touchPressSlot === slot) {
|
||||
delete this._touchPressSlot;
|
||||
delete this._touchButtonEvent;
|
||||
if (this._touchDelayPromise) {
|
||||
this._touchDelayPromise.cancel();
|
||||
delete this._touchDelayPromise;
|
||||
}
|
||||
|
||||
this._icon.click(this._getSimulatedButtonEvent(event));
|
||||
this.remove_style_pseudo_class('active');
|
||||
} else if (event.get_type() === Clutter.EventType.TOUCH_UPDATE &&
|
||||
this._touchPressSlot === slot) {
|
||||
this.add_style_pseudo_class('active');
|
||||
this._touchButtonEvent = this._getSimulatedButtonEvent(event);
|
||||
}
|
||||
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
vfunc_leave_event(event) {
|
||||
this.remove_style_pseudo_class('active');
|
||||
|
||||
if (this._touchDelayPromise) {
|
||||
this._touchDelayPromise.cancel();
|
||||
delete this._touchDelayPromise;
|
||||
}
|
||||
|
||||
return super.vfunc_leave_event(event);
|
||||
}
|
||||
|
||||
_updateIconSize() {
|
||||
const settings = SettingsManager.getDefaultGSettings();
|
||||
const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
|
||||
let iconSize = settings.get_int('icon-size');
|
||||
|
||||
if (iconSize <= 0)
|
||||
iconSize = DEFAULT_ICON_SIZE;
|
||||
|
||||
this.height = -1;
|
||||
this._icon.set({
|
||||
width: iconSize * scaleFactor,
|
||||
height: iconSize * scaleFactor,
|
||||
xAlign: Clutter.ActorAlign.CENTER,
|
||||
yAlign: Clutter.ActorAlign.CENTER,
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,66 @@
|
||||
<interface name="com.canonical.dbusmenu">
|
||||
<!-- Properties -->
|
||||
<property name="Version" type="u" access="read" />
|
||||
<property name="TextDirection" type="s" access="read" />
|
||||
<property name="Status" type="s" access="read" />
|
||||
<property name="IconThemePath" type="as" access="read" />
|
||||
|
||||
<!-- Functions -->
|
||||
<method name="GetLayout">
|
||||
<arg type="i" name="parentId" direction="in" />
|
||||
<arg type="i" name="recursionDepth" direction="in" />
|
||||
<arg type="as" name="propertyNames" direction="in" />
|
||||
<arg type="u" name="revision" direction="out" />
|
||||
<arg type="(ia{sv}av)" name="layout" direction="out" />
|
||||
</method>
|
||||
|
||||
<method name="GetGroupProperties">
|
||||
<arg type="ai" name="ids" direction="in" />
|
||||
<arg type="as" name="propertyNames" direction="in" />
|
||||
<arg type="a(ia{sv})" name="properties" direction="out" />
|
||||
</method>
|
||||
|
||||
<method name="GetProperty">
|
||||
<arg type="i" name="id" direction="in" />
|
||||
<arg type="s" name="name" direction="in" />
|
||||
<arg type="v" name="value" direction="out" />
|
||||
</method>
|
||||
|
||||
<method name="Event">
|
||||
<arg type="i" name="id" direction="in" />
|
||||
<arg type="s" name="eventId" direction="in" />
|
||||
<arg type="v" name="data" direction="in" />
|
||||
<arg type="u" name="timestamp" direction="in" />
|
||||
</method>
|
||||
|
||||
<method name="EventGroup">
|
||||
<arg type="a(isvu)" name="events" direction="in" />
|
||||
<arg type="ai" name="idErrors" direction="out" />
|
||||
</method>
|
||||
|
||||
<method name="AboutToShow">
|
||||
<arg type="i" name="id" direction="in" />
|
||||
<arg type="b" name="needUpdate" direction="out" />
|
||||
</method>
|
||||
|
||||
<method name="AboutToShowGroup">
|
||||
<arg type="ai" name="ids" direction="in" />
|
||||
<arg type="ai" name="updatesNeeded" direction="out" />
|
||||
<arg type="ai" name="idErrors" direction="out" />
|
||||
</method>
|
||||
|
||||
<!-- Signals
|
||||
<signal name="ItemsPropertiesUpdated">
|
||||
<arg type="a(ia{sv})" name="updatedProps" direction="out" />
|
||||
<arg type="a(ias)" name="removedProps" direction="out" />
|
||||
</signal>
|
||||
<signal name="LayoutUpdated">
|
||||
<arg type="u" name="revision" direction="out" />
|
||||
<arg type="i" name="parent" direction="out" />
|
||||
</signal>
|
||||
<signal name="ItemActivationRequested">
|
||||
<arg type="i" name="id" direction="out" />
|
||||
<arg type="u" name="timestamp" direction="out" />
|
||||
</signal>
|
||||
-->
|
||||
</interface>
|
||||
@ -0,0 +1,130 @@
|
||||
<!-- Based on:
|
||||
https://invent.kde.org/frameworks/knotifications/-/blob/master/src/org.kde.StatusNotifierItem.xml
|
||||
-->
|
||||
|
||||
<interface name="org.kde.StatusNotifierItem">
|
||||
|
||||
<property name="Category" type="s" access="read"/>
|
||||
<property name="Id" type="s" access="read"/>
|
||||
<property name="Title" type="s" access="read"/>
|
||||
<property name="Status" type="s" access="read"/>
|
||||
<property name="WindowId" type="i" access="read"/>
|
||||
|
||||
<!-- An additional path to add to the theme search path to find the icons specified above. -->
|
||||
<property name="IconThemePath" type="s" access="read"/>
|
||||
<property name="Menu" type="o" access="read"/>
|
||||
<property name="ItemIsMenu" type="b" access="read"/>
|
||||
|
||||
|
||||
<!-- main icon -->
|
||||
<!-- names are preferred over pixmaps -->
|
||||
<property name="IconName" type="s" access="read"/>
|
||||
|
||||
<!--struct containing width, height and image data-->
|
||||
<property name="IconPixmap" type="a(iiay)" access="read">
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusImageVector"/>
|
||||
</property>
|
||||
|
||||
<property name="OverlayIconName" type="s" access="read"/>
|
||||
|
||||
<property name="OverlayIconPixmap" type="a(iiay)" access="read">
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusImageVector"/>
|
||||
</property>
|
||||
|
||||
|
||||
<!-- Requesting attention icon -->
|
||||
<property name="AttentionIconName" type="s" access="read"/>
|
||||
|
||||
<!--same definition as image-->
|
||||
<property name="AttentionIconPixmap" type="a(iiay)" access="read">
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusImageVector"/>
|
||||
</property>
|
||||
|
||||
<property name="AttentionMovieName" type="s" access="read"/>
|
||||
|
||||
|
||||
|
||||
<!-- tooltip data -->
|
||||
|
||||
<!--(iiay) is an image-->
|
||||
<!-- We disable this as we don't support tooltip, so no need to go through it
|
||||
<property name="ToolTip" type="(sa(iiay)ss)" access="read">
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName" value="KDbusToolTipStruct"/>
|
||||
</property>
|
||||
-->
|
||||
|
||||
<!-- interaction: the systemtray wants the application to do something -->
|
||||
<method name="ContextMenu">
|
||||
<!-- we're passing the coordinates of the icon, so the app knows where to put the popup window -->
|
||||
<arg name="x" type="i" direction="in"/>
|
||||
<arg name="y" type="i" direction="in"/>
|
||||
</method>
|
||||
|
||||
<method name="Activate">
|
||||
<arg name="x" type="i" direction="in"/>
|
||||
<arg name="y" type="i" direction="in"/>
|
||||
</method>
|
||||
|
||||
<method name="ProvideXdgActivationToken">
|
||||
<arg name="token" type="s" direction="in"/>
|
||||
</method>
|
||||
|
||||
<method name="SecondaryActivate">
|
||||
<arg name="x" type="i" direction="in"/>
|
||||
<arg name="y" type="i" direction="in"/>
|
||||
</method>
|
||||
|
||||
<method name="XAyatanaSecondaryActivate">
|
||||
<arg name="timestamp" type="u" direction="in"/>
|
||||
</method>
|
||||
|
||||
<method name="Scroll">
|
||||
<arg name="delta" type="i" direction="in"/>
|
||||
<arg name="orientation" type="s" direction="in"/>
|
||||
</method>
|
||||
|
||||
<!-- Signals: the client wants to change something in the status
|
||||
<signal name="NewTitle">
|
||||
</signal>
|
||||
|
||||
<signal name="NewIcon">
|
||||
</signal>
|
||||
|
||||
<signal name="NewAttentionIcon">
|
||||
</signal>
|
||||
|
||||
<signal name="NewOverlayIcon">
|
||||
</signal>
|
||||
-->
|
||||
<!-- We disable this as we don't support tooltip, so no need to go through it
|
||||
<signal name="NewToolTip">
|
||||
</signal>
|
||||
-->
|
||||
|
||||
<!--
|
||||
<signal name="NewStatus">
|
||||
<arg name="status" type="s"/>
|
||||
</signal>
|
||||
-->
|
||||
|
||||
|
||||
<!-- The following items are not supported by specs, but widely used
|
||||
<signal name="NewIconThemePath">
|
||||
<arg type="s" name="icon_theme_path" direction="out" />
|
||||
</signal>
|
||||
|
||||
<signal name="NewMenu"></signal>
|
||||
-->
|
||||
|
||||
<!-- ayatana labels -->
|
||||
<!-- These are commented out because GDBusProxy would otherwise require them,
|
||||
but they are not available for KDE indicators
|
||||
-->
|
||||
<!--<signal name="XAyatanaNewLabel">
|
||||
<arg type="s" name="label" direction="out" />
|
||||
<arg type="s" name="guide" direction="out" />
|
||||
</signal>
|
||||
<property name="XAyatanaLabel" type="s" access="read" />
|
||||
<property name="XAyatanaLabelGuide" type="s" access="read" />-->
|
||||
|
||||
</interface>
|
||||
@ -0,0 +1,38 @@
|
||||
<interface name="org.kde.StatusNotifierWatcher">
|
||||
|
||||
<!-- methods -->
|
||||
<method name="RegisterStatusNotifierItem">
|
||||
<arg name="service" type="s" direction="in"/>
|
||||
</method>
|
||||
|
||||
<method name="RegisterStatusNotifierHost">
|
||||
<arg name="service" type="s" direction="in"/>
|
||||
</method>
|
||||
|
||||
|
||||
<!-- properties -->
|
||||
|
||||
<property name="RegisteredStatusNotifierItems" type="as" access="read">
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QStringList"/>
|
||||
</property>
|
||||
|
||||
<property name="IsStatusNotifierHostRegistered" type="b" access="read"/>
|
||||
|
||||
<property name="ProtocolVersion" type="i" access="read"/>
|
||||
|
||||
|
||||
<!-- signals -->
|
||||
<signal name="StatusNotifierItemRegistered">
|
||||
<arg type="s"/>
|
||||
</signal>
|
||||
|
||||
<signal name="StatusNotifierItemUnregistered">
|
||||
<arg type="s"/>
|
||||
</signal>
|
||||
|
||||
<signal name="StatusNotifierHostRegistered">
|
||||
</signal>
|
||||
|
||||
<signal name="StatusNotifierHostUnregistered">
|
||||
</signal>
|
||||
</interface>
|
||||
@ -0,0 +1,52 @@
|
||||
// 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.
|
||||
|
||||
export let StatusNotifierItem = null;
|
||||
export let StatusNotifierWatcher = null;
|
||||
export let DBusMenu = null;
|
||||
|
||||
// loads a xml file into an in-memory string
|
||||
function loadInterfaceXml(extension, filename) {
|
||||
const interfacesDir = extension.dir.get_child('interfaces-xml');
|
||||
const file = interfacesDir.get_child(filename);
|
||||
const [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"`
|
||||
// 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).
|
||||
let nodeContents = contents;
|
||||
if (contents instanceof Uint8Array)
|
||||
nodeContents = imports.byteArray.toString(contents);
|
||||
return `<node>${nodeContents}</node>`;
|
||||
} else {
|
||||
throw new Error(`AppIndicatorSupport: Could not load file: ${filename}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function initialize(extension) {
|
||||
StatusNotifierItem = loadInterfaceXml(extension, 'StatusNotifierItem.xml');
|
||||
StatusNotifierWatcher = loadInterfaceXml(extension, 'StatusNotifierWatcher.xml');
|
||||
DBusMenu = loadInterfaceXml(extension, 'DBusMenu.xml');
|
||||
}
|
||||
|
||||
export function destroy() {
|
||||
StatusNotifierItem = null;
|
||||
StatusNotifierWatcher = null;
|
||||
DBusMenu = null;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,13 @@
|
||||
{
|
||||
"_generated": "Generated by SweetTooth, do not edit",
|
||||
"description": "Adds AppIndicator, KStatusNotifierItem and legacy Tray icons support to the Shell",
|
||||
"gettext-domain": "AppIndicatorExtension",
|
||||
"name": "AppIndicator and KStatusNotifierItem Support",
|
||||
"settings-schema": "org.gnome.shell.extensions.appindicator",
|
||||
"shell-version": [
|
||||
"45"
|
||||
],
|
||||
"url": "https://github.com/ubuntu/gnome-shell-extension-appindicator",
|
||||
"uuid": "appindicatorsupport@rgcjonas.gmail.com",
|
||||
"version": 57
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
// 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.
|
||||
|
||||
export function argbToRgba(src) {
|
||||
const dest = new Uint8Array(src.length);
|
||||
|
||||
for (let j = 0; j < src.length; j += 4) {
|
||||
const 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 */
|
||||
}
|
||||
|
||||
return dest;
|
||||
}
|
||||
|
||||
export function getBestPixmap(pixmapsVariant, preferredSize) {
|
||||
if (!pixmapsVariant)
|
||||
throw new TypeError('null pixmapsVariant');
|
||||
|
||||
const pixmapsVariantsArray = new Array(pixmapsVariant.n_children());
|
||||
if (!pixmapsVariantsArray.length)
|
||||
throw TypeError('Empty Icon found');
|
||||
|
||||
for (let i = 0; i < pixmapsVariantsArray.length; ++i)
|
||||
pixmapsVariantsArray[i] = pixmapsVariant.get_child_value(i);
|
||||
|
||||
const pixmapsSizedArray = pixmapsVariantsArray.map((pixmapVariant, index) => ({
|
||||
width: pixmapVariant.get_child_value(0).unpack(),
|
||||
height: pixmapVariant.get_child_value(1).unpack(),
|
||||
index,
|
||||
}));
|
||||
|
||||
const sortedIconPixmapArray = pixmapsSizedArray.sort(
|
||||
({width: widthA, height: heightA}, {width: widthB, height: heightB}) => {
|
||||
const areaA = widthA * heightA;
|
||||
const areaB = widthB * heightB;
|
||||
|
||||
return areaA - areaB;
|
||||
});
|
||||
|
||||
// we prefer any pixmap that is equal or bigger than our requested size
|
||||
const qualifiedIconPixmapArray = sortedIconPixmapArray.filter(({width, height}) =>
|
||||
width >= preferredSize && height >= preferredSize);
|
||||
|
||||
const {width, height, index} = qualifiedIconPixmapArray.length > 0
|
||||
? qualifiedIconPixmapArray[0] : sortedIconPixmapArray.pop();
|
||||
|
||||
const pixmapVariant = pixmapsVariantsArray[index].get_child_value(2);
|
||||
const rowStride = width * 4; // hopefully this is correct
|
||||
|
||||
return {pixmapVariant, width, height, rowStride};
|
||||
}
|
||||
@ -0,0 +1,326 @@
|
||||
// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
|
||||
|
||||
/* exported init, buildPrefsWidget */
|
||||
|
||||
import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
import Gio from 'gi://Gio';
|
||||
import Gtk from 'gi://Gtk';
|
||||
|
||||
import {
|
||||
ExtensionPreferences,
|
||||
gettext as _
|
||||
} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
|
||||
|
||||
const AppIndicatorPreferences = GObject.registerClass(
|
||||
class AppIndicatorPreferences extends Gtk.Box {
|
||||
_init(extension) {
|
||||
super._init({orientation: Gtk.Orientation.VERTICAL, spacing: 30});
|
||||
this._settings = extension.getSettings();
|
||||
|
||||
let label = null;
|
||||
let widget = null;
|
||||
|
||||
this.preferences_vbox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
spacing: 8,
|
||||
margin_start: 30,
|
||||
margin_end: 30,
|
||||
margin_top: 30,
|
||||
margin_bottom: 30,
|
||||
});
|
||||
this.custom_icons_vbox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 10,
|
||||
margin_start: 10,
|
||||
margin_end: 10,
|
||||
margin_top: 10,
|
||||
margin_bottom: 10,
|
||||
});
|
||||
|
||||
label = new Gtk.Label({
|
||||
label: _('Enable Legacy Tray Icons support'),
|
||||
hexpand: true,
|
||||
halign: Gtk.Align.START,
|
||||
});
|
||||
widget = new Gtk.Switch({halign: Gtk.Align.END});
|
||||
|
||||
this._settings.bind('legacy-tray-enabled', widget, 'active',
|
||||
Gio.SettingsBindFlags.DEFAULT);
|
||||
|
||||
this.legacy_tray_hbox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 10,
|
||||
margin_start: 10,
|
||||
margin_end: 10,
|
||||
margin_top: 10,
|
||||
margin_bottom: 10,
|
||||
});
|
||||
|
||||
this.legacy_tray_hbox.append(label);
|
||||
this.legacy_tray_hbox.append(widget);
|
||||
|
||||
// Icon opacity
|
||||
this.opacity_hbox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 10,
|
||||
margin_start: 10,
|
||||
margin_end: 10,
|
||||
margin_top: 10,
|
||||
margin_bottom: 10,
|
||||
});
|
||||
label = new Gtk.Label({
|
||||
label: _('Opacity (min: 0, max: 255)'),
|
||||
hexpand: true,
|
||||
halign: Gtk.Align.START,
|
||||
});
|
||||
|
||||
widget = new Gtk.SpinButton({halign: Gtk.Align.END});
|
||||
widget.set_sensitive(true);
|
||||
widget.set_range(0, 255);
|
||||
widget.set_value(this._settings.get_int('icon-opacity'));
|
||||
widget.set_increments(1, 2);
|
||||
widget.connect('value-changed', w => {
|
||||
this._settings.set_int('icon-opacity', w.get_value_as_int());
|
||||
});
|
||||
this.opacity_hbox.append(label);
|
||||
this.opacity_hbox.append(widget);
|
||||
|
||||
// Icon saturation
|
||||
this.saturation_hbox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 10,
|
||||
margin_start: 10,
|
||||
margin_end: 10,
|
||||
margin_top: 10,
|
||||
margin_bottom: 10,
|
||||
});
|
||||
label = new Gtk.Label({
|
||||
label: _('Desaturation (min: 0.0, max: 1.0)'),
|
||||
hexpand: true,
|
||||
halign: Gtk.Align.START,
|
||||
});
|
||||
widget = new Gtk.SpinButton({halign: Gtk.Align.END, digits: 1});
|
||||
widget.set_sensitive(true);
|
||||
widget.set_range(0.0, 1.0);
|
||||
widget.set_value(this._settings.get_double('icon-saturation'));
|
||||
widget.set_increments(0.1, 0.2);
|
||||
widget.connect('value-changed', w => {
|
||||
this._settings.set_double('icon-saturation', w.get_value());
|
||||
});
|
||||
this.saturation_hbox.append(label);
|
||||
this.saturation_hbox.append(widget);
|
||||
|
||||
// Icon brightness
|
||||
this.brightness_hbox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 10,
|
||||
margin_start: 10,
|
||||
margin_end: 10,
|
||||
margin_top: 10,
|
||||
margin_bottom: 10,
|
||||
});
|
||||
label = new Gtk.Label({
|
||||
label: _('Brightness (min: -1.0, max: 1.0)'),
|
||||
hexpand: true,
|
||||
halign: Gtk.Align.START,
|
||||
});
|
||||
widget = new Gtk.SpinButton({halign: Gtk.Align.END, digits: 1});
|
||||
widget.set_sensitive(true);
|
||||
widget.set_range(-1.0, 1.0);
|
||||
widget.set_value(this._settings.get_double('icon-brightness'));
|
||||
widget.set_increments(0.1, 0.2);
|
||||
widget.connect('value-changed', w => {
|
||||
this._settings.set_double('icon-brightness', w.get_value());
|
||||
});
|
||||
this.brightness_hbox.append(label);
|
||||
this.brightness_hbox.append(widget);
|
||||
|
||||
// Icon contrast
|
||||
this.contrast_hbox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 10,
|
||||
margin_start: 10,
|
||||
margin_end: 10,
|
||||
margin_top: 10,
|
||||
margin_bottom: 10,
|
||||
});
|
||||
label = new Gtk.Label({
|
||||
label: _('Contrast (min: -1.0, max: 1.0)'),
|
||||
hexpand: true,
|
||||
halign: Gtk.Align.START,
|
||||
});
|
||||
widget = new Gtk.SpinButton({halign: Gtk.Align.END, digits: 1});
|
||||
widget.set_sensitive(true);
|
||||
widget.set_range(-1.0, 1.0);
|
||||
widget.set_value(this._settings.get_double('icon-contrast'));
|
||||
widget.set_increments(0.1, 0.2);
|
||||
widget.connect('value-changed', w => {
|
||||
this._settings.set_double('icon-contrast', w.get_value());
|
||||
});
|
||||
this.contrast_hbox.append(label);
|
||||
this.contrast_hbox.append(widget);
|
||||
|
||||
// Icon size
|
||||
this.icon_size_hbox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 10,
|
||||
margin_start: 10,
|
||||
margin_end: 10,
|
||||
margin_top: 10,
|
||||
margin_bottom: 10,
|
||||
});
|
||||
label = new Gtk.Label({
|
||||
label: _('Icon size (min: 0, max: 96)'),
|
||||
hexpand: true,
|
||||
halign: Gtk.Align.START,
|
||||
});
|
||||
widget = new Gtk.SpinButton({halign: Gtk.Align.END});
|
||||
widget.set_sensitive(true);
|
||||
widget.set_range(0, 96);
|
||||
widget.set_value(this._settings.get_int('icon-size'));
|
||||
widget.set_increments(1, 2);
|
||||
widget.connect('value-changed', w => {
|
||||
this._settings.set_int('icon-size', w.get_value_as_int());
|
||||
});
|
||||
this.icon_size_hbox.append(label);
|
||||
this.icon_size_hbox.append(widget);
|
||||
|
||||
// Tray position in panel
|
||||
this.tray_position_hbox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 10,
|
||||
margin_start: 10,
|
||||
margin_end: 10,
|
||||
margin_top: 10,
|
||||
margin_bottom: 10,
|
||||
});
|
||||
label = new Gtk.Label({
|
||||
label: _('Tray horizontal alignment'),
|
||||
hexpand: true,
|
||||
halign: Gtk.Align.START,
|
||||
});
|
||||
widget = new Gtk.ComboBoxText();
|
||||
widget.append('center', _('Center'));
|
||||
widget.append('left', _('Left'));
|
||||
widget.append('right', _('Right'));
|
||||
this._settings.bind('tray-pos', widget, 'active-id',
|
||||
Gio.SettingsBindFlags.DEFAULT);
|
||||
this.tray_position_hbox.append(label);
|
||||
this.tray_position_hbox.append(widget);
|
||||
|
||||
this.preferences_vbox.append(this.legacy_tray_hbox);
|
||||
this.preferences_vbox.append(this.opacity_hbox);
|
||||
this.preferences_vbox.append(this.saturation_hbox);
|
||||
this.preferences_vbox.append(this.brightness_hbox);
|
||||
this.preferences_vbox.append(this.contrast_hbox);
|
||||
this.preferences_vbox.append(this.icon_size_hbox);
|
||||
this.preferences_vbox.append(this.tray_position_hbox);
|
||||
|
||||
// Custom icons section
|
||||
|
||||
const customListStore = new Gtk.ListStore();
|
||||
customListStore.set_column_types([
|
||||
GObject.TYPE_STRING,
|
||||
GObject.TYPE_STRING,
|
||||
GObject.TYPE_STRING,
|
||||
]);
|
||||
const customInitArray = this._settings.get_value('custom-icons').deep_unpack();
|
||||
customInitArray.forEach(pair => {
|
||||
customListStore.set(customListStore.append(), [0, 1, 2], pair);
|
||||
});
|
||||
customListStore.append();
|
||||
|
||||
const customTreeView = new Gtk.TreeView({
|
||||
model: customListStore,
|
||||
hexpand: true,
|
||||
vexpand: true,
|
||||
});
|
||||
const customTitles = [
|
||||
_('Indicator ID'),
|
||||
_('Icon Name'),
|
||||
_('Attention Icon Name'),
|
||||
];
|
||||
const indicatorIdColumn = new Gtk.TreeViewColumn({
|
||||
title: customTitles[0],
|
||||
sizing: Gtk.TreeViewColumnSizing.AUTOSIZE,
|
||||
});
|
||||
const customIconColumn = new Gtk.TreeViewColumn({
|
||||
title: customTitles[1],
|
||||
sizing: Gtk.TreeViewColumnSizing.AUTOSIZE,
|
||||
});
|
||||
const customAttentionIconColumn = new Gtk.TreeViewColumn({
|
||||
title: customTitles[2],
|
||||
sizing: Gtk.TreeViewColumnSizing.AUTOSIZE,
|
||||
});
|
||||
|
||||
const cellrenderer = new Gtk.CellRendererText({editable: true});
|
||||
|
||||
indicatorIdColumn.pack_start(cellrenderer, true);
|
||||
customIconColumn.pack_start(cellrenderer, true);
|
||||
customAttentionIconColumn.pack_start(cellrenderer, true);
|
||||
indicatorIdColumn.add_attribute(cellrenderer, 'text', 0);
|
||||
customIconColumn.add_attribute(cellrenderer, 'text', 1);
|
||||
customAttentionIconColumn.add_attribute(cellrenderer, 'text', 2);
|
||||
customTreeView.insert_column(indicatorIdColumn, 0);
|
||||
customTreeView.insert_column(customIconColumn, 1);
|
||||
customTreeView.insert_column(customAttentionIconColumn, 2);
|
||||
customTreeView.set_grid_lines(Gtk.TreeViewGridLines.BOTH);
|
||||
|
||||
this.custom_icons_vbox.append(customTreeView);
|
||||
|
||||
cellrenderer.connect('edited', (w, path, text) => {
|
||||
this.selection = customTreeView.get_selection();
|
||||
const title = customTreeView.get_cursor()[1].get_title();
|
||||
const columnIndex = customTitles.indexOf(title);
|
||||
const selection = this.selection.get_selected();
|
||||
const iter = selection.at(2);
|
||||
const text2 = customListStore.get_value(iter, columnIndex ? 0 : 1);
|
||||
customListStore.set(iter, [columnIndex], [text]);
|
||||
const storeLength = customListStore.iter_n_children(null);
|
||||
const customIconArray = [];
|
||||
|
||||
for (let i = 0; i < storeLength; i++) {
|
||||
const returnIter = customListStore.iter_nth_child(null, i);
|
||||
const [success, iterList] = returnIter;
|
||||
if (!success)
|
||||
break;
|
||||
|
||||
if (iterList) {
|
||||
const id = customListStore.get_value(iterList, 0);
|
||||
const customIcon = customListStore.get_value(iterList, 1);
|
||||
const customAttentionIcon = customListStore.get_value(iterList, 2);
|
||||
if (id && customIcon)
|
||||
customIconArray.push([id, customIcon, customAttentionIcon || '']);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._settings.set_value('custom-icons', new GLib.Variant(
|
||||
'a(sss)', customIconArray));
|
||||
if (storeLength === 1 && (text || text2))
|
||||
customListStore.append();
|
||||
|
||||
if (storeLength > 1) {
|
||||
if ((!text && !text2) && (storeLength - 1 > path))
|
||||
customListStore.remove(iter);
|
||||
if ((text || text2) && storeLength - 1 <= path)
|
||||
customListStore.append();
|
||||
}
|
||||
});
|
||||
|
||||
this.notebook = new Gtk.Notebook();
|
||||
this.notebook.append_page(this.preferences_vbox,
|
||||
new Gtk.Label({label: _('Preferences')}));
|
||||
this.notebook.append_page(this.custom_icons_vbox,
|
||||
new Gtk.Label({label: _('Custom Icons')}));
|
||||
|
||||
this.append(this.notebook);
|
||||
}
|
||||
});
|
||||
|
||||
export default class DockPreferences extends ExtensionPreferences {
|
||||
getPreferencesWidget() {
|
||||
return new AppIndicatorPreferences(this);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,324 @@
|
||||
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
||||
|
||||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
import Meta from 'gi://GdkPixbuf';
|
||||
|
||||
import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
|
||||
|
||||
export class CancellablePromise 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 resolver;
|
||||
super((resolve, reject) => {
|
||||
resolver = resolve;
|
||||
rejector = reject;
|
||||
});
|
||||
|
||||
const {stack: promiseStack} = new Error();
|
||||
this._promiseStack = promiseStack;
|
||||
|
||||
this._resolver = (...args) => {
|
||||
resolver(...args);
|
||||
this._resolved = true;
|
||||
this._cleanup();
|
||||
};
|
||||
this._rejector = (...args) => {
|
||||
rejector(...args);
|
||||
this._rejected = true;
|
||||
this._cleanup();
|
||||
};
|
||||
|
||||
if (!cancellable) {
|
||||
executor(this._resolver, this._rejector);
|
||||
return;
|
||||
}
|
||||
|
||||
this._cancellable = cancellable;
|
||||
this._cancelled = cancellable.is_cancelled();
|
||||
if (this._cancelled) {
|
||||
this._rejector(new GLib.Error(Gio.IOErrorEnum,
|
||||
Gio.IOErrorEnum.CANCELLED, 'Promise cancelled'));
|
||||
return;
|
||||
}
|
||||
|
||||
this._cancellationId = cancellable.connect(() => {
|
||||
const id = this._cancellationId;
|
||||
this._cancellationId = 0;
|
||||
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => cancellable.disconnect(id));
|
||||
this.cancel();
|
||||
});
|
||||
|
||||
executor(this._resolver, this._rejector);
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
if (this._cancellationId)
|
||||
this._cancellable.disconnect(this._cancellationId);
|
||||
}
|
||||
|
||||
get cancellable() {
|
||||
return this._chainRoot._cancellable || null;
|
||||
}
|
||||
|
||||
get _chainRoot() {
|
||||
return this._root ? this._root : this;
|
||||
}
|
||||
|
||||
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._chainRoot;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
resolved() {
|
||||
return !!this._chainRoot._resolved;
|
||||
}
|
||||
|
||||
rejected() {
|
||||
return !!this._chainRoot._rejected;
|
||||
}
|
||||
|
||||
cancelled() {
|
||||
return !!this._chainRoot._cancelled;
|
||||
}
|
||||
|
||||
pending() {
|
||||
return !this.resolved() && !this.rejected();
|
||||
}
|
||||
|
||||
cancel() {
|
||||
if (this._root) {
|
||||
this._root.cancel();
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!this.pending())
|
||||
return this;
|
||||
|
||||
this._cancelled = true;
|
||||
const error = new GLib.Error(Gio.IOErrorEnum,
|
||||
Gio.IOErrorEnum.CANCELLED, 'Promise cancelled');
|
||||
error.stack += `## Promise created at:\n${this._promiseStack}`;
|
||||
this._rejector(error);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class SignalConnectionPromise 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 => {
|
||||
let connectSignal;
|
||||
if (object instanceof GObject.Object)
|
||||
connectSignal = (sig, cb) => GObject.signal_connect(object, sig, cb);
|
||||
else
|
||||
connectSignal = (sig, cb) => object.connect(sig, cb);
|
||||
|
||||
id = connectSignal(signal, (_obj, ...args) => {
|
||||
if (!args.length)
|
||||
resolve();
|
||||
else
|
||||
resolve(args.length === 1 ? args[0] : args);
|
||||
});
|
||||
|
||||
if (signal !== 'destroy' &&
|
||||
(!(object instanceof GObject.Object) ||
|
||||
GObject.signal_lookup('destroy', object.constructor.$gtype)))
|
||||
destroyId = connectSignal('destroy', () => this.cancel());
|
||||
}, cancellable);
|
||||
|
||||
this._object = object;
|
||||
this._id = id;
|
||||
this._destroyId = destroyId;
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
if (this._id) {
|
||||
let disconnectSignal;
|
||||
|
||||
if (this._object instanceof GObject.Object)
|
||||
disconnectSignal = id => GObject.signal_handler_disconnect(this._object, id);
|
||||
else
|
||||
disconnectSignal = id => this._object.disconnect(id);
|
||||
|
||||
disconnectSignal(this._id);
|
||||
if (this._destroyId) {
|
||||
disconnectSignal(this._destroyId);
|
||||
this._destroyId = 0;
|
||||
}
|
||||
this._object = null;
|
||||
this._id = 0;
|
||||
}
|
||||
|
||||
super._cleanup();
|
||||
}
|
||||
|
||||
get object() {
|
||||
return this._chainRoot._object;
|
||||
}
|
||||
}
|
||||
|
||||
export class GSourcePromise 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 === undefined)
|
||||
priority = GLib.PRIORITY_DEFAULT;
|
||||
else if (!Number.isInteger(priority))
|
||||
throw TypeError('Invalid priority');
|
||||
|
||||
super(resolve => {
|
||||
gsource.set_priority(priority);
|
||||
gsource.set_callback(() => {
|
||||
resolve();
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
gsource.attach(null);
|
||||
}, cancellable);
|
||||
|
||||
this._gsource = gsource;
|
||||
this._gsource.set_name(`[gnome-shell] ${this.constructor.name} ${
|
||||
new Error().stack.split('\n').filter(line =>
|
||||
!line.match(/misc\/promiseUtils\.js/))[0]}`);
|
||||
|
||||
if (this.rejected())
|
||||
this._gsource.destroy();
|
||||
}
|
||||
|
||||
get gsource() {
|
||||
return this._chainRoot._gsource;
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
if (this._gsource) {
|
||||
this._gsource.destroy();
|
||||
this._gsource = null;
|
||||
}
|
||||
super._cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
export class IdlePromise extends GSourcePromise {
|
||||
constructor(priority, cancellable) {
|
||||
if (arguments.length === 1 && priority instanceof Function) {
|
||||
super(priority);
|
||||
return;
|
||||
}
|
||||
|
||||
if (priority === undefined)
|
||||
priority = GLib.PRIORITY_DEFAULT_IDLE;
|
||||
|
||||
super(GLib.idle_source_new(), priority, cancellable);
|
||||
}
|
||||
}
|
||||
|
||||
export class TimeoutPromise 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);
|
||||
}
|
||||
}
|
||||
|
||||
export class TimeoutSecondsPromise 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);
|
||||
}
|
||||
}
|
||||
|
||||
export class MetaLaterPromise 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.remove();
|
||||
resolve();
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
}, cancellable);
|
||||
|
||||
this._id = id;
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
if (this._id) {
|
||||
Meta.later_remove(this._id);
|
||||
this._id = 0;
|
||||
}
|
||||
super._cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
export function _promisifySignals(proto) {
|
||||
if (proto.connect_once)
|
||||
return;
|
||||
|
||||
proto.connect_once = function (signal, cancellable) {
|
||||
return new SignalConnectionPromise(this, signal, cancellable);
|
||||
};
|
||||
}
|
||||
|
||||
_promisifySignals(GObject.Object.prototype);
|
||||
_promisifySignals(Signals.EventEmitter.prototype);
|
||||
Binary file not shown.
@ -0,0 +1,49 @@
|
||||
<schemalist gettext-domain="AppIndicatorExtension">
|
||||
<schema id="org.gnome.shell.extensions.appindicator" path="/org/gnome/shell/extensions/appindicator/">
|
||||
<key name="legacy-tray-enabled" type="b">
|
||||
<default>true</default>
|
||||
<summary>Enable legacy tray icons support</summary>
|
||||
</key>
|
||||
<key name="icon-saturation" type="d">
|
||||
<default>0.0</default>
|
||||
<summary>Saturation</summary>
|
||||
</key>
|
||||
<key name="icon-brightness" type="d">
|
||||
<default>0.0</default>
|
||||
<summary>Brightness</summary>
|
||||
</key>
|
||||
<key name="icon-contrast" type="d">
|
||||
<default>0.0</default>
|
||||
<summary>Contrast</summary>
|
||||
</key>
|
||||
<key name="icon-opacity" type="i">
|
||||
<default>240</default>
|
||||
<summary>Opacity</summary>
|
||||
</key>
|
||||
<key name="icon-size" type="i">
|
||||
<default>0</default>
|
||||
<summary>Icon size</summary>
|
||||
<description>Icon size in pixel</description>
|
||||
</key>
|
||||
<key name="icon-spacing" type="i">
|
||||
<default>12</default>
|
||||
<summary>Icon spacing</summary>
|
||||
<description>Icon spacing within the tray</description>
|
||||
</key>
|
||||
<key name="tray-pos" type="s">
|
||||
<default>"right"</default>
|
||||
<summary>Position in tray</summary>
|
||||
<description>Set where the Icon tray should appear in Gnome tray</description>
|
||||
</key>
|
||||
<key name="tray-order" type="i">
|
||||
<default>1</default>
|
||||
<summary>Order in tray</summary>
|
||||
<description>Set where the Icon tray should appear among other trays</description>
|
||||
</key>
|
||||
<key name="custom-icons" type="a(sss)">
|
||||
<default>[]</default>
|
||||
<summary>Custom icons</summary>
|
||||
<description>Replace any icons with custom icons from themes</description>
|
||||
</key>
|
||||
</schema>
|
||||
</schemalist>
|
||||
@ -0,0 +1,55 @@
|
||||
// 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.
|
||||
|
||||
let settingsManager;
|
||||
|
||||
export class SettingsManager {
|
||||
static initialize(extension) {
|
||||
SettingsManager._settingsManager = new SettingsManager(extension);
|
||||
}
|
||||
|
||||
static destroy() {
|
||||
SettingsManager._settingsManager.destroy();
|
||||
SettingsManager._settingsManager = null;
|
||||
}
|
||||
|
||||
static getDefault() {
|
||||
return this._settingsManager;
|
||||
}
|
||||
|
||||
get gsettings() {
|
||||
return this._gsettings;
|
||||
}
|
||||
|
||||
constructor(extension) {
|
||||
if (settingsManager)
|
||||
throw new Error('SettingsManager is already constructed');
|
||||
|
||||
this._gsettings = extension.getSettings();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._gsettings = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefault() {
|
||||
return SettingsManager.getDefault();
|
||||
}
|
||||
|
||||
export function getDefaultGSettings() {
|
||||
return SettingsManager.getDefault().gsettings;
|
||||
}
|
||||
@ -0,0 +1,287 @@
|
||||
// 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 Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
|
||||
import * as AppIndicator from './appIndicator.js';
|
||||
import * as IndicatorStatusIcon from './indicatorStatusIcon.js';
|
||||
import * as Interfaces from './interfaces.js';
|
||||
import * as PromiseUtils from './promiseUtils.js';
|
||||
import * as Util from './util.js';
|
||||
import * as DBusMenu from './dbusMenu.js';
|
||||
|
||||
import {DBusProxy} from './dbusProxy.js';
|
||||
|
||||
|
||||
// TODO: replace with org.freedesktop and /org/freedesktop when approved
|
||||
const KDE_PREFIX = 'org.kde';
|
||||
|
||||
export const WATCHER_BUS_NAME = `${KDE_PREFIX}.StatusNotifierWatcher`;
|
||||
const WATCHER_OBJECT = '/StatusNotifierWatcher';
|
||||
|
||||
const DEFAULT_ITEM_OBJECT_PATH = '/StatusNotifierItem';
|
||||
|
||||
/*
|
||||
* The StatusNotifierWatcher class implements the StatusNotifierWatcher dbus object
|
||||
*/
|
||||
export class StatusNotifierWatcher {
|
||||
constructor(watchDog) {
|
||||
this._watchDog = watchDog;
|
||||
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(Interfaces.StatusNotifierWatcher, this);
|
||||
try {
|
||||
this._dbusImpl.export(Gio.DBus.session, WATCHER_OBJECT);
|
||||
} catch (e) {
|
||||
Util.Logger.warn(`Failed to export ${WATCHER_OBJECT}`);
|
||||
logError(e);
|
||||
}
|
||||
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));
|
||||
this._items = new Map();
|
||||
|
||||
try {
|
||||
this._dbusImpl.emit_signal('StatusNotifierHostRegistered', null);
|
||||
} catch (e) {
|
||||
Util.Logger.warn(`Failed to notify registered host ${WATCHER_OBJECT}`);
|
||||
}
|
||||
|
||||
this._seekStatusNotifierItems().catch(e => {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e, 'Looking for StatusNotifierItem\'s');
|
||||
});
|
||||
}
|
||||
|
||||
_acquiredName() {
|
||||
this._everAcquiredName = true;
|
||||
this._watchDog.nameAcquired = true;
|
||||
}
|
||||
|
||||
_lostName() {
|
||||
if (this._everAcquiredName)
|
||||
Util.Logger.debug(`Lost name${WATCHER_BUS_NAME}`);
|
||||
else
|
||||
Util.Logger.warn(`Failed to acquire ${WATCHER_BUS_NAME}`);
|
||||
this._watchDog.nameAcquired = false;
|
||||
}
|
||||
|
||||
async _registerItem(service, busName, objPath) {
|
||||
const id = Util.indicatorId(service, busName, objPath);
|
||||
|
||||
if (this._items.has(id)) {
|
||||
Util.Logger.warn(`Item ${id} is already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
Util.Logger.debug(`Registering StatusNotifierItem ${id}`);
|
||||
|
||||
try {
|
||||
const indicator = new AppIndicator.AppIndicator(service, busName, objPath);
|
||||
this._items.set(id, indicator);
|
||||
indicator.connect('destroy', () => this._onIndicatorDestroyed(indicator));
|
||||
|
||||
indicator.connect('name-owner-changed', async () => {
|
||||
if (!indicator.hasNameOwner) {
|
||||
try {
|
||||
await new PromiseUtils.TimeoutPromise(500,
|
||||
GLib.PRIORITY_DEFAULT, this._cancellable);
|
||||
if (this._items.has(id) && !indicator.hasNameOwner)
|
||||
indicator.destroy();
|
||||
} catch (e) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// if the desktop is not ready delay the icon creation and signal emissions
|
||||
await Util.waitForStartupCompletion(indicator.cancellable);
|
||||
const statusIcon = new IndicatorStatusIcon.IndicatorStatusIcon(indicator);
|
||||
IndicatorStatusIcon.addIconToPanel(statusIcon);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
async _ensureItemRegistered(service, busName, objPath) {
|
||||
const id = Util.indicatorId(service, busName, objPath);
|
||||
const item = this._items.get(id);
|
||||
|
||||
if (item) {
|
||||
// delete the old one and add the new indicator
|
||||
Util.Logger.debug(`Attempting to re-register ${id}; resetting instead`);
|
||||
item.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
await 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... However let's do it after a low
|
||||
// priority idle, so that it won't affect startup.
|
||||
const cancellable = this._cancellable;
|
||||
const bus = Gio.DBus.session;
|
||||
const uniqueNames = await Util.getBusNames(bus, cancellable);
|
||||
const introspectName = async name => {
|
||||
const nodes = Util.introspectBusObject(bus, name, cancellable,
|
||||
['org.kde.StatusNotifierItem']);
|
||||
const services = [...uniqueNames.get(name)];
|
||||
|
||||
for await (const node of nodes) {
|
||||
const {path} = node;
|
||||
const ids = services.map(s => Util.indicatorId(s, name, path));
|
||||
if (ids.every(id => !this._items.has(id))) {
|
||||
const service = services.find(s =>
|
||||
s && s.startsWith('org.kde.StatusNotifierItem')) || services[0];
|
||||
const id = Util.indicatorId(
|
||||
path === DEFAULT_ITEM_OBJECT_PATH ? service : null,
|
||||
name, path);
|
||||
Util.Logger.warn(`Using Brute-force mode for StatusNotifierItem ${id}`);
|
||||
this._registerItem(service, name, path);
|
||||
}
|
||||
}
|
||||
};
|
||||
await Promise.allSettled([...uniqueNames.keys()].map(n => introspectName(n)));
|
||||
}
|
||||
|
||||
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
|
||||
const [service] = params;
|
||||
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) {
|
||||
const error = `Impossible to register an indicator for parameters '${
|
||||
service.toString()}'`;
|
||||
Util.Logger.warn(error);
|
||||
|
||||
invocation.return_dbus_error('org.gnome.gjs.JSError.ValueError',
|
||||
error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this._ensureItemRegistered(service, busName, objPath);
|
||||
invocation.return_value(null);
|
||||
} catch (e) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e);
|
||||
invocation.return_dbus_error('org.gnome.gjs.JSError.ValueError',
|
||||
e.message);
|
||||
}
|
||||
}
|
||||
|
||||
_onIndicatorDestroyed(indicator) {
|
||||
const {uniqueId} = indicator;
|
||||
this._items.delete(uniqueId);
|
||||
|
||||
try {
|
||||
this._dbusImpl.emit_signal('StatusNotifierItemUnregistered',
|
||||
GLib.Variant.new('(s)', [uniqueId]));
|
||||
this._dbusImpl.emit_property_changed('RegisteredStatusNotifierItems',
|
||||
GLib.Variant.new('as', this.RegisteredStatusNotifierItems));
|
||||
} catch (e) {
|
||||
Util.Logger.warn(`Failed to emit signals: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
RegisterStatusNotifierHostAsync(_service, invocation) {
|
||||
invocation.return_error_literal(
|
||||
Gio.DBusError,
|
||||
Gio.DBusError.NOT_SUPPORTED,
|
||||
'Registering additional notification hosts is not supported');
|
||||
}
|
||||
|
||||
IsNotificationHostRegistered() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get RegisteredStatusNotifierItems() {
|
||||
return Array.from(this._items.values()).map(i => i.uniqueId);
|
||||
}
|
||||
|
||||
get IsStatusNotifierHostRegistered() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get ProtocolVersion() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this._isDestroyed)
|
||||
return;
|
||||
|
||||
// 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)
|
||||
this._items.forEach(indicator => indicator.destroy());
|
||||
this._cancellable.cancel();
|
||||
|
||||
try {
|
||||
this._dbusImpl.emit_signal('StatusNotifierHostUnregistered', null);
|
||||
} catch (e) {
|
||||
Util.Logger.warn(`Failed to emit uinregistered signal: ${e}`);
|
||||
}
|
||||
|
||||
Gio.DBus.session.unown_name(this._ownName);
|
||||
|
||||
try {
|
||||
this._dbusImpl.unexport();
|
||||
} catch (e) {
|
||||
Util.Logger.warn(`Failed to unexport watcher object: ${e}`);
|
||||
}
|
||||
|
||||
DBusMenu.DBusClient.destroy();
|
||||
AppIndicator.AppIndicatorProxy.destroy();
|
||||
DBusProxy.destroy();
|
||||
Util.destroyDefaultTheme();
|
||||
|
||||
this._dbusImpl.run_dispose();
|
||||
delete this._dbusImpl;
|
||||
|
||||
delete this._items;
|
||||
this._isDestroyed = true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
// 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 Shell from 'gi://Shell';
|
||||
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
|
||||
|
||||
import * as IndicatorStatusIcon from './indicatorStatusIcon.js';
|
||||
import * as Util from './util.js';
|
||||
import * as SettingsManager from './settingsManager.js';
|
||||
|
||||
let trayIconsManager;
|
||||
|
||||
export class TrayIconsManager extends Signals.EventEmitter {
|
||||
static initialize() {
|
||||
if (!trayIconsManager)
|
||||
trayIconsManager = new TrayIconsManager();
|
||||
return trayIconsManager;
|
||||
}
|
||||
|
||||
static destroy() {
|
||||
trayIconsManager.destroy();
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (trayIconsManager)
|
||||
throw new Error('TrayIconsManager is already constructed');
|
||||
|
||||
this._changedId = SettingsManager.getDefaultGSettings().connect(
|
||||
'changed::legacy-tray-enabled', () => this._toggle());
|
||||
|
||||
this._toggle();
|
||||
}
|
||||
|
||||
_toggle() {
|
||||
if (SettingsManager.getDefaultGSettings().get_boolean('legacy-tray-enabled'))
|
||||
this._enable();
|
||||
else
|
||||
this._disable();
|
||||
}
|
||||
|
||||
_enable() {
|
||||
if (this._tray)
|
||||
return;
|
||||
|
||||
this._tray = new Shell.TrayManager();
|
||||
Util.connectSmart(this._tray, 'tray-icon-added', this, this.onTrayIconAdded);
|
||||
Util.connectSmart(this._tray, 'tray-icon-removed', this, this.onTrayIconRemoved);
|
||||
|
||||
this._tray.manage_screen(Main.panel);
|
||||
}
|
||||
|
||||
_disable() {
|
||||
if (!this._tray)
|
||||
return;
|
||||
|
||||
IndicatorStatusIcon.getTrayIcons().forEach(i => i.destroy());
|
||||
if (this._tray.unmanage_screen) {
|
||||
this._tray.unmanage_screen();
|
||||
this._tray = null;
|
||||
} else {
|
||||
// FIXME: This is very ugly, but it's needed by old shell versions
|
||||
this._tray = null;
|
||||
imports.system.gc(); // force finalizing tray to unmanage screen
|
||||
}
|
||||
}
|
||||
|
||||
onTrayIconAdded(_tray, icon) {
|
||||
const trayIcon = new IndicatorStatusIcon.IndicatorStatusTrayIcon(icon);
|
||||
IndicatorStatusIcon.addIconToPanel(trayIcon);
|
||||
}
|
||||
|
||||
onTrayIconRemoved(_tray, icon) {
|
||||
try {
|
||||
const [trayIcon] = IndicatorStatusIcon.getTrayIcons().filter(i => i.icon === icon);
|
||||
trayIcon.destroy();
|
||||
} catch (e) {
|
||||
Util.Logger.warning(`No icon container found for ${icon.title} (${icon})`);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.emit('destroy');
|
||||
SettingsManager.getDefaultGSettings().disconnect(this._changedId);
|
||||
this._disable();
|
||||
trayIconsManager = null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,434 @@
|
||||
// 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 Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import GObject from 'gi://GObject';
|
||||
import St from 'gi://St';
|
||||
|
||||
const ByteArray = imports.byteArray;
|
||||
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
import * as Config from 'resource:///org/gnome/shell/misc/config.js';
|
||||
import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
|
||||
|
||||
import {BaseStatusIcon} from './indicatorStatusIcon.js';
|
||||
|
||||
export const BUS_ADDRESS_REGEX = /([a-zA-Z0-9._-]+\.[a-zA-Z0-9.-]+)|(:[0-9]+\.[0-9]+)$/;
|
||||
|
||||
Gio._promisify(Gio.DBusConnection.prototype, 'call');
|
||||
Gio._promisify(Gio._LocalFilePrototype, 'read');
|
||||
Gio._promisify(Gio.InputStream.prototype, 'read_bytes_async');
|
||||
|
||||
export function indicatorId(service, busName, objectPath) {
|
||||
if (service !== busName && service?.match(BUS_ADDRESS_REGEX))
|
||||
return service;
|
||||
|
||||
return `${busName}@${objectPath}`;
|
||||
}
|
||||
|
||||
export async function getUniqueBusName(bus, name, cancellable) {
|
||||
if (name[0] === ':')
|
||||
return name;
|
||||
|
||||
if (!bus)
|
||||
bus = Gio.DBus.session;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export async function getBusNames(bus, cancellable) {
|
||||
if (!bus)
|
||||
bus = Gio.DBus.session;
|
||||
|
||||
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 Map();
|
||||
const requests = names.map(name => getUniqueBusName(bus, name, cancellable));
|
||||
const results = await Promise.allSettled(requests);
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const result = results[i];
|
||||
if (result.status === 'fulfilled') {
|
||||
let namesForBus = uniqueNames.get(result.value);
|
||||
if (!namesForBus) {
|
||||
namesForBus = new Set();
|
||||
uniqueNames.set(result.value, namesForBus);
|
||||
}
|
||||
namesForBus.add(result.value !== names[i] ? names[i] : null);
|
||||
} else if (!result.reason.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
|
||||
Logger.debug(`Impossible to get the unique name of ${names[i]}: ${result.reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueNames;
|
||||
}
|
||||
|
||||
async function getProcessId(connectionName, cancellable = null, bus = Gio.DBus.session) {
|
||||
const res = await bus.call('org.freedesktop.DBus', '/',
|
||||
'org.freedesktop.DBus', 'GetConnectionUnixProcessID',
|
||||
new GLib.Variant('(s)', [connectionName]),
|
||||
new GLib.VariantType('(u)'),
|
||||
Gio.DBusCallFlags.NONE,
|
||||
-1,
|
||||
cancellable);
|
||||
const [pid] = res.deepUnpack();
|
||||
return pid;
|
||||
}
|
||||
|
||||
export async function getProcessName(connectionName, cancellable = null,
|
||||
priority = GLib.PRIORITY_DEFAULT, bus = Gio.DBus.session) {
|
||||
const pid = await getProcessId(connectionName, cancellable, bus);
|
||||
const cmdFile = Gio.File.new_for_path(`/proc/${pid}/cmdline`);
|
||||
const inputStream = await cmdFile.read_async(priority, cancellable);
|
||||
const bytes = await inputStream.read_bytes_async(2048, priority, cancellable);
|
||||
return ByteArray.toString(bytes.toArray().map(v => !v ? 0x20 : v));
|
||||
}
|
||||
|
||||
export async function* introspectBusObject(bus, name, cancellable,
|
||||
interfaces = undefined, 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,
|
||||
5000, cancellable)).deep_unpack();
|
||||
|
||||
const nodeInfo = Gio.DBusNodeInfo.new_for_xml(introspection);
|
||||
|
||||
if (!interfaces || dbusNodeImplementsInterfaces(nodeInfo, interfaces))
|
||||
yield {nodeInfo, path};
|
||||
|
||||
if (path === '/')
|
||||
path = '';
|
||||
|
||||
for (const subNodeInfo of nodeInfo.nodes) {
|
||||
const subPath = `${path}/${subNodeInfo.path}`;
|
||||
yield* introspectBusObject(bus, name, cancellable, interfaces, subPath);
|
||||
}
|
||||
}
|
||||
|
||||
function dbusNodeImplementsInterfaces(nodeInfo, interfaces) {
|
||||
if (!(nodeInfo instanceof Gio.DBusNodeInfo) || !Array.isArray(interfaces))
|
||||
return false;
|
||||
|
||||
return interfaces.some(iface => nodeInfo.lookup_interface(iface));
|
||||
}
|
||||
|
||||
export class NameWatcher extends Signals.EventEmitter {
|
||||
constructor(name) {
|
||||
super();
|
||||
|
||||
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');
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.emit('destroy');
|
||||
|
||||
Gio.DBus.session.unwatch_name(this._watcherId);
|
||||
delete this._watcherId;
|
||||
}
|
||||
|
||||
get nameOnBus() {
|
||||
return !!this._nameOnBus;
|
||||
}
|
||||
}
|
||||
|
||||
function connectSmart3A(src, signal, handler) {
|
||||
const id = src.connect(signal, handler);
|
||||
let destroyId = 0;
|
||||
|
||||
if (src.connect && (!(src instanceof GObject.Object) || GObject.signal_lookup('destroy', src))) {
|
||||
destroyId = src.connect('destroy', () => {
|
||||
src.disconnect(id);
|
||||
src.disconnect(destroyId);
|
||||
});
|
||||
}
|
||||
|
||||
return [id, 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
|
||||
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;
|
||||
|
||||
return [signalId, srcDestroyId, tgtDestroyId];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line valid-jsdoc
|
||||
/**
|
||||
* Connect signals to slots, and remove the connection when either source or
|
||||
* target are destroyed
|
||||
*
|
||||
* Usage:
|
||||
* Util.connectSmart(srcOb, 'signal', tgtObj, 'handler')
|
||||
* or
|
||||
* Util.connectSmart(srcOb, 'signal', () => { ... })
|
||||
*/
|
||||
export function connectSmart(...args) {
|
||||
if (arguments.length === 4)
|
||||
return connectSmart4A(...args);
|
||||
else
|
||||
return connectSmart3A(...args);
|
||||
}
|
||||
|
||||
function disconnectSmart3A(src, signalIds) {
|
||||
const [id, destroyId] = signalIds;
|
||||
src.disconnect(id);
|
||||
|
||||
if (destroyId)
|
||||
src.disconnect(destroyId);
|
||||
}
|
||||
|
||||
function disconnectSmart4A(src, tgt, signalIds) {
|
||||
const [signalId, srcDestroyId, tgtDestroyId] = signalIds;
|
||||
|
||||
disconnectSmart3A(src, [signalId, srcDestroyId]);
|
||||
|
||||
if (tgtDestroyId)
|
||||
tgt.disconnect(tgtDestroyId);
|
||||
}
|
||||
|
||||
export function disconnectSmart(...args) {
|
||||
if (arguments.length === 2)
|
||||
return disconnectSmart3A(...args);
|
||||
else if (arguments.length === 3)
|
||||
return disconnectSmart4A(...args);
|
||||
|
||||
throw new TypeError('Unexpected number of arguments');
|
||||
}
|
||||
|
||||
let _defaultTheme;
|
||||
export function getDefaultTheme() {
|
||||
if (_defaultTheme)
|
||||
return _defaultTheme;
|
||||
|
||||
_defaultTheme = new St.IconTheme();
|
||||
return _defaultTheme;
|
||||
}
|
||||
|
||||
export function destroyDefaultTheme() {
|
||||
_defaultTheme = null;
|
||||
}
|
||||
|
||||
// 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.
|
||||
*/
|
||||
export async function waitForStartupCompletion(cancellable) {
|
||||
if (Main.layoutManager._startingUp)
|
||||
await Main.layoutManager.connect_once('startup-complete', cancellable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for logging stuff
|
||||
*/
|
||||
export class Logger {
|
||||
static _logStructured(logLevel, message, extraFields = {}) {
|
||||
if (!Object.values(GLib.LogLevelFlags).includes(logLevel)) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_WARNING,
|
||||
'logLevel is not a valid GLib.LogLevelFlags');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Logger._levels.includes(logLevel))
|
||||
return;
|
||||
|
||||
let fields = {
|
||||
'SYSLOG_IDENTIFIER': this.uuid,
|
||||
'MESSAGE': `${message}`,
|
||||
};
|
||||
|
||||
let thisFile = null;
|
||||
const {stack} = new Error();
|
||||
for (let stackLine of stack.split('\n')) {
|
||||
stackLine = stackLine.replace('resource:///org/gnome/Shell/', '');
|
||||
const [code, line] = stackLine.split(':');
|
||||
const [func, file] = code.split(/@(.+)/);
|
||||
|
||||
if (!thisFile || thisFile === file) {
|
||||
thisFile = file;
|
||||
continue;
|
||||
}
|
||||
|
||||
fields = Object.assign(fields, {
|
||||
'CODE_FILE': file || '',
|
||||
'CODE_LINE': line || '',
|
||||
'CODE_FUNC': func || '',
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
GLib.log_structured(Logger._domain, logLevel, Object.assign(fields, extraFields));
|
||||
}
|
||||
|
||||
static init(extension) {
|
||||
if (Logger._domain)
|
||||
return;
|
||||
|
||||
const allLevels = Object.values(GLib.LogLevelFlags);
|
||||
const domains = GLib.getenv('G_MESSAGES_DEBUG');
|
||||
const {name: domain} = extension.metadata;
|
||||
this.uuid = extension.metadata.uuid;
|
||||
Logger._domain = domain.replaceAll(' ', '-');
|
||||
|
||||
if (domains === 'all' || (domains && domains.split(' ').includes(Logger._domain))) {
|
||||
Logger._levels = allLevels;
|
||||
} else {
|
||||
Logger._levels = allLevels.filter(
|
||||
l => l <= GLib.LogLevelFlags.LEVEL_WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
static debug(message) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_DEBUG, message);
|
||||
}
|
||||
|
||||
static message(message) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_MESSAGE, message);
|
||||
}
|
||||
|
||||
static warn(message) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_WARNING, message);
|
||||
}
|
||||
|
||||
static error(message) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_ERROR, message);
|
||||
}
|
||||
|
||||
static critical(message) {
|
||||
Logger._logStructured(GLib.LogLevelFlags.LEVEL_CRITICAL, message);
|
||||
}
|
||||
}
|
||||
|
||||
export function versionCheck(required) {
|
||||
const current = Config.PACKAGE_VERSION;
|
||||
const currentArray = current.split('.');
|
||||
const [major] = currentArray;
|
||||
return major >= required;
|
||||
}
|
||||
|
||||
export function tryCleanupOldIndicators() {
|
||||
const indicatorType = BaseStatusIcon;
|
||||
const indicators = Object.values(Main.panel.statusArea).filter(i => i instanceof indicatorType);
|
||||
|
||||
try {
|
||||
const panelBoxes = [
|
||||
Main.panel._leftBox, Main.panel._centerBox, Main.panel._rightBox,
|
||||
];
|
||||
|
||||
panelBoxes.forEach(box =>
|
||||
indicators.push(...box.get_children().filter(i => i instanceof indicatorType)));
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
|
||||
new Set(indicators).forEach(i => i.destroy());
|
||||
}
|
||||
|
||||
export const CancellableChild = GObject.registerClass({
|
||||
Properties: {
|
||||
'parent': GObject.ParamSpec.object(
|
||||
'parent', 'parent', 'parent',
|
||||
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
|
||||
Gio.Cancellable.$gtype),
|
||||
},
|
||||
},
|
||||
class CancellableChild extends Gio.Cancellable {
|
||||
_init(parent) {
|
||||
if (parent && !(parent instanceof Gio.Cancellable))
|
||||
throw TypeError('Not a valid cancellable');
|
||||
|
||||
super._init({parent});
|
||||
|
||||
if (parent) {
|
||||
if (parent.is_cancelled()) {
|
||||
this.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
this._connectToParent();
|
||||
}
|
||||
}
|
||||
|
||||
_connectToParent() {
|
||||
this._connectId = this.parent.connect(() => {
|
||||
this._realCancel();
|
||||
|
||||
if (this._disconnectIdle)
|
||||
return;
|
||||
|
||||
this._disconnectIdle = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
|
||||
delete this._disconnectIdle;
|
||||
this._disconnectFromParent();
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_disconnectFromParent() {
|
||||
if (this._connectId && !this._disconnectIdle) {
|
||||
this.parent.disconnect(this._connectId);
|
||||
delete this._connectId;
|
||||
}
|
||||
}
|
||||
|
||||
_realCancel() {
|
||||
Gio.Cancellable.prototype.cancel.call(this);
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this._disconnectFromParent();
|
||||
this._realCancel();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user