hook to package
This commit is contained in:
1 parent
5ebbb12d4e
commit
c70376c29b
128 files changed
+26638
-1
No files matched your search
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
gnome-extensions enable dash-to-dock@micxgx.gmail.com
|
||||
gnome-extensions enable appindicatorsupport@rgcjonas.gmail.com
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
update-alternatives --install /usr/share/images/desktop-base/desktop-background desktop-background /usr/share/hfgos-theme/wallpaper/contents/hfgos-background.svg 85
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
+1678
File diff suppressed because it is too large.
Load diff
+972
@@ -0,0 +1,972 @@
|
||||
// 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 MenuUtils from './menuUtils.js';
|
||||
import * as PromiseUtils from './promiseUtils.js';
|
||||
import * as Util from './util.js';
|
||||
import {DBusProxy} from './dbusProxy.js';
|
||||
|
||||
Gio._promisify(GdkPixbuf.Pixbuf, 'new_from_stream_async');
|
||||
|
||||
// ////////////////////////////////////////////////////////////////////////
|
||||
// PART ONE: "ViewModel" backend implementation.
|
||||
// Both code and design are inspired by libdbusmenu
|
||||
// ////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Saves menu property values and handles type checking and defaults
|
||||
*/
|
||||
export class PropertyStore {
|
||||
constructor(initialProperties) {
|
||||
this._props = new Map();
|
||||
|
||||
if (initialProperties) {
|
||||
for (const [prop, value] of Object.entries(initialProperties))
|
||||
this.set(prop, value);
|
||||
}
|
||||
}
|
||||
|
||||
set(name, value) {
|
||||
if (name in PropertyStore.MandatedTypes && value &&
|
||||
!value.is_of_type(PropertyStore.MandatedTypes[name]))
|
||||
Util.Logger.warn(`Cannot set property ${name}: type mismatch!`);
|
||||
else if (value)
|
||||
this._props.set(name, value);
|
||||
else
|
||||
this._props.delete(name);
|
||||
}
|
||||
|
||||
get(name) {
|
||||
const prop = this._props.get(name);
|
||||
if (prop)
|
||||
return prop;
|
||||
else if (name in PropertyStore.DefaultValues)
|
||||
return PropertyStore.DefaultValues[name];
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// we list all the properties we know and use here, so we won' have to deal with unexpected type mismatches
|
||||
PropertyStore.MandatedTypes = {
|
||||
'visible': GLib.VariantType.new('b'),
|
||||
'enabled': GLib.VariantType.new('b'),
|
||||
'label': GLib.VariantType.new('s'),
|
||||
'type': GLib.VariantType.new('s'),
|
||||
'children-display': GLib.VariantType.new('s'),
|
||||
'icon-name': GLib.VariantType.new('s'),
|
||||
'icon-data': GLib.VariantType.new('ay'),
|
||||
'toggle-type': GLib.VariantType.new('s'),
|
||||
'toggle-state': GLib.VariantType.new('i'),
|
||||
};
|
||||
|
||||
PropertyStore.DefaultValues = {
|
||||
'visible': GLib.Variant.new_boolean(true),
|
||||
'enabled': GLib.Variant.new_boolean(true),
|
||||
'label': GLib.Variant.new_string(''),
|
||||
'type': GLib.Variant.new_string('standard'),
|
||||
// elements not in here must return null
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a single menu item
|
||||
*/
|
||||
export class DbusMenuItem extends Signals.EventEmitter {
|
||||
// will steal the properties object
|
||||
constructor(client, id, properties, childrenIds) {
|
||||
super();
|
||||
|
||||
this._client = client;
|
||||
this._id = id;
|
||||
this._propStore = new PropertyStore(properties);
|
||||
this._children_ids = childrenIds;
|
||||
}
|
||||
|
||||
propertyGet(propName) {
|
||||
const prop = this.propertyGetVariant(propName);
|
||||
return prop ? prop.get_string()[0] : null;
|
||||
}
|
||||
|
||||
propertyGetVariant(propName) {
|
||||
return this._propStore.get(propName);
|
||||
}
|
||||
|
||||
propertyGetBool(propName) {
|
||||
const prop = this.propertyGetVariant(propName);
|
||||
return prop ? prop.get_boolean() : false;
|
||||
}
|
||||
|
||||
propertyGetInt(propName) {
|
||||
const prop = this.propertyGetVariant(propName);
|
||||
return prop ? prop.get_int32() : 0;
|
||||
}
|
||||
|
||||
propertySet(prop, value) {
|
||||
this._propStore.set(prop, value);
|
||||
|
||||
this.emit('property-changed', prop, this.propertyGetVariant(prop));
|
||||
}
|
||||
|
||||
resetProperties() {
|
||||
Object.entries(PropertyStore.DefaultValues).forEach(([prop, value]) =>
|
||||
this.propertySet(prop, value));
|
||||
}
|
||||
|
||||
getChildrenIds() {
|
||||
return this._children_ids.concat(); // clone it!
|
||||
}
|
||||
|
||||
addChild(pos, childId) {
|
||||
this._children_ids.splice(pos, 0, childId);
|
||||
this.emit('child-added', this._client.getItem(childId), pos);
|
||||
}
|
||||
|
||||
removeChild(childId) {
|
||||
// find it
|
||||
let pos = -1;
|
||||
for (let i = 0; i < this._children_ids.length; ++i) {
|
||||
if (this._children_ids[i] === childId) {
|
||||
pos = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pos < 0) {
|
||||
Util.Logger.critical("Trying to remove child which doesn't exist");
|
||||
} else {
|
||||
this._children_ids.splice(pos, 1);
|
||||
this.emit('child-removed', this._client.getItem(childId));
|
||||
}
|
||||
}
|
||||
|
||||
moveChild(childId, newPos) {
|
||||
// find the old position
|
||||
let oldPos = -1;
|
||||
for (let i = 0; i < this._children_ids.length; ++i) {
|
||||
if (this._children_ids[i] === childId) {
|
||||
oldPos = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldPos < 0) {
|
||||
Util.Logger.critical("tried to move child which wasn't in the list");
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldPos !== newPos) {
|
||||
this._children_ids.splice(oldPos, 1);
|
||||
this._children_ids.splice(newPos, 0, childId);
|
||||
this.emit('child-moved', this._client.getItem(childId), oldPos, newPos);
|
||||
}
|
||||
}
|
||||
|
||||
getChildren() {
|
||||
return this._children_ids.map(el => this._client.getItem(el));
|
||||
}
|
||||
|
||||
hasChildren() {
|
||||
return this._children_ids.length > 0;
|
||||
}
|
||||
|
||||
handleEvent(event, data, timestamp) {
|
||||
if (!data)
|
||||
data = GLib.Variant.new_int32(0);
|
||||
|
||||
return this._client.sendEvent(this._id, event, data, timestamp);
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
sendAboutToShow() {
|
||||
this._client.sendAboutToShow(this._id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The client does the heavy lifting of actually reading layouts and distributing events
|
||||
*/
|
||||
|
||||
export const DBusClient = GObject.registerClass({
|
||||
Signals: {'ready-changed': {}},
|
||||
}, class AppIndicatorsDBusClient extends DBusProxy {
|
||||
static get interfaceInfo() {
|
||||
if (!this._interfaceInfo) {
|
||||
this._interfaceInfo = Gio.DBusInterfaceInfo.new_for_xml(
|
||||
DBusInterfaces.DBusMenu);
|
||||
}
|
||||
return this._interfaceInfo;
|
||||
}
|
||||
|
||||
static get baseItems() {
|
||||
if (!this._baseItems) {
|
||||
this._baseItems = {
|
||||
'children-display': GLib.Variant.new_string('submenu'),
|
||||
};
|
||||
}
|
||||
return this._baseItems;
|
||||
}
|
||||
|
||||
static destroy() {
|
||||
delete this._interfaceInfo;
|
||||
}
|
||||
|
||||
_init(busName, objectPath) {
|
||||
const {interfaceInfo} = AppIndicatorsDBusClient;
|
||||
|
||||
super._init(busName, objectPath, interfaceInfo,
|
||||
Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES);
|
||||
|
||||
this._items = new Map();
|
||||
this._items.set(0, new DbusMenuItem(this, 0, DBusClient.baseItems, []));
|
||||
this._flagItemsUpdateRequired = false;
|
||||
|
||||
// will be set to true if a layout update is needed once active
|
||||
this._flagLayoutUpdateRequired = false;
|
||||
|
||||
// property requests are queued
|
||||
this._propertiesRequestedFor = new Set(/* ids */);
|
||||
|
||||
this._layoutUpdated = false;
|
||||
this._active = false;
|
||||
}
|
||||
|
||||
async initAsync(cancellable) {
|
||||
await super.initAsync(cancellable);
|
||||
|
||||
this._requestLayoutUpdate();
|
||||
}
|
||||
|
||||
_onNameOwnerChanged() {
|
||||
if (this.isReady)
|
||||
this._requestLayoutUpdate();
|
||||
}
|
||||
|
||||
get isReady() {
|
||||
return this._layoutUpdated && !!this.gNameOwner;
|
||||
}
|
||||
|
||||
get cancellable() {
|
||||
return this._cancellable;
|
||||
}
|
||||
|
||||
getRoot() {
|
||||
return this._items.get(0);
|
||||
}
|
||||
|
||||
_requestLayoutUpdate() {
|
||||
const cancellable = new Util.CancellableChild(this._cancellable);
|
||||
this._beginLayoutUpdate(cancellable);
|
||||
}
|
||||
|
||||
async _requestProperties(propertyId, cancellable) {
|
||||
this._propertiesRequestedFor.add(propertyId);
|
||||
|
||||
if (this._propertiesRequest && this._propertiesRequest.pending())
|
||||
return;
|
||||
|
||||
// if we don't have any requests queued, we'll need to add one
|
||||
this._propertiesRequest = new PromiseUtils.IdlePromise(
|
||||
GLib.PRIORITY_DEFAULT_IDLE, cancellable);
|
||||
await this._propertiesRequest;
|
||||
|
||||
const requestedProperties = Array.from(this._propertiesRequestedFor);
|
||||
this._propertiesRequestedFor.clear();
|
||||
const [result] = await this.GetGroupPropertiesAsync(requestedProperties,
|
||||
[], cancellable);
|
||||
|
||||
result.forEach(([id, properties]) => {
|
||||
const item = this._items.get(id);
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
item.resetProperties();
|
||||
|
||||
for (const [prop, value] of Object.entries(properties))
|
||||
item.propertySet(prop, value);
|
||||
});
|
||||
}
|
||||
|
||||
// Traverses the list of cached menu items and removes everyone that is not in the list
|
||||
// so we don't keep alive unused items
|
||||
_gcItems() {
|
||||
const tag = new Date().getTime();
|
||||
|
||||
const toTraverse = [0];
|
||||
while (toTraverse.length > 0) {
|
||||
const item = this.getItem(toTraverse.shift());
|
||||
item._dbusClientGcTag = tag;
|
||||
Array.prototype.push.apply(toTraverse, item.getChildrenIds());
|
||||
}
|
||||
|
||||
this._items.forEach((i, id) => {
|
||||
if (i._dbusClientGcTag !== tag)
|
||||
this._items.delete(id);
|
||||
});
|
||||
}
|
||||
|
||||
// the original implementation will only request partial layouts if somehow possible
|
||||
// we try to save us from multiple kinds of race conditions by always requesting a full layout
|
||||
_beginLayoutUpdate(cancellable) {
|
||||
this._layoutUpdateUpdateAsync(cancellable).catch(e => {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e);
|
||||
});
|
||||
}
|
||||
|
||||
// the original implementation will only request partial layouts if somehow possible
|
||||
// we try to save us from multiple kinds of race conditions by always requesting a full layout
|
||||
async _layoutUpdateUpdateAsync(cancellable) {
|
||||
// we only read the type property, because if the type changes after reading all properties,
|
||||
// the view would have to replace the item completely which we try to avoid
|
||||
if (this._layoutUpdateCancellable)
|
||||
this._layoutUpdateCancellable.cancel();
|
||||
|
||||
this._layoutUpdateCancellable = cancellable;
|
||||
|
||||
try {
|
||||
const [revision_, root] = await this.GetLayoutAsync(0, -1,
|
||||
['type', 'children-display'], cancellable);
|
||||
|
||||
this._updateLayoutState(true);
|
||||
this._doLayoutUpdate(root, cancellable);
|
||||
this._gcItems();
|
||||
this._flagLayoutUpdateRequired = false;
|
||||
this._flagItemsUpdateRequired = false;
|
||||
} catch (e) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
this._updateLayoutState(false);
|
||||
throw e;
|
||||
} finally {
|
||||
if (this._layoutUpdateCancellable === cancellable)
|
||||
this._layoutUpdateCancellable = null;
|
||||
}
|
||||
}
|
||||
|
||||
_updateLayoutState(state) {
|
||||
const wasReady = this.isReady;
|
||||
this._layoutUpdated = state;
|
||||
if (this.isReady !== wasReady)
|
||||
this.emit('ready-changed');
|
||||
}
|
||||
|
||||
_doLayoutUpdate(item, cancellable) {
|
||||
const [id, properties, children] = item;
|
||||
|
||||
const childrenUnpacked = children.map(c => c.deep_unpack());
|
||||
const childrenIds = childrenUnpacked.map(([c]) => c);
|
||||
|
||||
// make sure all our children exist
|
||||
childrenUnpacked.forEach(c => this._doLayoutUpdate(c, cancellable));
|
||||
|
||||
// make sure we exist
|
||||
const menuItem = this._items.get(id);
|
||||
|
||||
if (menuItem) {
|
||||
// we do, update our properties if necessary
|
||||
for (const [prop, value] of Object.entries(properties))
|
||||
menuItem.propertySet(prop, value);
|
||||
|
||||
// make sure our children are all at the right place, and exist
|
||||
const oldChildrenIds = menuItem.getChildrenIds();
|
||||
for (let i = 0; i < childrenIds.length; ++i) {
|
||||
// try to recycle an old child
|
||||
let oldChild = -1;
|
||||
for (let j = 0; j < oldChildrenIds.length; ++j) {
|
||||
if (oldChildrenIds[j] === childrenIds[i]) {
|
||||
[oldChild] = oldChildrenIds.splice(j, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldChild < 0) {
|
||||
// no old child found, so create a new one!
|
||||
menuItem.addChild(i, childrenIds[i]);
|
||||
} else {
|
||||
// old child found, reuse it!
|
||||
menuItem.moveChild(childrenIds[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
// remove any old children that weren't reused
|
||||
oldChildrenIds.forEach(c => menuItem.removeChild(c));
|
||||
|
||||
if (!this._flagItemsUpdateRequired)
|
||||
return id;
|
||||
}
|
||||
|
||||
// we don't, so let's create us
|
||||
let newMenuItem = menuItem;
|
||||
|
||||
if (!newMenuItem) {
|
||||
newMenuItem = new DbusMenuItem(this, id, properties, childrenIds);
|
||||
this._items.set(id, newMenuItem);
|
||||
}
|
||||
|
||||
this._requestProperties(id, cancellable).catch(e => {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
Util.Logger.warn(`Could not get menu properties menu proxy: ${e}`);
|
||||
});
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
async _doPropertiesUpdateAsync(cancellable) {
|
||||
if (this._propertiesUpdateCancellable)
|
||||
this._propertiesUpdateCancellable.cancel();
|
||||
|
||||
this._propertiesUpdateCancellable = cancellable;
|
||||
|
||||
try {
|
||||
const requests = [];
|
||||
|
||||
this._items.forEach((_, id) =>
|
||||
requests.push(this._requestProperties(id, cancellable)));
|
||||
|
||||
await Promise.all(requests);
|
||||
} finally {
|
||||
if (this._propertiesUpdateCancellable === cancellable)
|
||||
this._propertiesUpdateCancellable = null;
|
||||
}
|
||||
}
|
||||
|
||||
_doPropertiesUpdate() {
|
||||
const cancellable = new Util.CancellableChild(this._cancellable);
|
||||
this._doPropertiesUpdateAsync(cancellable).catch(e => {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
Util.Logger.warn(`Could not get menu properties menu proxy: ${e}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
set active(active) {
|
||||
const wasActive = this._active;
|
||||
this._active = active;
|
||||
|
||||
if (active && wasActive !== active) {
|
||||
if (this._flagLayoutUpdateRequired) {
|
||||
this._requestLayoutUpdate();
|
||||
} else if (this._flagItemsUpdateRequired) {
|
||||
this._doPropertiesUpdate();
|
||||
this._flagItemsUpdateRequired = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onSignal(_sender, signal, params) {
|
||||
if (signal === 'LayoutUpdated') {
|
||||
if (!this._active && this.getRoot()?.hasChildren()) {
|
||||
this._flagLayoutUpdateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this._requestLayoutUpdate();
|
||||
} else if (signal === 'ItemsPropertiesUpdated') {
|
||||
if (!this._active && this.getRoot()?.hasChildren()) {
|
||||
this._flagItemsUpdateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this._onPropertiesUpdated(params.deep_unpack());
|
||||
}
|
||||
}
|
||||
|
||||
getItem(id) {
|
||||
const item = this._items.get(id);
|
||||
if (!item)
|
||||
Util.Logger.warn(`trying to retrieve item for non-existing id ${id} !?`);
|
||||
return item || null;
|
||||
}
|
||||
|
||||
// we don't need to cache and burst-send that since it will not happen that frequently
|
||||
async sendAboutToShow(id) {
|
||||
if (this._hasAboutToShow === false)
|
||||
return;
|
||||
|
||||
/* Some indicators (you, dropbox!) don't use the right signature
|
||||
* and don't return a boolean, so we need to support both cases */
|
||||
try {
|
||||
const ret = await this.gConnection.call(this.gName, this.gObjectPath,
|
||||
this.gInterfaceName, 'AboutToShow', new GLib.Variant('(i)', [id]),
|
||||
null, Gio.DBusCallFlags.NONE, -1, this._cancellable);
|
||||
|
||||
if ((ret.is_of_type(new GLib.VariantType('(b)')) &&
|
||||
ret.get_child_value(0).get_boolean()) ||
|
||||
ret.is_of_type(new GLib.VariantType('()')))
|
||||
this._requestLayoutUpdate();
|
||||
} catch (e) {
|
||||
Util.Logger.debug('Error when calling \'AboutToShow()\' in ' +
|
||||
`${this.gName}, ${this.gObjectPath}, ${this.gInterfaceName}`);
|
||||
if (e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_METHOD) ||
|
||||
e.matches(Gio.DBusError, Gio.DBusError.FAILED)) {
|
||||
this._hasAboutToShow = false;
|
||||
return;
|
||||
}
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
async sendEvent(id, event, params, timestamp) {
|
||||
if (!this.gNameOwner)
|
||||
return;
|
||||
|
||||
try {
|
||||
await this.EventAsync(id, event, params, timestamp, this._cancellable);
|
||||
} catch (e) {
|
||||
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
return;
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
_onPropertiesUpdated([changed, removed]) {
|
||||
changed.forEach(([id, props]) => {
|
||||
const item = this._items.get(id);
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
for (const [prop, value] of Object.entries(props))
|
||||
item.propertySet(prop, value);
|
||||
});
|
||||
removed.forEach(([id, propNames]) => {
|
||||
const item = this._items.get(id);
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
propNames.forEach(propName => item.propertySet(propName, null));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ////////////////////////////////////////////////////////////////////////
|
||||
// PART TWO: "View" frontend implementation.
|
||||
// ////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// https://bugzilla.gnome.org/show_bug.cgi?id=731514
|
||||
// GNOME 3.10 and 3.12 can't open a nested submenu.
|
||||
// Patches have been written, but it's not clear when (if?) they will be applied.
|
||||
// We also don't know whether they will be backported to 3.10, so we will work around
|
||||
// it in the meantime. Offending versions can be clearly identified:
|
||||
const NEED_NESTED_SUBMENU_FIX = '_setOpenedSubMenu' in PopupMenu.PopupMenu.prototype;
|
||||
|
||||
/**
|
||||
* Creates new wrapper menu items and injects methods for managing them at runtime.
|
||||
*
|
||||
* Many functions in this object will be bound to the created item and executed as event
|
||||
* handlers, so any `this` will refer to a menu item create in createItem
|
||||
*/
|
||||
const MenuItemFactory = {
|
||||
createItem(client, dbusItem) {
|
||||
// first, decide whether it's a submenu or not
|
||||
let shellItem;
|
||||
if (dbusItem.propertyGet('children-display') === 'submenu')
|
||||
shellItem = new PopupMenu.PopupSubMenuMenuItem('FIXME');
|
||||
else if (dbusItem.propertyGet('type') === 'separator')
|
||||
shellItem = new PopupMenu.PopupSeparatorMenuItem('');
|
||||
else
|
||||
shellItem = new PopupMenu.PopupMenuItem('FIXME');
|
||||
|
||||
shellItem._dbusItem = dbusItem;
|
||||
shellItem._dbusClient = client;
|
||||
|
||||
if (shellItem instanceof PopupMenu.PopupMenuItem) {
|
||||
shellItem._icon = new St.Icon({
|
||||
style_class: 'popup-menu-icon',
|
||||
xAlign: Clutter.ActorAlign.END,
|
||||
});
|
||||
shellItem.add_child(shellItem._icon);
|
||||
shellItem.label.x_expand = true;
|
||||
}
|
||||
|
||||
// initialize our state
|
||||
MenuItemFactory._updateLabel.call(shellItem);
|
||||
MenuItemFactory._updateOrnament.call(shellItem);
|
||||
MenuItemFactory._updateImage.call(shellItem);
|
||||
MenuItemFactory._updateVisible.call(shellItem);
|
||||
MenuItemFactory._updateSensitive.call(shellItem);
|
||||
|
||||
// initially create children
|
||||
if (shellItem instanceof PopupMenu.PopupSubMenuMenuItem) {
|
||||
dbusItem.getChildren().forEach(c =>
|
||||
shellItem.menu.addMenuItem(MenuItemFactory.createItem(client, c)));
|
||||
}
|
||||
|
||||
// now, connect various events
|
||||
Util.connectSmart(dbusItem, 'property-changed',
|
||||
shellItem, MenuItemFactory._onPropertyChanged);
|
||||
Util.connectSmart(dbusItem, 'child-added',
|
||||
shellItem, MenuItemFactory._onChildAdded);
|
||||
Util.connectSmart(dbusItem, 'child-removed',
|
||||
shellItem, MenuItemFactory._onChildRemoved);
|
||||
Util.connectSmart(dbusItem, 'child-moved',
|
||||
shellItem, MenuItemFactory._onChildMoved);
|
||||
Util.connectSmart(shellItem, 'activate',
|
||||
shellItem, MenuItemFactory._onActivate);
|
||||
|
||||
shellItem.connect('destroy', () => {
|
||||
shellItem._dbusItemCancellable?.cancel();
|
||||
shellItem._dbusItemCancellable = null;
|
||||
shellItem._dbusItem = null;
|
||||
shellItem._dbusClient = null;
|
||||
shellItem._icon = null;
|
||||
});
|
||||
|
||||
if (shellItem.menu) {
|
||||
Util.connectSmart(shellItem.menu, 'open-state-changed',
|
||||
shellItem, MenuItemFactory._onOpenStateChanged);
|
||||
}
|
||||
|
||||
return shellItem;
|
||||
},
|
||||
|
||||
_onOpenStateChanged(menu, open) {
|
||||
if (open) {
|
||||
if (NEED_NESTED_SUBMENU_FIX) {
|
||||
// close our own submenus
|
||||
if (menu._openedSubMenu)
|
||||
menu._openedSubMenu.close(false);
|
||||
|
||||
// register ourselves and close sibling submenus
|
||||
if (menu._parent._openedSubMenu && menu._parent._openedSubMenu !== menu)
|
||||
menu._parent._openedSubMenu.close(true);
|
||||
|
||||
menu._parent._openedSubMenu = menu;
|
||||
}
|
||||
|
||||
this._dbusItem.handleEvent('opened', null, 0).catch(logError);
|
||||
this._dbusItem.sendAboutToShow();
|
||||
} else {
|
||||
if (NEED_NESTED_SUBMENU_FIX) {
|
||||
// close our own submenus
|
||||
if (menu._openedSubMenu)
|
||||
menu._openedSubMenu.close(false);
|
||||
}
|
||||
|
||||
this._dbusItem.handleEvent('closed', null, 0).catch(logError);
|
||||
}
|
||||
},
|
||||
|
||||
_onActivate(_item, event) {
|
||||
const timestamp = event.get_time();
|
||||
const handleEvent = () =>
|
||||
this._dbusItem.handleEvent('clicked', GLib.Variant.new('i', 0),
|
||||
timestamp).catch(logError);
|
||||
|
||||
if (timestamp && this._dbusClient.indicator) {
|
||||
this._dbusClient.indicator.provideActivationToken(
|
||||
timestamp).catch(logError).finally(handleEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
handleEvent();
|
||||
},
|
||||
|
||||
_onPropertyChanged(dbusItem, prop, _value) {
|
||||
if (prop === 'toggle-type' || prop === 'toggle-state')
|
||||
MenuItemFactory._updateOrnament.call(this);
|
||||
else if (prop === 'label')
|
||||
MenuItemFactory._updateLabel.call(this);
|
||||
else if (prop === 'enabled')
|
||||
MenuItemFactory._updateSensitive.call(this);
|
||||
else if (prop === 'visible')
|
||||
MenuItemFactory._updateVisible.call(this);
|
||||
else if (prop === 'icon-name' || prop === 'icon-data')
|
||||
MenuItemFactory._updateImage.call(this);
|
||||
else if (prop === 'type' || prop === 'children-display')
|
||||
MenuItemFactory._replaceSelf.call(this);
|
||||
else
|
||||
Util.Logger.debug(`Unhandled property change: ${prop}`);
|
||||
},
|
||||
|
||||
_onChildAdded(dbusItem, child, position) {
|
||||
if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) {
|
||||
Util.Logger.warn('Tried to add a child to non-submenu item. Better recreate it as whole');
|
||||
MenuItemFactory._replaceSelf.call(this);
|
||||
} else {
|
||||
this.menu.addMenuItem(MenuItemFactory.createItem(this._dbusClient, child), position);
|
||||
}
|
||||
},
|
||||
|
||||
_onChildRemoved(dbusItem, child) {
|
||||
if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) {
|
||||
Util.Logger.warn('Tried to remove a child from non-submenu item. Better recreate it as whole');
|
||||
MenuItemFactory._replaceSelf.call(this);
|
||||
} else {
|
||||
// find it!
|
||||
this.menu._getMenuItems().forEach(item => {
|
||||
if (item._dbusItem === child)
|
||||
item.destroy();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_onChildMoved(dbusItem, child, oldpos, newpos) {
|
||||
if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) {
|
||||
Util.Logger.warn('Tried to move a child in non-submenu item. Better recreate it as whole');
|
||||
MenuItemFactory._replaceSelf.call(this);
|
||||
} else {
|
||||
MenuUtils.moveItemInMenu(this.menu, child, newpos);
|
||||
}
|
||||
},
|
||||
|
||||
_updateLabel() {
|
||||
const label = this._dbusItem.propertyGet('label').replace(/_([^_])/, '$1');
|
||||
|
||||
if (this.label) // especially on GS3.8, the separator item might not even have a hidden label
|
||||
this.label.set_text(label);
|
||||
},
|
||||
|
||||
_updateOrnament() {
|
||||
if (!this.setOrnament)
|
||||
return; // separators and alike might not have gotten the polyfill
|
||||
|
||||
if (this._dbusItem.propertyGet('toggle-type') === 'checkmark' &&
|
||||
this._dbusItem.propertyGetInt('toggle-state'))
|
||||
this.setOrnament(PopupMenu.Ornament.CHECK);
|
||||
else if (this._dbusItem.propertyGet('toggle-type') === 'radio' &&
|
||||
this._dbusItem.propertyGetInt('toggle-state'))
|
||||
this.setOrnament(PopupMenu.Ornament.DOT);
|
||||
else
|
||||
this.setOrnament(PopupMenu.Ornament.NONE);
|
||||
},
|
||||
|
||||
async _updateImage() {
|
||||
if (!this._icon)
|
||||
return; // might be missing on submenus / separators
|
||||
|
||||
const iconName = this._dbusItem.propertyGet('icon-name');
|
||||
const iconData = this._dbusItem.propertyGetVariant('icon-data');
|
||||
if (iconName) {
|
||||
this._icon.icon_name = iconName;
|
||||
} else if (iconData) {
|
||||
try {
|
||||
if (!this._dbusItemCancellable) {
|
||||
this._dbusItemCancellable = new Util.CancellableChild(
|
||||
this._dbusClient.cancellable);
|
||||
}
|
||||
|
||||
const inputStream = Gio.MemoryInputStream.new_from_bytes(
|
||||
iconData.get_data_as_bytes());
|
||||
this._icon.gicon = await GdkPixbuf.Pixbuf.new_from_stream_async(
|
||||
inputStream, this._dbusItemCancellable);
|
||||
} catch (e) {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_updateVisible() {
|
||||
this.visible = this._dbusItem.propertyGetBool('visible');
|
||||
},
|
||||
|
||||
_updateSensitive() {
|
||||
this.setSensitive(this._dbusItem.propertyGetBool('enabled'));
|
||||
},
|
||||
|
||||
_replaceSelf(newSelf) {
|
||||
// create our new self if needed
|
||||
if (!newSelf)
|
||||
newSelf = MenuItemFactory.createItem(this._dbusClient, this._dbusItem);
|
||||
|
||||
// first, we need to find our old position
|
||||
let pos = -1;
|
||||
const family = this._parent._getMenuItems();
|
||||
for (let i = 0; i < family.length; ++i) {
|
||||
if (family[i] === this)
|
||||
pos = i;
|
||||
}
|
||||
|
||||
if (pos < 0)
|
||||
throw new Error("DBusMenu: can't replace non existing menu item");
|
||||
|
||||
|
||||
// add our new self while we're still alive
|
||||
this._parent.addMenuItem(newSelf, pos);
|
||||
|
||||
// now destroy our old self
|
||||
this.destroy();
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Processes DBus events, creates the menu items and handles the actions
|
||||
*
|
||||
* Something like a mini-god-object
|
||||
*/
|
||||
export class Client extends Signals.EventEmitter {
|
||||
constructor(busName, path, indicator) {
|
||||
super();
|
||||
|
||||
this._busName = busName;
|
||||
this._busPath = path;
|
||||
this._client = new DBusClient(busName, path);
|
||||
this._rootMenu = null; // the shell menu
|
||||
this._rootItem = null; // the DbusMenuItem for the root
|
||||
this.indicator = indicator;
|
||||
this.cancellable = new Util.CancellableChild(this.indicator.cancellable);
|
||||
|
||||
this._client.initAsync(this.cancellable).catch(e => {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e);
|
||||
});
|
||||
|
||||
Util.connectSmart(this._client, 'ready-changed', this,
|
||||
() => this.emit('ready-changed'));
|
||||
}
|
||||
|
||||
get isReady() {
|
||||
return this._client.isReady;
|
||||
}
|
||||
|
||||
// this will attach the client to an already existing menu that will be used as the root menu.
|
||||
// it will also connect the client to be automatically destroyed when the menu dies.
|
||||
attachToMenu(menu) {
|
||||
this._rootMenu = menu;
|
||||
this._rootItem = this._client.getRoot();
|
||||
this._itemsBeingAdded = new Set();
|
||||
|
||||
// cleanup: remove existing children (just in case)
|
||||
this._rootMenu.removeAll();
|
||||
|
||||
if (NEED_NESTED_SUBMENU_FIX)
|
||||
menu._setOpenedSubMenu = this._setOpenedSubmenu.bind(this);
|
||||
|
||||
// connect handlers
|
||||
Util.connectSmart(menu, 'open-state-changed', this, this._onMenuOpenStateChanged);
|
||||
Util.connectSmart(menu, 'destroy', this, this.destroy);
|
||||
|
||||
Util.connectSmart(this._rootItem, 'child-added', this, this._onRootChildAdded);
|
||||
Util.connectSmart(this._rootItem, 'child-removed', this, this._onRootChildRemoved);
|
||||
Util.connectSmart(this._rootItem, 'child-moved', this, this._onRootChildMoved);
|
||||
|
||||
// Dropbox requires us to call AboutToShow(0) first
|
||||
this._rootItem.sendAboutToShow();
|
||||
|
||||
// fill the menu for the first time
|
||||
const children = this._rootItem.getChildren();
|
||||
children.forEach(child =>
|
||||
this._onRootChildAdded(this._rootItem, child));
|
||||
}
|
||||
|
||||
_setOpenedSubmenu(submenu) {
|
||||
if (!submenu)
|
||||
return;
|
||||
|
||||
if (submenu._parent !== this._rootMenu)
|
||||
return;
|
||||
|
||||
if (submenu === this._openedSubMenu)
|
||||
return;
|
||||
|
||||
if (this._openedSubMenu && this._openedSubMenu.isOpen)
|
||||
this._openedSubMenu.close(true);
|
||||
|
||||
this._openedSubMenu = submenu;
|
||||
}
|
||||
|
||||
_onRootChildAdded(dbusItem, child, position) {
|
||||
// Menu additions can be expensive, so let's do it in different chunks
|
||||
const basePriority = this.isOpen ? GLib.PRIORITY_DEFAULT : GLib.PRIORITY_LOW;
|
||||
const idlePromise = new PromiseUtils.IdlePromise(
|
||||
basePriority + this._itemsBeingAdded.size, this.cancellable);
|
||||
this._itemsBeingAdded.add(child);
|
||||
|
||||
idlePromise.then(() => {
|
||||
if (!this._itemsBeingAdded.has(child))
|
||||
return;
|
||||
|
||||
this._rootMenu.addMenuItem(
|
||||
MenuItemFactory.createItem(this, child), position);
|
||||
}).catch(e => {
|
||||
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
logError(e);
|
||||
}).finally(() => this._itemsBeingAdded?.delete(child));
|
||||
}
|
||||
|
||||
_onRootChildRemoved(dbusItem, child) {
|
||||
// children like to play hide and seek
|
||||
// but we know how to find it for sure!
|
||||
const item = this._rootMenu._getMenuItems().find(it =>
|
||||
it._dbusItem === child);
|
||||
|
||||
if (item)
|
||||
item.destroy();
|
||||
else
|
||||
this._itemsBeingAdded.delete(child);
|
||||
}
|
||||
|
||||
_onRootChildMoved(dbusItem, child, oldpos, newpos) {
|
||||
MenuUtils.moveItemInMenu(this._rootMenu, child, newpos);
|
||||
}
|
||||
|
||||
_onMenuOpenStateChanged(menu, state) {
|
||||
if (!this._rootItem)
|
||||
return;
|
||||
|
||||
this._client.active = state;
|
||||
|
||||
if (state) {
|
||||
if (this._openedSubMenu && this._openedSubMenu.isOpen)
|
||||
this._openedSubMenu.close();
|
||||
|
||||
this._rootItem.handleEvent('opened', null, 0).catch(logError);
|
||||
this._rootItem.sendAboutToShow();
|
||||
} else {
|
||||
this._rootItem.handleEvent('closed', null, 0).catch(e => {
|
||||
if (e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_OBJECT)) {
|
||||
// The menu hay have been removed at this point, thus do not
|
||||
// spam the users about this if it happens.
|
||||
return;
|
||||
}
|
||||
|
||||
logError(e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.emit('destroy');
|
||||
|
||||
this.cancellable.cancel();
|
||||
|
||||
if (this._client)
|
||||
this._client.destroy();
|
||||
|
||||
this._client = null;
|
||||
this._rootItem = null;
|
||||
this._rootMenu = null;
|
||||
this.indicator = null;
|
||||
this._itemsBeingAdded = null;
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
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', () => {
|
||||
if (!this.gNameOwner) {
|
||||
this._cancellable.cancel();
|
||||
this._cancellable = new Gio.Cancellable();
|
||||
}
|
||||
this._onNameOwnerChanged();
|
||||
}));
|
||||
}
|
||||
|
||||
async initAsync(cancellable) {
|
||||
this._cancellable = new CancellableChild(cancellable);
|
||||
await this.init_async(GLib.PRIORITY_DEFAULT, this._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);
|
||||
}
|
||||
});
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
// 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 {Logger} from './logger.js';
|
||||
|
||||
export const BUS_ADDRESS_REGEX = /([a-zA-Z0-9._-]+\.[a-zA-Z0-9.-]+)|(:[0-9]+\.[0-9]+)$/;
|
||||
export const BUS_SNI_NAME_REGEX = /^org\.freedesktop\.StatusNotifierItem-[0-9]+-[0-9]+$/;
|
||||
|
||||
Gio._promisify(Gio.DBusConnection.prototype, 'call');
|
||||
|
||||
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);
|
||||
}
|
||||
if (result.value !== names[i])
|
||||
namesForBus.add(names[i]);
|
||||
} 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;
|
||||
}
|
||||
|
||||
export 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 getProcessNameForPid(pid, cancellable = null,
|
||||
priority = GLib.PRIORITY_DEFAULT) {
|
||||
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);
|
||||
const textDecoder = new TextDecoder();
|
||||
return textDecoder.decode(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));
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
// 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 {Logger} from './logger.js';
|
||||
import {SettingsManager} from './settingsManager.js';
|
||||
|
||||
export default class AppIndicatorExtension extends Extension.Extension {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
Logger.init(this);
|
||||
Interfaces.initialize(this);
|
||||
|
||||
this._isEnabled = false;
|
||||
this._statusNotifierWatcher = null;
|
||||
this._watchDog = new Util.NameWatcher(StatusNotifierWatcher.WATCHER_BUS_NAME);
|
||||
this._watchDogId = 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'] = () => {
|
||||
Logger.debug('Reload detected, destroying old watchdog');
|
||||
this._watchDog.disconnect(this._watchDogId);
|
||||
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, this._watchDog);
|
||||
}
|
||||
}
|
||||
+179
@@ -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.equal(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();
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
id="svg2"
|
||||
sodipodi:docname="custom-icons-symbolic.svg"
|
||||
inkscape:version="1.4 (1:1.4+202410161351+e7c3feb100)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="namedview2"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="31.404053"
|
||||
inkscape:cx="7.4671891"
|
||||
inkscape:cy="8.9001252"
|
||||
inkscape:window-width="1860"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" />
|
||||
<g
|
||||
fill="#222222"
|
||||
id="g3"
|
||||
transform="translate(-0.16009403)">
|
||||
<path
|
||||
d="m 5.207031,1.292969 c -0.390625,-0.390625 -1.023437,-0.390625 -1.414062,0 l -2.5,2.5 c -0.390625,0.390625 -0.390625,1.023437 0,1.414062 l 2.5,2.5 c 0.390625,0.390625 1.023437,0.390625 1.414062,0 l 2.496094,-2.5 c 0.390625,-0.390625 0.390625,-1.023437 0,-1.414062 z m 0,0"
|
||||
id="path1-3" />
|
||||
<path
|
||||
d="m 10,12 c 0,1.65625 -1.34375,3 -3,3 -1.660156,0 -3,-1.34375 -3,-3 0,-1.65625 1.339844,-3 3,-3 1.65625,0 3,1.34375 3,3 z m 0,0"
|
||||
id="path2-6" />
|
||||
<path
|
||||
d="M 11.945312,2.003906 C 11.605469,2.019531 11.300781,2.207031 11.132812,2.5 L 8.8125,6.5 C 8.425781,7.164062 8.90625,8 9.675781,8 h 4.644531 c 0.769532,0 1.25,-0.835938 0.863282,-1.5 l -2.320313,-4 C 12.675781,2.171875 12.320312,1.980469 11.945312,2.003906 Z m 0,0"
|
||||
id="path3" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
+1
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M12.5 2A2.5 2.5 0 0 0 10 4.5 2.5 2.5 0 0 0 12.5 7 2.5 2.5 0 0 0 15 4.5 2.5 2.5 0 0 0 12.5 2zm0 1A1.5 1.5 0 0 1 14 4.5 1.5 1.5 0 0 1 12.5 6 1.5 1.5 0 0 1 11 4.5 1.5 1.5 0 0 1 12.5 3zm-9 5A2.5 2.5 0 0 0 1 10.5 2.5 2.5 0 0 0 3.5 13a2.5 2.5 0 0 0 2.443-2H15v-1H5.95A2.5 2.5 0 0 0 3.5 8zm0 1A1.5 1.5 0 0 1 5 10.5 1.5 1.5 0 0 1 3.5 12 1.5 1.5 0 0 1 2 10.5 1.5 1.5 0 0 1 3.5 9z" style="fill:gray;stroke-width:.70228"/><path d="M-9 4h8v1h-8z" style="fill:gray;fill-opacity:1;stroke-width:1.05153;opacity:.5" transform="scale(-1 1)"/></svg>
|
||||
|
After Width: | Height: | Size: 600 B |
+620
@@ -0,0 +1,620 @@
|
||||
// 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;
|
||||
|
||||
function getClutterSettings() {
|
||||
// When we will depend on GNOME 47 we can just use Clutter.Actor.get_context()
|
||||
try {
|
||||
const clutterContext = global.stage.context;
|
||||
return clutterContext.get_settings();
|
||||
} catch {
|
||||
return Clutter.Settings.get_default();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
this.set_style(IndicatorBaseStatusIcon.DEFAULT_STYLE);
|
||||
}
|
||||
|
||||
_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;
|
||||
|
||||
Util.disconnectSmart(settings, this, this._compactModeEnabledIds);
|
||||
delete this._compactModeEnabledIds;
|
||||
} 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);
|
||||
this._compactModeEnabledIds =
|
||||
Util.connectSmart(settings, 'changed::compact-mode-enabled', this,
|
||||
this._updateCompactMode);
|
||||
}
|
||||
}
|
||||
|
||||
static get DEFAULT_STYLE() {
|
||||
const settings = SettingsManager.getDefaultGSettings();
|
||||
if (!settings.get_boolean('compact-mode-enabled'))
|
||||
return null; // drop to default -natural-hpadding.
|
||||
|
||||
return '-natural-hpadding: 10px';
|
||||
}
|
||||
|
||||
_updateCompactMode() {
|
||||
this._icon.set_style(AppIndicator.IconActor.DEFAULT_STYLE);
|
||||
this.set_style(IndicatorBaseStatusIcon.DEFAULT_STYLE);
|
||||
}
|
||||
|
||||
_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));
|
||||
|
||||
// Disable upstream's click gesture and fall back to vfunc_button_press_event etc.
|
||||
this._clickGesture?.set_enabled(false);
|
||||
|
||||
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();
|
||||
Util.addActor(this._labelBin, this._label);
|
||||
Util.addActor(this._box, this._labelBin);
|
||||
}
|
||||
this._label.set_text(label);
|
||||
if (!this._box.contains(this._labelBin))
|
||||
Util.addActor(this._box, this._labelBin); // FIXME: why is it suddenly necessary?
|
||||
} else if (this._label) {
|
||||
this._labelBin.destroy_all_children();
|
||||
Util.removeActor(this._box, 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} = getClutterSettings();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// FIXME: Add a double click gesture handle when we can depend on GNOME 49.
|
||||
_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} = getClutterSettings();
|
||||
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_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,
|
||||
});
|
||||
}
|
||||
});
|
||||
+66
@@ -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>
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
<!-- 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="IconAccessibleDesc" type="s" access="read"/> -->
|
||||
|
||||
<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"/>
|
||||
|
||||
<!-- <property name="AttentionAccessibleDesc" 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>
|
||||
+38
@@ -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>
|
||||
+52
@@ -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;
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+105
@@ -0,0 +1,105 @@
|
||||
// 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';
|
||||
|
||||
/**
|
||||
* 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': Logger._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;
|
||||
Logger._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 destroy() {
|
||||
delete Logger._domain;
|
||||
delete Logger._uuid;
|
||||
delete Logger._levels;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
// 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.
|
||||
|
||||
|
||||
/**
|
||||
* Utility functions not necessarily belonging into the item factory
|
||||
*/
|
||||
|
||||
export function 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_child(family[i]);
|
||||
|
||||
// skip the rest
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"_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",
|
||||
"46",
|
||||
"47",
|
||||
"48",
|
||||
"49",
|
||||
"50",
|
||||
"51"
|
||||
],
|
||||
"url": "https://github.com/ubuntu/gnome-shell-extension-appindicator",
|
||||
"uuid": "appindicatorsupport@rgcjonas.gmail.com",
|
||||
"version": 66
|
||||
}
|
||||
+68
@@ -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};
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
/* exported GeneralPage*/
|
||||
|
||||
import Adw from 'gi://Adw';
|
||||
import Gtk from 'gi://Gtk';
|
||||
import GObject from 'gi://GObject';
|
||||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import {gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
|
||||
|
||||
const IconData = GObject.registerClass({
|
||||
GTypeName: 'IconData',
|
||||
Properties: {
|
||||
'id': GObject.ParamSpec.string(
|
||||
'id',
|
||||
'Id',
|
||||
'A read and write string property',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
''
|
||||
),
|
||||
'name': GObject.ParamSpec.string(
|
||||
'name',
|
||||
'Name',
|
||||
'A read and write string property',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
''
|
||||
),
|
||||
'at-name': GObject.ParamSpec.string(
|
||||
'at-name',
|
||||
'At Name',
|
||||
'A read and write string property',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
''
|
||||
),
|
||||
|
||||
},
|
||||
}, class IconData extends GObject.Object {
|
||||
constructor(props = {}) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
get id() {
|
||||
if (this._id === 'undefined')
|
||||
this._id = null;
|
||||
return this._id;
|
||||
}
|
||||
|
||||
set id(value) {
|
||||
if (this.id === value)
|
||||
return;
|
||||
|
||||
this._id = value;
|
||||
this.notify('id');
|
||||
}
|
||||
|
||||
get name() {
|
||||
if (this._name === 'undefined')
|
||||
this._name = null;
|
||||
return this._name;
|
||||
}
|
||||
|
||||
set name(value) {
|
||||
if (this.name === value)
|
||||
return;
|
||||
|
||||
this._name = value;
|
||||
this.notify('name');
|
||||
}
|
||||
|
||||
get atName() {
|
||||
if (this._atName === 'undefined')
|
||||
this._atName = null;
|
||||
return this._atName;
|
||||
}
|
||||
|
||||
set atName(value) {
|
||||
if (this.atName === value)
|
||||
return;
|
||||
|
||||
this._atName = value;
|
||||
this.notify('at-name');
|
||||
}
|
||||
});
|
||||
|
||||
export var CustomIconPage = GObject.registerClass(
|
||||
class AppIndicatorCustomIconPage extends Adw.PreferencesPage {
|
||||
_init(settings, settingsKey) {
|
||||
super._init({
|
||||
title: _('Custom Icons'),
|
||||
icon_name: 'custom-icons-symbolic',
|
||||
name: 'Custom Icons Page',
|
||||
});
|
||||
this._settings = settings;
|
||||
this._settingsKey = settingsKey;
|
||||
|
||||
this.group = new Adw.PreferencesGroup();
|
||||
|
||||
this.container = new Gtk.Box({
|
||||
halign: Gtk.Align.FILL,
|
||||
hexpand: true,
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
});
|
||||
|
||||
this.listStore = new Gio.ListStore({
|
||||
item_type: IconData,
|
||||
});
|
||||
|
||||
const initList = this._settings.get_value(this._settingsKey.CUSTOM_ICONS).deep_unpack();
|
||||
initList.forEach(pair => {
|
||||
let data = new IconData({
|
||||
id: pair[0],
|
||||
name: pair[1],
|
||||
atName: pair[2],
|
||||
});
|
||||
this.listStore.append(data);
|
||||
});
|
||||
|
||||
this.listStore.append(new IconData({id: '', name: '', atName: ''}));
|
||||
|
||||
this.selectionModel = new Gtk.SingleSelection({
|
||||
model: this.listStore,
|
||||
});
|
||||
|
||||
this.columnView = new Gtk.ColumnView({
|
||||
hexpand: true,
|
||||
model: this.selectionModel,
|
||||
reorderable: false,
|
||||
});
|
||||
|
||||
const columnTitles = [
|
||||
_('Indicator ID'),
|
||||
_('Icon Name'),
|
||||
_('Attention Icon Name'),
|
||||
];
|
||||
const indicatorIdColumn = new Gtk.ColumnViewColumn({
|
||||
title: columnTitles[0],
|
||||
expand: true,
|
||||
});
|
||||
const iconNameColumn = new Gtk.ColumnViewColumn({
|
||||
title: columnTitles[1],
|
||||
expand: true,
|
||||
});
|
||||
const attentionIconNameColumn = new Gtk.ColumnViewColumn({
|
||||
title: columnTitles[2],
|
||||
expand: true,
|
||||
});
|
||||
|
||||
|
||||
const factoryId = new Gtk.SignalListItemFactory();
|
||||
factoryId.connect('setup', (_widget, item) => {
|
||||
const label = new Gtk.EditableLabel({text: ''});
|
||||
item.set_child(label);
|
||||
});
|
||||
factoryId.connect('bind', (_widget, item) => {
|
||||
const label = item.get_child();
|
||||
const data = item.get_item();
|
||||
label.set_text(data.id);
|
||||
label.bind_property('text', data, 'id', GObject.BindingFlags.SYNC_CREATE);
|
||||
label.connect('changed', () => this._autoSave(item));
|
||||
});
|
||||
|
||||
const factoryName = new Gtk.SignalListItemFactory();
|
||||
factoryName.connect('setup', (_widget, item) => {
|
||||
const label = new Gtk.EditableLabel({text: ''});
|
||||
item.set_child(label);
|
||||
});
|
||||
factoryName.connect('bind', (_widget, item) => {
|
||||
const label = item.get_child();
|
||||
const data = item.get_item();
|
||||
label.set_text(data.name);
|
||||
label.bind_property('text', data, 'name', GObject.BindingFlags.SYNC_CREATE);
|
||||
label.connect('changed', () => this._autoSave(item));
|
||||
});
|
||||
|
||||
const factoryItName = new Gtk.SignalListItemFactory();
|
||||
factoryItName.connect('setup', (_widget, item) => {
|
||||
const label = new Gtk.EditableLabel({text: ''});
|
||||
item.set_child(label);
|
||||
});
|
||||
factoryItName.connect('bind', (_widget, item) => {
|
||||
const label = item.get_child();
|
||||
const data = item.get_item();
|
||||
label.set_text(data.atName);
|
||||
label.bind_property('text', data, 'at-name', GObject.BindingFlags.SYNC_CREATE);
|
||||
label.connect('changed', () => this._autoSave(item));
|
||||
});
|
||||
|
||||
indicatorIdColumn.set_factory(factoryId);
|
||||
iconNameColumn.set_factory(factoryName);
|
||||
attentionIconNameColumn.set_factory(factoryItName);
|
||||
|
||||
this.columnView.append_column(indicatorIdColumn);
|
||||
this.columnView.append_column(iconNameColumn);
|
||||
this.columnView.append_column(attentionIconNameColumn);
|
||||
|
||||
this.container.append(this.columnView);
|
||||
|
||||
this.group.add(this.container);
|
||||
this.add(this.group);
|
||||
}
|
||||
|
||||
_autoSave(item) {
|
||||
const storeLength = this.listStore.get_n_items();
|
||||
const newStore = [];
|
||||
|
||||
for (let i = 0; i < storeLength; i++) {
|
||||
const currentItem = this.listStore.get_item(i);
|
||||
const id = currentItem.id;
|
||||
const name = currentItem.name;
|
||||
const atName = currentItem.atName;
|
||||
if (id && name)
|
||||
newStore.push([id, name, atName || '']);
|
||||
}
|
||||
|
||||
this._settings.set_value(this._settingsKey.CUSTOM_ICONS,
|
||||
new GLib.Variant('a(sss)', newStore));
|
||||
|
||||
const id = item.get_item().id;
|
||||
const name = item.get_item().name;
|
||||
|
||||
/* dynamic new entry*/
|
||||
if (storeLength === 1 && (id || name))
|
||||
this.listStore.append(new IconData({id: '', name: '', atName: ''}));
|
||||
|
||||
if (storeLength > 1) {
|
||||
if ((id || name) && item.get_position() >= storeLength - 1)
|
||||
this.listStore.append(new IconData({id: '', name: '', atName: ''}));
|
||||
}
|
||||
}
|
||||
|
||||
_autoDelete(item) {
|
||||
const storeLength = this.listStore.get_n_items();
|
||||
const id = item.get_item().id;
|
||||
const name = item.get_item().name;
|
||||
if (storeLength > 1 && !id && !name &&
|
||||
item.get_position() < storeLength - 1)
|
||||
this.listStore.remove(item.get_position());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
/* exported GeneralPage*/
|
||||
|
||||
import Adw from 'gi://Adw';
|
||||
import Gtk from 'gi://Gtk';
|
||||
import GObject from 'gi://GObject';
|
||||
import {gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
|
||||
|
||||
|
||||
export var GeneralPage = GObject.registerClass(
|
||||
class AppIndicatorGeneralPage extends Adw.PreferencesPage {
|
||||
_init(settings, settingsKey) {
|
||||
super._init({
|
||||
title: _('General'),
|
||||
icon_name: 'general-preferences-symbolic',
|
||||
name: 'General Page',
|
||||
});
|
||||
|
||||
this._settings = settings;
|
||||
this._settingsKey = settingsKey;
|
||||
|
||||
this.group = new Adw.PreferencesGroup(); // no title since only single group
|
||||
|
||||
const legacyTraySwitch = new Adw.SwitchRow({
|
||||
title: _('Enable Legacy Tray Icons support'),
|
||||
subtitle: _('Add X11 legacy tray icons to the panel area'),
|
||||
active: this._settings.get_boolean(this._settingsKey.LEGACY_TRAY_ENABLED),
|
||||
});
|
||||
|
||||
legacyTraySwitch.connect('notify::active', widget =>
|
||||
this._settings.set_boolean(
|
||||
this._settingsKey.LEGACY_TRAY_ENABLED,
|
||||
widget.get_active()
|
||||
)
|
||||
);
|
||||
|
||||
this.group.add(legacyTraySwitch);
|
||||
|
||||
const compactModeSwitch = new Adw.SwitchRow({
|
||||
title: _('Compact Mode'),
|
||||
subtitle: _('Puts tray indicators closer together'),
|
||||
active: this._settings.get_boolean(this._settingsKey.COMPACT_MODE_ENABLED),
|
||||
});
|
||||
|
||||
compactModeSwitch.connect('notify::active', widget =>
|
||||
this._settings.set_boolean(
|
||||
this._settingsKey.COMPACT_MODE_ENABLED,
|
||||
widget.get_active()
|
||||
)
|
||||
);
|
||||
|
||||
this.group.add(compactModeSwitch);
|
||||
|
||||
this._createSpinRow({
|
||||
title: _('Opacity'),
|
||||
settingsKey: this._settingsKey.ICON_OPACITY,
|
||||
from: 0,
|
||||
to: 255,
|
||||
step: 1,
|
||||
round: true,
|
||||
});
|
||||
|
||||
this._createSpinRow({
|
||||
title: _('Desaturation'),
|
||||
settingsKey: this._settingsKey.ICON_SATURATION,
|
||||
from: 0.0,
|
||||
to: 1.0,
|
||||
step: 0.1,
|
||||
});
|
||||
|
||||
this._createSpinRow({
|
||||
title: _('Brightness'),
|
||||
settingsKey: this._settingsKey.ICON_BRIGHTNESS,
|
||||
from: -1.0,
|
||||
to: 1.0,
|
||||
step: 0.1,
|
||||
});
|
||||
|
||||
this._createSpinRow({
|
||||
title: _('Contrast'),
|
||||
settingsKey: this._settingsKey.ICON_CONTRAST,
|
||||
from: -1.0,
|
||||
to: 1.0,
|
||||
step: 0.1,
|
||||
});
|
||||
|
||||
this._createSpinRow({
|
||||
title: _('Icon Size'),
|
||||
settingsKey: this._settingsKey.ICON_SIZE,
|
||||
from: 0,
|
||||
to: 96,
|
||||
step: 2,
|
||||
round: true,
|
||||
});
|
||||
|
||||
const alignmentList = new Gtk.StringList();
|
||||
const comboItems = [
|
||||
{pos: 'center', label: _('Center')},
|
||||
{pos: 'left', label: _('Left')},
|
||||
{pos: 'right', label: _('Right')},
|
||||
];
|
||||
|
||||
comboItems.forEach(item => alignmentList.append(item.label));
|
||||
|
||||
const combo = new Adw.ComboRow({
|
||||
title: _('Tray Horizontal Alignment'),
|
||||
model: alignmentList,
|
||||
});
|
||||
const trayPos = this._settings.get_string(this._settingsKey.TRAY_POS);
|
||||
combo.set_selected(comboItems.findIndex(item => trayPos === item.pos));
|
||||
|
||||
combo.connect('notify::selected', widget => this._settings.set_string(
|
||||
this._settingsKey.TRAY_POS, comboItems[widget.get_selected()].pos));
|
||||
|
||||
this.group.add(combo);
|
||||
this.add(this.group);
|
||||
}
|
||||
|
||||
_createSpinRow(args) {
|
||||
let title = args.title || 'Default Title';
|
||||
let subtitle = args.subtitle || null;
|
||||
let settingsKey = args.settingsKey || '';
|
||||
let from = args.from || 0;
|
||||
let to = args.to || 100;
|
||||
let step = args.step || 1;
|
||||
let round = args.round || false;
|
||||
|
||||
let spin = Adw.SpinRow.new_with_range(from, to, step);
|
||||
spin.title = title;
|
||||
if (subtitle !== null)
|
||||
spin.subtitle = subtitle;
|
||||
|
||||
if (round)
|
||||
spin.set_value(this._settings.get_int(settingsKey));
|
||||
else
|
||||
spin.set_value(this._settings.get_int(settingsKey));
|
||||
|
||||
|
||||
spin.connect('input', widget => {
|
||||
if (round)
|
||||
this._settings.set_int(settingsKey, parseInt(widget.get_value(), 10));
|
||||
else
|
||||
this._settings.set_double(settingsKey, widget.get_value());
|
||||
|
||||
return false;
|
||||
});
|
||||
spin.connect('output', widget => {
|
||||
if (round)
|
||||
this._settings.set_int(settingsKey, parseInt(widget.get_value(), 10));
|
||||
else
|
||||
this._settings.set_double(settingsKey, widget.get_value());
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.group.add(spin);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
|
||||
|
||||
/* exported init, buildPrefsWidget */
|
||||
|
||||
import Gtk from 'gi://Gtk'; // will be removed
|
||||
import Gdk from 'gi://Gdk';
|
||||
import * as GeneralPreferences from './preferences/generalPage.js';
|
||||
import * as CustomIconPreferences from './preferences/customIconPage.js';
|
||||
|
||||
import {
|
||||
ExtensionPreferences,
|
||||
gettext as _
|
||||
} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
|
||||
|
||||
const SettingsKey = {
|
||||
LEGACY_TRAY_ENABLED: 'legacy-tray-enabled',
|
||||
COMPACT_MODE_ENABLED: 'compact-mode-enabled',
|
||||
ICON_SIZE: 'icon-size',
|
||||
ICON_OPACITY: 'icon-opacity',
|
||||
ICON_SATURATION: 'icon-saturation',
|
||||
ICON_BRIGHTNESS: 'icon-brightness',
|
||||
ICON_CONTRAST: 'icon-contrast',
|
||||
TRAY_POS: 'tray-pos',
|
||||
CUSTOM_ICONS: 'custom-icons',
|
||||
};
|
||||
|
||||
export default class DockPreferences extends ExtensionPreferences {
|
||||
fillPreferencesWindow(window) {
|
||||
const iconTheme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default());
|
||||
if (!iconTheme.get_search_path().includes(`${this.path}/icons`))
|
||||
iconTheme.add_search_path(`${this.path}/icons`);
|
||||
|
||||
|
||||
const settings = this.getSettings();
|
||||
const generalPage = new GeneralPreferences.GeneralPage(settings, SettingsKey);
|
||||
const customIconPage = new CustomIconPreferences.CustomIconPage(settings, SettingsKey);
|
||||
|
||||
window.add(generalPage);
|
||||
window.add(customIconPage);
|
||||
|
||||
window.connect('close-request', () => {
|
||||
window.destroy();
|
||||
});
|
||||
}
|
||||
}
|
||||
+324
@@ -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);
|
||||
BIN
Binary file not shown.
+49
@@ -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="compact-mode-enabled" type="b">
|
||||
<default>true</default>
|
||||
<summary>Compact Mode</summary>
|
||||
<description>Reduces the Spacing Between Indicators</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>
|
||||
+55
@@ -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;
|
||||
}
|
||||
+342
@@ -0,0 +1,342 @@
|
||||
// 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 DBusUtils from './dbusUtils.js';
|
||||
import * as DBusMenu from './dbusMenu.js';
|
||||
|
||||
import {DBusProxy} from './dbusProxy.js';
|
||||
|
||||
Gio._promisify(Gio.Subprocess.prototype, 'wait_async');
|
||||
Gio._promisify(Gio.Subprocess.prototype, 'communicate_async');
|
||||
Gio._promisify(Gio.DataInputStream.prototype, 'read_line_async', 'read_line_finish_utf8');
|
||||
|
||||
|
||||
// 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(extension, 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}: ${e.message}`);
|
||||
}
|
||||
|
||||
this._seekStatusNotifierItems(extension).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);
|
||||
const cancellable = this._cancellable;
|
||||
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, 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.service]));
|
||||
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(extension) {
|
||||
// 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 timeout, and using an external process so that it won't
|
||||
// affect startup or memory (as it seems that gjs is not great at
|
||||
// handling the memory of the bus analyzer async code).
|
||||
const cancellable = this._cancellable;
|
||||
await new PromiseUtils.TimeoutSecondsPromise(2, GLib.PRIORITY_LOW, cancellable);
|
||||
const busAnalyzer = GLib.build_filenamev([
|
||||
extension.path, 'tools', 'busAnalyzer.js',
|
||||
]);
|
||||
|
||||
const subProcess = Gio.Subprocess.new(['gjs', '-m', busAnalyzer],
|
||||
Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);
|
||||
|
||||
const stdOut = subProcess.get_stdout_pipe();
|
||||
const dataInputStream = new Gio.DataInputStream({base_stream: stdOut});
|
||||
const textDecoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const [line] = await dataInputStream.read_line_async(GLib.PRIORITY_DEFAULT,
|
||||
cancellable);
|
||||
if (!line)
|
||||
break;
|
||||
|
||||
try {
|
||||
const {services, name, path} = JSON.parse(textDecoder.decode(line));
|
||||
const ids = [null, ...services].map(s => Util.indicatorId(s, name, path));
|
||||
|
||||
if (ids.every(id => !this._items.has(id))) {
|
||||
const service = services.find(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}`);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await this._registerItem(service, name, path);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
const [, stdErr] = await subProcess.communicate_async(null, cancellable);
|
||||
await subProcess.wait_async(cancellable);
|
||||
|
||||
if (subProcess.get_exit_status() !== 0) {
|
||||
const errorLines = textDecoder.decode(stdErr.toArray()).split('\n');
|
||||
const error = new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED,
|
||||
errorLines[0]);
|
||||
error.stack = `${errorLines.slice(3).join('\n')}${error.stack}`;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
Util.Logger.debug(`Received registration request for service: '${params}'`);
|
||||
const pathIndex = service.indexOf('/');
|
||||
|
||||
if (pathIndex === 0) { // looks like a path
|
||||
busName = invocation.get_sender();
|
||||
objPath = service;
|
||||
} else if (pathIndex > 0) {
|
||||
// Electron started another non-standard format: yay!
|
||||
// They're now providing the service name as first element, followed
|
||||
// by a DBus path. This is wrong, but not much we can do about it.
|
||||
const name = service.slice(0, pathIndex);
|
||||
try {
|
||||
busName = await DBusUtils.getUniqueBusName(invocation.get_connection(),
|
||||
name, this._cancellable);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
objPath = service.slice(pathIndex);
|
||||
} else if (service.match(DBusUtils.BUS_SNI_NAME_REGEX)) {
|
||||
// If the app uses a standard SNI name, use it as is, in this case,
|
||||
// as some apps (look at you electron!) rely on the bus name and
|
||||
// match rules to handle the requests.
|
||||
// We still check they are registered in the bus though.
|
||||
busName = service;
|
||||
objPath = DEFAULT_ITEM_OBJECT_PATH;
|
||||
} else if (service.match(DBusUtils.BUS_ADDRESS_REGEX)) {
|
||||
try {
|
||||
busName = await DBusUtils.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, service} = indicator;
|
||||
this._items.delete(uniqueId);
|
||||
|
||||
try {
|
||||
this._dbusImpl.emit_signal('StatusNotifierItemUnregistered',
|
||||
GLib.Variant.new('(s)', [service]));
|
||||
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.service);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
delete this._dbusImpl;
|
||||
|
||||
delete this._items;
|
||||
this._isDestroyed = true;
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env gjs -m
|
||||
|
||||
import GLib from 'gi://GLib';
|
||||
import Gio from 'gi://Gio';
|
||||
import GioUnix from 'gi://GioUnix';
|
||||
|
||||
import * as DBusUtils from '../dbusUtils.js';
|
||||
|
||||
async function 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...
|
||||
const cancellable = null;
|
||||
const bus = Gio.DBus.session;
|
||||
const uniqueNames = await DBusUtils.getBusNames(bus, cancellable);
|
||||
|
||||
const stdOutOutputStream = new GioUnix.OutputStream({fd: 1, closeFd: true});
|
||||
const introspectName = async name => {
|
||||
const nodes = DBusUtils.introspectBusObject(bus, name, cancellable,
|
||||
['org.kde.StatusNotifierItem']);
|
||||
const services = [...uniqueNames.get(name)];
|
||||
|
||||
for await (const node of nodes) {
|
||||
const {path} = node;
|
||||
stdOutOutputStream.write(`${JSON.stringify({services, name, path})}\n`,
|
||||
cancellable);
|
||||
}
|
||||
};
|
||||
await Promise.allSettled([...uniqueNames.keys()].map(n => introspectName(n)));
|
||||
}
|
||||
|
||||
function main(_argv) {
|
||||
const loop = new GLib.MainLoop(null, false);
|
||||
|
||||
let exitCode = 0;
|
||||
seekStatusNotifierItems().catch(e => {
|
||||
logError(e);
|
||||
exitCode = 1;
|
||||
}).finally(() => loop.quit());
|
||||
loop.run();
|
||||
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
imports.system.exit(main(ARGV));
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
// 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();
|
||||
}
|
||||
|
||||
_getPanelBgColor() {
|
||||
return Main.panel?.get_parent()
|
||||
? Main.panel.get_theme_node()?.get_background_color() : null;
|
||||
}
|
||||
|
||||
_enable() {
|
||||
if (this._tray)
|
||||
return;
|
||||
|
||||
this._tray = new Shell.TrayManager({bgColor: this._getPanelBgColor()});
|
||||
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());
|
||||
this._tray.unmanage_screen();
|
||||
this._tray = null;
|
||||
}
|
||||
|
||||
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}): ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.emit('destroy');
|
||||
SettingsManager.getDefaultGSettings().disconnect(this._changedId);
|
||||
this._disable();
|
||||
trayIconsManager = null;
|
||||
}
|
||||
}
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
// 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';
|
||||
|
||||
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 {Logger} from './logger.js';
|
||||
import {BaseStatusIcon} from './indicatorStatusIcon.js';
|
||||
import {BUS_ADDRESS_REGEX} from './dbusUtils.js';
|
||||
|
||||
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 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];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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', () => { ... })
|
||||
*
|
||||
* @param {...*} args Signal connection arguments.
|
||||
* @returns {number[]} Signal and destroy handler connection identifiers.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param {Gio.Cancellable} cancellable Cancellation object for the wait.
|
||||
* @returns {Promise<void>} A promise that resolves after startup completes.
|
||||
*/
|
||||
export async function waitForStartupCompletion(cancellable) {
|
||||
if (Main.layoutManager._startingUp)
|
||||
await Main.layoutManager.connect_once('startup-complete', cancellable);
|
||||
}
|
||||
|
||||
export {Logger};
|
||||
|
||||
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 function addActor(obj, actor) {
|
||||
if (obj.add_actor)
|
||||
obj.add_actor(actor);
|
||||
else
|
||||
obj.add_child(actor);
|
||||
}
|
||||
|
||||
export function removeActor(obj, actor) {
|
||||
if (obj.remove_actor)
|
||||
obj.remove_actor(actor);
|
||||
else
|
||||
obj.remove_child(actor);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
+339
@@ -0,0 +1,339 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
# Dash to Dock
|
||||

|
||||
|
||||
## A dock for the GNOME Shell
|
||||
This extension enhances the dash moving it out of the overview and transforming it in a dock for an easier launching of applications and a faster switching between windows and desktops without having to leave the desktop view.
|
||||
|
||||
[<img src="https://micheleg.github.io/dash-to-dock/media/get-it-on-ego.png" height="100">](https://extensions.gnome.org/extension/307/dash-to-dock)
|
||||
|
||||
For additional installation instructions and more information visit [https://micheleg.github.io/dash-to-dock/](https://micheleg.github.io/dash-to-dock/).
|
||||
|
||||
## Installation from source
|
||||
|
||||
The extension can be installed directly from source, either for the convenience of using git or to test the latest development version. Clone the desired branch with git
|
||||
|
||||
### Build Dependencies
|
||||
|
||||
To compile the stylesheet you'll need an implementation of SASS. Dash to Dock supports `dart-sass` (`sass`), `sassc`, and `ruby-sass`. Every distro should have at least one of these implementations, we recommend using `dart-sass` (`sass`) or `sassc` over `ruby-sass` as `ruby-sass` is deprecated.
|
||||
|
||||
By default, Dash to Dock will attempt to build with `sassc`. To change this behavior set the `SASS` environment variable to either `dart` or `ruby`.
|
||||
|
||||
```bash
|
||||
export SASS=dart
|
||||
# or...
|
||||
export SASS=ruby
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
Clone the repository or download the branch from github. A simple Makefile is included.
|
||||
|
||||
Next use `make` to install the extension into your home directory. A Shell reload is required <kbd>Alt</kbd> + <kbd>F2</kbd> <kbd>r</kbd> <kbd>Enter</kbd> under Xorg or under Wayland you may have to logout and login. The extension has to be enabled with *gnome-extensions-app* (GNOME Extensions) or with *dconf*.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/micheleg/dash-to-dock.git
|
||||
make -C dash-to-dock install
|
||||
```
|
||||
|
||||
If `msgfmt` is not available on your system, you will see an error message like the following:
|
||||
|
||||
```bash
|
||||
make: msgfmt: No such file or directory
|
||||
```
|
||||
|
||||
In this case install the `gettext` package from your distribution's repository.
|
||||
|
||||
|
||||
## Bug Reporting
|
||||
|
||||
Bugs should be reported to the Github bug tracker [https://github.com/micheleg/dash-to-dock/issues](https://github.com/micheleg/dash-to-dock/issues).
|
||||
|
||||
## License
|
||||
Dash to Dock Gnome Shell extension is distributed under the terms of the GNU General Public License,
|
||||
version 2 or later. See the COPYING file for details.
|
||||
+3114
File diff suppressed because it is too large.
Load diff
+1334
File diff suppressed because it is too large.
Load diff
+1666
File diff suppressed because it is too large.
Load diff
+183
@@ -0,0 +1,183 @@
|
||||
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
||||
|
||||
|
||||
import {
|
||||
AppIconIndicators,
|
||||
AppIcons,
|
||||
Docking,
|
||||
Utils,
|
||||
} from './imports.js';
|
||||
|
||||
import {
|
||||
AppMenu,
|
||||
AppDisplay,
|
||||
Main,
|
||||
PopupMenu,
|
||||
} from './dependencies/shell/ui.js';
|
||||
|
||||
const Labels = Object.freeze({
|
||||
RESULTS: Symbol('results'),
|
||||
ICONS: Symbol('icons'),
|
||||
});
|
||||
|
||||
export class AppIconsDecorator {
|
||||
constructor() {
|
||||
this._signals = new Utils.GlobalSignalsHandler();
|
||||
this._methodInjections = new Utils.InjectionsHandler();
|
||||
this._propertyInjections = new Utils.PropertyInjectionsHandler(
|
||||
null, {allowNewProperty: true});
|
||||
this._indicators = new Set();
|
||||
this._resultIndicators = new Set();
|
||||
this._updatingIcons = new WeakSet();
|
||||
|
||||
this._patchAppIcons();
|
||||
this._decorateIcons();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._signals?.destroy();
|
||||
delete this._signals;
|
||||
this._methodInjections?.destroy();
|
||||
delete this._methodInjections;
|
||||
this._propertyInjections?.destroy();
|
||||
delete this._propertyInjections;
|
||||
this._clearIndicators(Labels.ICONS);
|
||||
this._clearIndicators(Labels.RESULTS);
|
||||
delete this._indicators;
|
||||
delete this._resultIndicators;
|
||||
delete this._updatingIcons;
|
||||
}
|
||||
|
||||
_indicatorsSet(label) {
|
||||
return {
|
||||
[Labels.ICONS]: this._indicators,
|
||||
[Labels.RESULTS]: this._resultIndicators,
|
||||
}[label];
|
||||
}
|
||||
|
||||
_clearIndicators(label) {
|
||||
const indicatorsSet = this._indicatorsSet(label);
|
||||
indicatorsSet.forEach(i => i.destroy());
|
||||
indicatorsSet.clear();
|
||||
}
|
||||
|
||||
_decorateIcon(parentIcon, signalLabel) {
|
||||
const indicator = new AppIconIndicators.UnityIndicator(parentIcon);
|
||||
const indicatorsSet = this._indicatorsSet(signalLabel);
|
||||
indicatorsSet.add(indicator);
|
||||
this._signals.addWithLabel(signalLabel, parentIcon, 'destroy', () => {
|
||||
indicatorsSet.delete(indicator);
|
||||
indicator.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
_decorateIcons() {
|
||||
const {appDisplay} = Docking.DockManager.getDefault().overviewControls;
|
||||
|
||||
const decorateAppIcons = () => {
|
||||
this._signals.removeWithLabel(Labels.ICONS);
|
||||
this._clearIndicators(Labels.ICONS);
|
||||
|
||||
const decorateViewIcons = view => {
|
||||
const items = view.getAllItems();
|
||||
items.forEach(i => {
|
||||
if (i instanceof AppDisplay.AppIcon) {
|
||||
this._decorateIcon(i, Labels.ICONS);
|
||||
} else if (i instanceof AppDisplay.FolderIcon) {
|
||||
decorateViewIcons(i.view);
|
||||
this._signals.addWithLabel(Labels.ICONS, i.view,
|
||||
'view-loaded', () => decorateAppIcons());
|
||||
}
|
||||
});
|
||||
};
|
||||
decorateViewIcons(appDisplay);
|
||||
};
|
||||
|
||||
this._signals.add(appDisplay, 'view-loaded', () => decorateAppIcons());
|
||||
decorateAppIcons();
|
||||
}
|
||||
|
||||
_patchAppIcons() {
|
||||
const self = this;
|
||||
|
||||
this._methodInjections.add(AppDisplay.AppSearchProvider.prototype,
|
||||
'createResultObject', function (originalFunction, ...args) {
|
||||
/* eslint-disable no-invalid-this */
|
||||
const result = originalFunction.call(this, ...args);
|
||||
if (result instanceof AppDisplay.AppIcon)
|
||||
self._decorateIcon(result, Labels.RESULTS);
|
||||
return result;
|
||||
/* eslint-enable no-invalid-this */
|
||||
});
|
||||
|
||||
this._methodInjections.add(AppDisplay.AppIcon.prototype,
|
||||
'activate', function (originalFunction, ...args) {
|
||||
/* eslint-disable no-invalid-this */
|
||||
if (this.updating) {
|
||||
const {notifyAppIconUpdating} = AppIcons.DockAbstractAppIcon.prototype;
|
||||
notifyAppIconUpdating.call(this,
|
||||
Main.layoutManager.primaryMonitor.index);
|
||||
return;
|
||||
}
|
||||
|
||||
originalFunction.call(this, ...args);
|
||||
/* eslint-enable no-invalid-this */
|
||||
});
|
||||
|
||||
const appIconsTypes = [
|
||||
AppDisplay.AppSearchProvider,
|
||||
AppDisplay.AppIcon,
|
||||
];
|
||||
appIconsTypes.forEach(type =>
|
||||
this._propertyInjections.add(type.prototype, 'updating', {
|
||||
get() {
|
||||
return self._updatingIcons.has(this);
|
||||
},
|
||||
set(updating) {
|
||||
if (this.updating === updating)
|
||||
return;
|
||||
|
||||
if (updating) {
|
||||
self._updatingIcons.add(this);
|
||||
this.add_style_class_name('updating');
|
||||
} else {
|
||||
self._updatingIcons.delete(this);
|
||||
this.remove_style_class_name('updating');
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
this._methodInjections.add(AppMenu.AppMenu.prototype,
|
||||
'open', function (originalFunction, ...args) {
|
||||
/* eslint-disable no-invalid-this */
|
||||
if (!this.sourceActor.updating) {
|
||||
originalFunction.call(this, ...args);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isOpen)
|
||||
return;
|
||||
|
||||
if (this.isEmpty())
|
||||
return;
|
||||
|
||||
// Temporarily hide all the menu items a part the Pinning and
|
||||
// the details one while we're updating.
|
||||
const validItems = [
|
||||
this._toggleFavoriteItem,
|
||||
this._detailsItem,
|
||||
];
|
||||
const items = this._getMenuItems().filter(
|
||||
i => !validItems.includes(i)).map(i =>
|
||||
i instanceof PopupMenu.PopupMenuBase ? i.actor : i);
|
||||
const itemsVisibility = items.map(i => i.visible);
|
||||
items.forEach(i => (i.visible = false));
|
||||
const menuClosedId = this.connect('menu-closed', () => {
|
||||
this.disconnect(menuClosedId);
|
||||
items.forEach((i, idx) => (i.visible = itemsVisibility[idx]));
|
||||
});
|
||||
originalFunction.call(this, ...args);
|
||||
/* eslint-enable no-invalid-this */
|
||||
});
|
||||
}
|
||||
}
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
import {Atk, Clutter} from './dependencies/gi.js';
|
||||
|
||||
import {
|
||||
Main,
|
||||
SearchController,
|
||||
Workspace,
|
||||
WorkspaceThumbnail,
|
||||
} from './dependencies/shell/ui.js';
|
||||
|
||||
import {Utils} from './imports.js';
|
||||
|
||||
const APP_SPREAD_RESTORE_ACTION = 'dock-app-spread-restore';
|
||||
|
||||
export class AppSpread {
|
||||
constructor() {
|
||||
this.app = null;
|
||||
this.supported = true;
|
||||
this.windows = [];
|
||||
|
||||
// fail early and do nothing, if mandatory gnome shell functions are missing
|
||||
if (Main.overview.isDummy ||
|
||||
!Workspace?.Workspace?.prototype._isOverviewWindow ||
|
||||
!WorkspaceThumbnail?.WorkspaceThumbnail?.prototype._isOverviewWindow) {
|
||||
log('Dash to dock: Unable to temporarily replace shell functions ' +
|
||||
'for app spread - using previews instead');
|
||||
this.supported = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this._signalHandlers = new Utils.GlobalSignalsHandler();
|
||||
this._methodInjections = new Utils.InjectionsHandler();
|
||||
this._vfuncInjections = new Utils.VFuncInjectionsHandler();
|
||||
}
|
||||
|
||||
get isInAppSpread() {
|
||||
return !!this.app;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (!this.supported)
|
||||
return;
|
||||
this._hideAppSpread();
|
||||
this._signalHandlers.destroy();
|
||||
this._methodInjections.destroy();
|
||||
this._vfuncInjections.destroy();
|
||||
}
|
||||
|
||||
toggle(app) {
|
||||
const newApp = this.app !== app;
|
||||
if (this.app)
|
||||
Main.overview.hide(); // also triggers hook 'hidden'
|
||||
|
||||
if (app && newApp)
|
||||
this._showAppSpread(app);
|
||||
}
|
||||
|
||||
_updateWindows() {
|
||||
this.windows = this.app.get_windows();
|
||||
}
|
||||
|
||||
_restoreDefaultWindows() {
|
||||
const {workspaceManager} = global;
|
||||
|
||||
for (let i = 0; i < workspaceManager.nWorkspaces; i++) {
|
||||
const metaWorkspace = workspaceManager.get_workspace_by_index(i);
|
||||
metaWorkspace.list_windows().forEach(w => metaWorkspace.emit('window-added', w));
|
||||
}
|
||||
}
|
||||
|
||||
_filterWindows() {
|
||||
const {workspaceManager} = global;
|
||||
|
||||
for (let i = 0; i < workspaceManager.nWorkspaces; i++) {
|
||||
const metaWorkspace = workspaceManager.get_workspace_by_index(i);
|
||||
metaWorkspace.list_windows().filter(w => !this.windows.includes(w)).forEach(
|
||||
w => metaWorkspace.emit('window-removed', w));
|
||||
}
|
||||
}
|
||||
|
||||
_restoreDefaultOverview() {
|
||||
this._hideAppSpread();
|
||||
this._restoreDefaultWindows();
|
||||
}
|
||||
|
||||
_showAppSpread(app) {
|
||||
if (this.isInAppSpread)
|
||||
return;
|
||||
|
||||
// Checked in overview "hide" event handler _hideAppSpread
|
||||
this.app = app;
|
||||
this._updateWindows();
|
||||
|
||||
// we need to hook into overview 'hidden' like this, in case app spread
|
||||
// overview is hidden by choosing another app it should then do its
|
||||
// cleanup too
|
||||
this._signalHandlers.add(Main.overview, 'hidden', () => this._hideAppSpread());
|
||||
|
||||
const appSpread = this;
|
||||
this._methodInjections.add([
|
||||
// Filter workspaces to only show current app windows
|
||||
Workspace.Workspace.prototype, '_isOverviewWindow',
|
||||
function (originalMethod, window) {
|
||||
/* eslint-disable no-invalid-this */
|
||||
const isOverviewWindow = originalMethod.call(this, window);
|
||||
return isOverviewWindow && appSpread.windows.includes(window);
|
||||
/* eslint-enable no-invalid-this */
|
||||
},
|
||||
],
|
||||
[
|
||||
// Filter thumbnails to only show current app windows
|
||||
WorkspaceThumbnail.WorkspaceThumbnail.prototype, '_isOverviewWindow',
|
||||
function (originalMethod, windowActor) {
|
||||
/* eslint-disable no-invalid-this */
|
||||
const isOverviewWindow = originalMethod.call(this, windowActor);
|
||||
return isOverviewWindow && appSpread.windows.includes(windowActor.metaWindow);
|
||||
/* eslint-enable no-invalid-this */
|
||||
},
|
||||
]);
|
||||
|
||||
const activitiesButton = Main.panel.statusArea?.activities;
|
||||
|
||||
if (activitiesButton) {
|
||||
this._signalHandlers.add(Main.overview, 'showing', () => {
|
||||
activitiesButton.remove_style_pseudo_class('overview');
|
||||
activitiesButton.remove_accessible_state(Atk.StateType.CHECKED);
|
||||
});
|
||||
|
||||
let hasEventVFunc = false;
|
||||
try {
|
||||
hasEventVFunc = !!activitiesButton.constructor.prototype.vfunc_event;
|
||||
} catch {}
|
||||
|
||||
if (hasEventVFunc) {
|
||||
this._vfuncInjections.add([
|
||||
activitiesButton.constructor.prototype,
|
||||
'event',
|
||||
function (event) {
|
||||
if (event.type() === Clutter.EventType.TOUCH_END ||
|
||||
event.type() === Clutter.EventType.BUTTON_RELEASE) {
|
||||
if (Main.overview.shouldToggleByCornerOrButton())
|
||||
appSpread._restoreDefaultOverview();
|
||||
}
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
this._activitiesClickGesture = activitiesButton._clickGesture;
|
||||
this._activitiesClickGesture.set_enabled(false);
|
||||
|
||||
const click = new Clutter.ClickGesture();
|
||||
click.connect('recognize', () => {
|
||||
if (this.isInAppSpread &&
|
||||
Main.overview.shouldToggleByCornerOrButton())
|
||||
this.toggle(this.app);
|
||||
});
|
||||
activitiesButton.add_action_with_name(APP_SPREAD_RESTORE_ACTION, click);
|
||||
}
|
||||
|
||||
let hasKeyReleaseVFunc = false;
|
||||
try {
|
||||
hasKeyReleaseVFunc =
|
||||
!!activitiesButton.constructor.prototype.vfunc_key_release_event;
|
||||
} catch {}
|
||||
|
||||
if (hasKeyReleaseVFunc) {
|
||||
this._vfuncInjections.add([
|
||||
activitiesButton.constructor.prototype,
|
||||
'key_release_event',
|
||||
function (keyEvent) {
|
||||
const keyval = keyEvent.get_key_symbol?.() ?? keyEvent.keyval;
|
||||
if (keyval === Clutter.KEY_Return || keyval === Clutter.KEY_space) {
|
||||
if (Main.overview.shouldToggleByCornerOrButton())
|
||||
appSpread._restoreDefaultOverview();
|
||||
}
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
this._methodInjections.add([
|
||||
activitiesButton.constructor.prototype,
|
||||
'_toggleAction',
|
||||
function (originalMethod, ...args) {
|
||||
/* eslint-disable no-invalid-this */
|
||||
if (appSpread.isInAppSpread &&
|
||||
Main.overview.shouldToggleByCornerOrButton()) {
|
||||
appSpread.toggle(appSpread.app);
|
||||
return Clutter.EVENT_STOP;
|
||||
}
|
||||
|
||||
return originalMethod.call(this, ...args);
|
||||
/* eslint-enable no-invalid-this */
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
this._signalHandlers.add(Main.overview.dash.showAppsButton, 'notify::checked', () => {
|
||||
if (Main.overview.dash.showAppsButton.checked)
|
||||
this._restoreDefaultOverview();
|
||||
});
|
||||
|
||||
// If closing windows in AppSpread, and only one window left:
|
||||
// exit app spread and focus remaining window (handled in _hideAppSpread)
|
||||
this._signalHandlers.add(this.app, 'windows-changed', () => {
|
||||
this._updateWindows();
|
||||
|
||||
if (this.windows.length <= 1)
|
||||
Main.overview.hide();
|
||||
});
|
||||
|
||||
this._disableSearch();
|
||||
|
||||
Main.overview.show();
|
||||
}
|
||||
|
||||
_hideAppSpread() {
|
||||
if (!this.isInAppSpread)
|
||||
return;
|
||||
|
||||
if (Main.overview.visible) {
|
||||
Main.panel.statusArea?.activities.add_style_pseudo_class('overview');
|
||||
Main.panel.statusArea?.activities.add_accessible_state(Atk.StateType.CHECKED);
|
||||
}
|
||||
|
||||
// Restore original behaviour
|
||||
this.app = null;
|
||||
this._enableSearch();
|
||||
this._methodInjections.clear();
|
||||
this._signalHandlers.clear();
|
||||
this._vfuncInjections.clear();
|
||||
Main.panel.statusArea?.activities.remove_action_by_name(APP_SPREAD_RESTORE_ACTION);
|
||||
this._activitiesClickGesture.set_enabled(true);
|
||||
this._activitiesClickGesture = null;
|
||||
|
||||
// Check reason for leaving AppSpread was closing app windows and only one window left...
|
||||
if (this.windows.length === 1)
|
||||
Main.activateWindow(this.windows[0]);
|
||||
|
||||
this.windows = [];
|
||||
}
|
||||
|
||||
_disableSearch() {
|
||||
if (Main.overview.searchEntry) {
|
||||
Main.overview.searchEntry.opacity = 0;
|
||||
Main.overview.searchEntry.reactive = false;
|
||||
}
|
||||
|
||||
if (!SearchController.SearchController.prototype._shouldTriggerSearch)
|
||||
return;
|
||||
|
||||
this._methodInjections.add(
|
||||
SearchController.SearchController.prototype,
|
||||
'_shouldTriggerSearch', () => false);
|
||||
}
|
||||
|
||||
_enableSearch() {
|
||||
if (Main.overview.searchEntry) {
|
||||
Main.overview.searchEntry.opacity = 255;
|
||||
Main.overview.searchEntry.reactive = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1226
File diff suppressed because it is too large.
Load diff
+280
@@ -0,0 +1,280 @@
|
||||
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
||||
|
||||
import {
|
||||
Atk,
|
||||
Clutter,
|
||||
Gio,
|
||||
GLib,
|
||||
St,
|
||||
} from './dependencies/gi.js';
|
||||
|
||||
import {PopupMenu} from './dependencies/shell/ui.js';
|
||||
|
||||
import {Utils} from './imports.js';
|
||||
|
||||
// Dbusmenu features not (yet) supported:
|
||||
//
|
||||
// * The CHILD_DISPLAY property
|
||||
//
|
||||
// This seems to have only one possible value in the Dbusmenu API, so
|
||||
// there's little point in depending on it--the code in libdbusmenu sets it
|
||||
// if and only if an item has children, so for our purposes it's simpler
|
||||
// and more intuitive to just check children.length. (This does ignore the
|
||||
// possibility of a program not using libdbusmenu and setting CHILD_DISPLAY
|
||||
// independently, perhaps to indicate that an childless menu item should
|
||||
// nevertheless be displayed like a submenu.)
|
||||
//
|
||||
// * Children more than two levels deep
|
||||
//
|
||||
// PopupMenu doesn't seem to support submenus in submenus.
|
||||
//
|
||||
// * Shortcut keys
|
||||
//
|
||||
// If these keys are supposed to be installed as global shortcuts, we'd
|
||||
// have to query these aggressively and not wait for the DBus menu to be
|
||||
// mapped to a popup menu. A shortcut key that only works once the popup
|
||||
// menu is open and has key focus is possibly of marginal value.
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export async function haveDBusMenu() {
|
||||
try {
|
||||
const {default: DBusMenu} = await import('gi://Dbusmenu');
|
||||
return DBusMenu;
|
||||
} catch (e) {
|
||||
log(`Failed to import DBusMenu, quicklists are not available: ${e}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const DBusMenu = await haveDBusMenu();
|
||||
|
||||
/**
|
||||
* @param dbusmenuItem
|
||||
* @param deep
|
||||
*/
|
||||
export function makePopupMenuItem(dbusmenuItem, deep) {
|
||||
// These are the only properties guaranteed to be available when the root
|
||||
// item is first announced. Other properties might be loaded already, but
|
||||
// be sure to connect to Dbusmenu.MENUITEM_SIGNAL_PROPERTY_CHANGED to get
|
||||
// the most up-to-date values in case they aren't.
|
||||
const itemType = dbusmenuItem.property_get(DBusMenu.MENUITEM_PROP_TYPE);
|
||||
const label = dbusmenuItem.property_get(DBusMenu.MENUITEM_PROP_LABEL);
|
||||
const visible = dbusmenuItem.property_get_bool(DBusMenu.MENUITEM_PROP_VISIBLE);
|
||||
const enabled = dbusmenuItem.property_get_bool(DBusMenu.MENUITEM_PROP_ENABLED);
|
||||
const accessibleDesc = dbusmenuItem.property_get(DBusMenu.MENUITEM_PROP_ACCESSIBLE_DESC);
|
||||
// const childDisplay = dbusmenuItem.property_get(Dbusmenu.MENUITEM_PROP_CHILD_DISPLAY);
|
||||
|
||||
let item;
|
||||
const signalsHandler = new Utils.GlobalSignalsHandler();
|
||||
const wantIcon = itemType === DBusMenu.CLIENT_TYPES_IMAGE;
|
||||
|
||||
// If the basic type of the menu item needs to change, call this.
|
||||
const recreateItem = () => {
|
||||
const newItem = makePopupMenuItem(dbusmenuItem, deep);
|
||||
const parentMenu = item._parent;
|
||||
parentMenu.addMenuItem(newItem);
|
||||
// Reminder: Clutter thinks of later entries in the child list as
|
||||
// "above" earlier ones, so "above" here means "below" in terms of the
|
||||
// menu's vertical order.
|
||||
parentMenu.actor.set_child_above_sibling(newItem.actor, item.actor);
|
||||
if (newItem.menu)
|
||||
parentMenu.actor.set_child_above_sibling(newItem.menu.actor, newItem.actor);
|
||||
|
||||
parentMenu.actor.remove_child(item.actor);
|
||||
item.destroy();
|
||||
item = null;
|
||||
};
|
||||
|
||||
const updateDisposition = () => {
|
||||
const disposition = dbusmenuItem.property_get(DBusMenu.MENUITEM_PROP_DISPOSITION);
|
||||
let iconName = null;
|
||||
switch (disposition) {
|
||||
case DBusMenu.MENUITEM_DISPOSITION_ALERT:
|
||||
case DBusMenu.MENUITEM_DISPOSITION_WARNING:
|
||||
iconName = 'dialog-warning-symbolic';
|
||||
break;
|
||||
case DBusMenu.MENUITEM_DISPOSITION_INFORMATIVE:
|
||||
iconName = 'dialog-information-symbolic';
|
||||
break;
|
||||
}
|
||||
if (iconName) {
|
||||
item._dispositionIcon = new St.Icon({
|
||||
icon_name: iconName,
|
||||
style_class: 'popup-menu-icon',
|
||||
y_align: Clutter.ActorAlign.CENTER,
|
||||
y_expand: true,
|
||||
});
|
||||
let expander;
|
||||
for (let child = item.label.get_next_sibling(); ; child = child.get_next_sibling()) {
|
||||
if (!child) {
|
||||
expander = new St.Bin({
|
||||
style_class: 'popup-menu-item-expander',
|
||||
x_expand: true,
|
||||
});
|
||||
item.actor.add_child(expander);
|
||||
break;
|
||||
} else if (child instanceof St.Widget &&
|
||||
child.has_style_class_name('popup-menu-item-expander')) {
|
||||
expander = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
item.actor.insert_child_above(item._dispositionIcon, expander);
|
||||
} else if (item._dispositionIcon) {
|
||||
item.actor.remove_child(item._dispositionIcon);
|
||||
item._dispositionIcon = null;
|
||||
}
|
||||
};
|
||||
|
||||
const updateIcon = () => {
|
||||
if (!wantIcon)
|
||||
return;
|
||||
|
||||
const iconData = dbusmenuItem.property_get_byte_array(DBusMenu.MENUITEM_PROP_ICON_DATA);
|
||||
const iconName = dbusmenuItem.property_get(DBusMenu.MENUITEM_PROP_ICON_NAME);
|
||||
if (iconName)
|
||||
item.icon.icon_name = iconName;
|
||||
else if (iconData.length)
|
||||
item.icon.gicon = Gio.BytesIcon.new(iconData);
|
||||
};
|
||||
|
||||
const updateOrnament = () => {
|
||||
const toggleType = dbusmenuItem.property_get(DBusMenu.MENUITEM_PROP_TOGGLE_TYPE);
|
||||
switch (toggleType) {
|
||||
case DBusMenu.MENUITEM_TOGGLE_CHECK:
|
||||
item.actor.accessible_role = Atk.Role.CHECK_MENU_ITEM;
|
||||
break;
|
||||
case DBusMenu.MENUITEM_TOGGLE_RADIO:
|
||||
item.actor.accessible_role = Atk.Role.RADIO_MENU_ITEM;
|
||||
break;
|
||||
default:
|
||||
item.actor.accessible_role = Atk.Role.MENU_ITEM;
|
||||
}
|
||||
let ornament = PopupMenu.Ornament.NONE;
|
||||
const state = dbusmenuItem.property_get_int(DBusMenu.MENUITEM_PROP_TOGGLE_STATE);
|
||||
if (state === DBusMenu.MENUITEM_TOGGLE_STATE_UNKNOWN) {
|
||||
// PopupMenu doesn't natively support an "unknown" ornament, but we
|
||||
// can hack one in:
|
||||
item.setOrnament(ornament);
|
||||
item.actor.add_accessible_state(Atk.StateType.INDETERMINATE);
|
||||
item._ornamentLabel.text = '\u2501';
|
||||
item.actor.remove_style_pseudo_class('checked');
|
||||
} else {
|
||||
item.actor.remove_accessible_state(Atk.StateType.INDETERMINATE);
|
||||
if (state === DBusMenu.MENUITEM_TOGGLE_STATE_CHECKED) {
|
||||
if (toggleType === DBusMenu.MENUITEM_TOGGLE_CHECK)
|
||||
ornament = PopupMenu.Ornament.CHECK;
|
||||
else if (toggleType === DBusMenu.MENUITEM_TOGGLE_RADIO)
|
||||
ornament = PopupMenu.Ornament.DOT;
|
||||
|
||||
item.actor.add_style_pseudo_class('checked');
|
||||
} else {
|
||||
item.actor.remove_style_pseudo_class('checked');
|
||||
}
|
||||
item.setOrnament(ornament);
|
||||
}
|
||||
};
|
||||
|
||||
const onPropertyChanged = (_, name, value) => {
|
||||
// `value` is null when a property is cleared, so handle those cases
|
||||
// with sensible defaults.
|
||||
switch (name) {
|
||||
case DBusMenu.MENUITEM_PROP_TYPE:
|
||||
recreateItem();
|
||||
break;
|
||||
case DBusMenu.MENUITEM_PROP_ENABLED:
|
||||
item.setSensitive(value ? value.unpack() : false);
|
||||
break;
|
||||
case DBusMenu.MENUITEM_PROP_LABEL:
|
||||
item.label.text = value ? value.unpack() : '';
|
||||
break;
|
||||
case DBusMenu.MENUITEM_PROP_VISIBLE:
|
||||
item.actor.visible = value ? value.unpack() : false;
|
||||
break;
|
||||
case DBusMenu.MENUITEM_PROP_DISPOSITION:
|
||||
updateDisposition();
|
||||
break;
|
||||
case DBusMenu.MENUITEM_PROP_ACCESSIBLE_DESC:
|
||||
item.actor.get_accessible().accessible_description = value && value.unpack() || '';
|
||||
break;
|
||||
case DBusMenu.MENUITEM_PROP_ICON_DATA:
|
||||
case DBusMenu.MENUITEM_PROP_ICON_NAME:
|
||||
updateIcon();
|
||||
break;
|
||||
case DBusMenu.MENUITEM_PROP_TOGGLE_TYPE:
|
||||
case DBusMenu.MENUITEM_PROP_TOGGLE_STATE:
|
||||
updateOrnament();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Start actually building the menu item.
|
||||
const children = dbusmenuItem.get_children();
|
||||
if (children.length && !deep) {
|
||||
// Make a submenu.
|
||||
item = new PopupMenu.PopupSubMenuMenuItem(label, wantIcon);
|
||||
const updateChildren = () => {
|
||||
const itemChildren = dbusmenuItem.get_children();
|
||||
if (!itemChildren.length) {
|
||||
recreateItem();
|
||||
return;
|
||||
}
|
||||
|
||||
item.menu.removeAll();
|
||||
itemChildren.forEach(remoteChild =>
|
||||
item.menu.addMenuItem(makePopupMenuItem(remoteChild, true)));
|
||||
};
|
||||
updateChildren();
|
||||
signalsHandler.add(
|
||||
[dbusmenuItem, DBusMenu.MENUITEM_SIGNAL_CHILD_ADDED, updateChildren],
|
||||
[dbusmenuItem, DBusMenu.MENUITEM_SIGNAL_CHILD_MOVED, updateChildren],
|
||||
[dbusmenuItem, DBusMenu.MENUITEM_SIGNAL_CHILD_REMOVED, updateChildren]);
|
||||
} else {
|
||||
// Don't make a submenu.
|
||||
if (!deep) {
|
||||
// We only have the potential to get a submenu if we aren't deep.
|
||||
signalsHandler.add(
|
||||
[dbusmenuItem, DBusMenu.MENUITEM_SIGNAL_CHILD_ADDED, recreateItem],
|
||||
[dbusmenuItem, DBusMenu.MENUITEM_SIGNAL_CHILD_MOVED, recreateItem],
|
||||
[dbusmenuItem, DBusMenu.MENUITEM_SIGNAL_CHILD_REMOVED, recreateItem]);
|
||||
}
|
||||
|
||||
if (itemType === DBusMenu.CLIENT_TYPES_SEPARATOR) {
|
||||
item = new PopupMenu.PopupSeparatorMenuItem();
|
||||
} else if (wantIcon) {
|
||||
item = new PopupMenu.PopupImageMenuItem(label, null);
|
||||
item.icon = item._icon;
|
||||
} else {
|
||||
item = new PopupMenu.PopupMenuItem(label);
|
||||
}
|
||||
}
|
||||
|
||||
// Set common initial properties.
|
||||
item.actor.visible = visible;
|
||||
item.setSensitive(enabled);
|
||||
if (accessibleDesc)
|
||||
item.actor.get_accessible().accessible_description = accessibleDesc;
|
||||
|
||||
updateDisposition();
|
||||
updateIcon();
|
||||
updateOrnament();
|
||||
|
||||
// Prevent an initial resize flicker.
|
||||
if (wantIcon)
|
||||
item.icon.icon_size = 16;
|
||||
|
||||
|
||||
signalsHandler.add(dbusmenuItem, DBusMenu.MENUITEM_SIGNAL_PROPERTY_CHANGED, onPropertyChanged);
|
||||
|
||||
// Connections on item will be lost when item is disposed; there's no need
|
||||
// to add them to signalsHandler.
|
||||
item.connect('activate', () =>
|
||||
dbusmenuItem.handle_event(DBusMenu.MENUITEM_EVENT_ACTIVATED,
|
||||
new GLib.Variant('i', 0), Math.floor(Date.now() / 1000)));
|
||||
item.connect('destroy', () => signalsHandler.destroy());
|
||||
|
||||
return item;
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
export {default as Atk} from 'gi://Atk';
|
||||
export {default as Clutter} from 'gi://Clutter';
|
||||
export {default as Cogl} from 'gi://Cogl';
|
||||
export {default as GLib} from 'gi://GLib';
|
||||
export {default as GObject} from 'gi://GObject';
|
||||
export {default as GdkPixbuf} from 'gi://GdkPixbuf';
|
||||
export {default as Gio} from 'gi://Gio';
|
||||
export {default as GioUnix} from 'gi://GioUnix';
|
||||
export {default as Meta} from 'gi://Meta';
|
||||
export {default as Mtk} from 'gi://Mtk';
|
||||
export {default as Pango} from 'gi://Pango';
|
||||
export {default as Shell} from 'gi://Shell';
|
||||
export {default as St} from 'gi://St';
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * as Extension from 'resource:///org/gnome/shell/extensions/extension.js';
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export * as AnimationUtils from 'resource:///org/gnome/shell/misc/animationUtils.js';
|
||||
export * as Config from 'resource:///org/gnome/shell/misc/config.js';
|
||||
export * as ExtensionUtils from 'resource:///org/gnome/shell/misc/extensionUtils.js';
|
||||
export * as ParentalControlsManager from 'resource:///org/gnome/shell/misc/parentalControlsManager.js';
|
||||
export * as Util from 'resource:///org/gnome/shell/misc/util.js';
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
export * as AppDisplay from 'resource:///org/gnome/shell/ui/appDisplay.js';
|
||||
export * as AppMenu from 'resource:///org/gnome/shell/ui/appMenu.js';
|
||||
export * as AppFavorites from 'resource:///org/gnome/shell/ui/appFavorites.js';
|
||||
export * as BoxPointer from 'resource:///org/gnome/shell/ui/boxpointer.js';
|
||||
export * as DND from 'resource:///org/gnome/shell/ui/dnd.js';
|
||||
export * as Dash from 'resource:///org/gnome/shell/ui/dash.js';
|
||||
export * as Layout from 'resource:///org/gnome/shell/ui/layout.js';
|
||||
export * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
export * as Overview from 'resource:///org/gnome/shell/ui/overview.js';
|
||||
export * as OverviewControls from 'resource:///org/gnome/shell/ui/overviewControls.js';
|
||||
export * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
|
||||
export * as SearchController from 'resource:///org/gnome/shell/ui/searchController.js';
|
||||
export * as ShellMountOperation from 'resource:///org/gnome/shell/ui/shellMountOperation.js';
|
||||
export * as SwitcherPopup from 'resource:///org/gnome/shell/ui/switcherPopup.js';
|
||||
export * as Workspace from 'resource:///org/gnome/shell/ui/workspace.js';
|
||||
export * as WorkspaceSwitcherPopup from 'resource:///org/gnome/shell/ui/workspaceSwitcherPopup.js';
|
||||
export * as WorkspaceThumbnail from 'resource:///org/gnome/shell/ui/workspaceThumbnail.js';
|
||||
export * as WorkspacesView from 'resource:///org/gnome/shell/ui/workspacesView.js';
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* The code in this file is distributed under a "1-clause BSD license",
|
||||
* which makes it compatible with GPLv2 and GPLv3 too, and others.
|
||||
*
|
||||
* License text:
|
||||
*
|
||||
* Copyright (C) 2021 Sergio Costas (rastersoft@gmail.com)
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Integration class
|
||||
*
|
||||
* This class must be added to other extensions in order to integrate
|
||||
* them with Desktop Icons NG. It allows an extension to notify how much margin
|
||||
* it uses in each side of each monitor.
|
||||
*
|
||||
* DON'T SEND PATCHES TO THIS FILE TO THE EXTENSION MAINTAINER. SEND THEM TO
|
||||
* DESKTOP ICONS NG MAINTAINER: https://gitlab.com/rastersoft/desktop-icons-ng
|
||||
*
|
||||
* In the *enable()* function, create a *DesktopIconsUsableAreaClass()*
|
||||
* object with
|
||||
*
|
||||
* new DesktopIconsIntegration.DesktopIconsUsableAreaClass(object);
|
||||
*
|
||||
* Now, in the *disable()* function just call to the *destroy()* method before
|
||||
* nullifying the pointer. You must create a new object in enable() the next
|
||||
* time the extension is enabled.
|
||||
*
|
||||
* In your code, every time you change the margins, you should call first to
|
||||
* *resetMargins()* method to clear the current margins, and then call to
|
||||
* *setMargins(...)* method as many times as you need to set the margins in each
|
||||
* monitor. You don't need to call it for all the monitors, only for those where
|
||||
* you are painting something. If you don't set values for a monitor, they will
|
||||
* be considered zero.
|
||||
*
|
||||
* The margins values are relative to the monitor border.
|
||||
*
|
||||
*******************************************************************************/
|
||||
|
||||
import GLib from 'gi://GLib';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
import * as ExtensionUtils from 'resource:///org/gnome/shell/misc/extensionUtils.js';
|
||||
|
||||
const IDENTIFIER_UUID = '130cbc66-235c-4bd6-8571-98d2d8bba5e2';
|
||||
|
||||
export class DesktopIconsUsableAreaClass {
|
||||
_checkIfExtensionIsEnabled(extension) {
|
||||
return (extension?.state === ExtensionUtils.ExtensionState.ENABLED) ||
|
||||
(extension?.state === ExtensionUtils.ExtensionState.ACTIVE);
|
||||
}
|
||||
|
||||
constructor(extension) {
|
||||
this._UUID = extension.uuid;
|
||||
this._extensionManager = Main.extensionManager;
|
||||
this._timedMarginsID = 0;
|
||||
this._margins = {};
|
||||
this._emID = this._extensionManager.connect('extension-state-changed', (_obj, ext) => {
|
||||
if (!ext)
|
||||
return;
|
||||
|
||||
// If an extension is being enabled and lacks the
|
||||
// DesktopIconsUsableArea object, we can avoid launching a refresh
|
||||
if (this._checkIfExtensionIsEnabled(ext)) {
|
||||
this._sendMarginsToExtension(ext);
|
||||
return;
|
||||
}
|
||||
// if the extension is being disabled, we must do a full refresh,
|
||||
// because if there were other extensions originally
|
||||
// loaded after that extension, those extensions will be disabled
|
||||
// and enabled again without notification
|
||||
this._changedMargins();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or updates the top, bottom, left and right margins for a
|
||||
* monitor. Values are measured from the monitor border (and NOT from
|
||||
* the workspace border).
|
||||
*
|
||||
* @param {int} monitor Monitor number to which set the margins.
|
||||
* A negative value means "the primary monitor".
|
||||
* @param {int} top Top margin in pixels
|
||||
* @param {int} bottom Bottom margin in pixels
|
||||
* @param {int} left Left margin in pixels
|
||||
* @param {int} right Right margin in pixels
|
||||
*/
|
||||
setMargins(monitor, top, bottom, left, right) {
|
||||
this._margins[monitor] = {
|
||||
top,
|
||||
bottom,
|
||||
left,
|
||||
right,
|
||||
};
|
||||
this._changedMargins();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the current margins. Must be called before configuring the monitors
|
||||
* margins with setMargins().
|
||||
*/
|
||||
resetMargins() {
|
||||
this._margins = {};
|
||||
this._changedMargins();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects all the signals and removes the margins.
|
||||
*/
|
||||
destroy() {
|
||||
if (this._emID) {
|
||||
this._extensionManager.disconnect(this._emID);
|
||||
this._emID = 0;
|
||||
}
|
||||
if (this._timedMarginsID) {
|
||||
GLib.source_remove(this._timedMarginsID);
|
||||
this._timedMarginsID = 0;
|
||||
}
|
||||
this._margins = null;
|
||||
this._changedMargins();
|
||||
}
|
||||
|
||||
_changedMargins() {
|
||||
if (this._timedMarginsID)
|
||||
GLib.source_remove(this._timedMarginsID);
|
||||
|
||||
this._timedMarginsID = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => {
|
||||
this._sendMarginsToAll();
|
||||
this._timedMarginsID = 0;
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
}
|
||||
|
||||
_sendMarginsToAll() {
|
||||
this._extensionManager.getUuids().forEach(uuid =>
|
||||
this._sendMarginsToExtension(this._extensionManager.lookup(uuid)));
|
||||
}
|
||||
|
||||
_sendMarginsToExtension(extension) {
|
||||
// check that the extension is an extension that has the logic to accept
|
||||
// working margins
|
||||
if (!this._checkIfExtensionIsEnabled(extension))
|
||||
return;
|
||||
|
||||
const usableArea = extension?.stateObj?.DesktopIconsUsableArea;
|
||||
if (usableArea?.uuid === IDENTIFIER_UUID) {
|
||||
usableArea.setMarginsForExtension(
|
||||
this._UUID, this._margins);
|
||||
}
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
// Reserved-extent export for dash-to-dock.
|
||||
//
|
||||
// dash-to-dock publishes the space its dock occupies on each monitor so other
|
||||
// extensions can avoid placing their own UI underneath it. The case that
|
||||
// motivates this is a BOTTOM dock in auto-hide/overlay mode: it claims no
|
||||
// mutter strut (so the work area does not shrink for it), yet it slides out
|
||||
// over the bottom of the work area, so a pill or OSD placed at the work
|
||||
// area's bottom edge ends up hidden behind the dock.
|
||||
//
|
||||
// While dash-to-dock is enabled, the read-only `DashToDockStruts` view is
|
||||
// published as `Main.layoutManager.dashToDockStruts` (a property of the
|
||||
// mutable LayoutManager singleton, not of the frozen `Main` module namespace)
|
||||
// and nulled again on disable. Consumers:
|
||||
//
|
||||
// - check for its presence;
|
||||
// - read `monitors`, keyed by monitor index, where each entry is
|
||||
// `{side, x, y, width, height, affectsStruts}`;
|
||||
// - connect to `updated`, which carries the changed monitor index (or -1
|
||||
// for all monitors), and rely on `connectObject()` to auto-disconnect
|
||||
// their handlers on `destroy`, since the type is registered with the
|
||||
// shell's SignalTracker as destroyable.
|
||||
//
|
||||
// `side` is the raw `St.Side` enum, never a string, and `affectsStruts` says
|
||||
// whether the dock claims a real mutter strut (dock-fixed mode). A consumer
|
||||
// should reserve the extent only when `affectsStruts` is false; otherwise the
|
||||
// work area already excludes it.
|
||||
//
|
||||
// The published extent is the dock's full-size rectangle - where it would be
|
||||
// when fully shown - regardless of whether it is currently auto-hidden, so
|
||||
// consumers reserve the space even while the dock is hidden and are never
|
||||
// covered when it slides out. A dock-fixed dock already claims a mutter
|
||||
// strut, so its extent coincides with (or sits inside) the work-area edge;
|
||||
// reserving it is harmless either way.
|
||||
|
||||
import GObject from 'gi://GObject';
|
||||
|
||||
import {registerDestroyableType} from 'resource:///org/gnome/shell/misc/signalTracker.js';
|
||||
|
||||
import {Main} from './dependencies/shell/ui.js';
|
||||
|
||||
/**
|
||||
* Read-only view of the space dash-to-dock reserves on each monitor.
|
||||
*
|
||||
* Published as `Main.layoutManager.dashToDockStruts` while the extension is
|
||||
* enabled. It is backed by a private {@link StrutsManager} owned by the
|
||||
* DockManager; consumers only read `monitors` and connect to `updated` and
|
||||
* `destroy`.
|
||||
*/
|
||||
export const DashToDockStruts = GObject.registerClass({
|
||||
Signals: {
|
||||
'updated': {param_types: [GObject.TYPE_INT]},
|
||||
'destroy': {},
|
||||
},
|
||||
}, class DashToDockStruts extends GObject.Object {
|
||||
#manager;
|
||||
|
||||
constructor(manager) {
|
||||
super();
|
||||
this.#manager = manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* The per-monitor reserved extents, keyed by monitor index.
|
||||
*
|
||||
* Each entry is `{side, x, y, width, height, affectsStruts}` in global
|
||||
* coordinates. The object is frozen and replaced whenever it changes.
|
||||
*
|
||||
* @type {Readonly<Record<number, object>>}
|
||||
*/
|
||||
get monitors() {
|
||||
return this.#manager.monitors;
|
||||
}
|
||||
});
|
||||
registerDestroyableType(DashToDockStruts);
|
||||
|
||||
export class StrutsManager {
|
||||
#monitors = new Map();
|
||||
#frozenMonitors = Object.freeze({});
|
||||
#struts = new DashToDockStruts(this);
|
||||
|
||||
constructor() {
|
||||
Main.layoutManager.dashToDockStruts = this.#struts;
|
||||
}
|
||||
|
||||
/** The snapshot exposed through `this.struts.monitors`. */
|
||||
get monitors() {
|
||||
return this.#frozenMonitors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or clear the reserved extent for the dock on a monitor.
|
||||
*
|
||||
* The extent is the dock's full-size rectangle, independent of its
|
||||
* current slide offset, so callers may push it on every geometry update:
|
||||
* only genuine changes are published.
|
||||
*
|
||||
* @param {number} monitorIndex - the monitor the dock is on.
|
||||
* @param {object|null} extent - `{side, x, y, width, height,
|
||||
* affectsStruts}` in global coordinates, or `null` to drop the entry.
|
||||
*/
|
||||
setMonitorExtent(monitorIndex, extent) {
|
||||
const previous = this.#monitors.get(monitorIndex) ?? null;
|
||||
const unchanged = previous !== null && extent !== null &&
|
||||
previous.side === extent.side &&
|
||||
previous.x === extent.x && previous.y === extent.y &&
|
||||
previous.width === extent.width && previous.height === extent.height &&
|
||||
previous.affectsStruts === extent.affectsStruts;
|
||||
|
||||
if (unchanged)
|
||||
return;
|
||||
|
||||
if (extent === null)
|
||||
this.#monitors.delete(monitorIndex);
|
||||
else
|
||||
this.#monitors.set(monitorIndex, extent);
|
||||
|
||||
this.#publish(monitorIndex);
|
||||
}
|
||||
|
||||
clear() {
|
||||
if (this.#monitors.size === 0)
|
||||
return;
|
||||
|
||||
this.#monitors.clear();
|
||||
this.#publish(-1);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.clear();
|
||||
if (Main.layoutManager.dashToDockStruts === this.#struts)
|
||||
delete Main.layoutManager.dashToDockStruts;
|
||||
|
||||
this.#struts.emit('destroy');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the frozen snapshot and notify.
|
||||
*
|
||||
* @param {number} monitorIndex - the changed monitor, or -1 for all.
|
||||
*/
|
||||
#publish(monitorIndex) {
|
||||
this.#frozenMonitors = Object.freeze(Object.fromEntries(this.#monitors));
|
||||
this.#struts.emit('updated', monitorIndex);
|
||||
}
|
||||
}
|
||||
+2850
File diff suppressed because it is too large.
Load diff
+23
@@ -0,0 +1,23 @@
|
||||
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
||||
|
||||
import {DockManager} from './docking.js';
|
||||
import {Extension} from './dependencies/shell/extensions/extension.js';
|
||||
|
||||
// We export this so it can be accessed by other extensions
|
||||
export let dockManager;
|
||||
|
||||
export default class DashToDockExtension extends Extension.Extension {
|
||||
enable() {
|
||||
// TODO: Remove this when upstream will disable extensions on shutdown
|
||||
// See: https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/4214
|
||||
this._shutdownID = global.connect('shutdown', () => this.disable());
|
||||
dockManager = new DockManager(this);
|
||||
}
|
||||
|
||||
disable() {
|
||||
global.disconnect(this._shutdownID);
|
||||
delete this._shutdownID;
|
||||
dockManager?.destroy();
|
||||
dockManager = null;
|
||||
}
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
||||
|
||||
import {GLib, Gio} from './dependencies/gi.js';
|
||||
const {signals: Signals} = imports;
|
||||
|
||||
import {Utils} from './imports.js';
|
||||
|
||||
const FileManager1Iface = '<node><interface name="org.freedesktop.FileManager1">\
|
||||
<property name="OpenWindowsWithLocations" type="a{sas}" access="read"/>\
|
||||
</interface></node>';
|
||||
|
||||
const FileManager1Proxy = Gio.DBusProxy.makeProxyWrapper(FileManager1Iface);
|
||||
|
||||
const Labels = Object.freeze({
|
||||
WINDOWS: Symbol('windows'),
|
||||
});
|
||||
|
||||
/**
|
||||
* This class implements a client for the org.freedesktop.FileManager1 dbus
|
||||
* interface, and specifically for the OpenWindowsWithLocations property
|
||||
* which is published by Nautilus, but is not an official part of the interface.
|
||||
*
|
||||
* The property is a map from window identifiers to a list of locations open in
|
||||
* the window.
|
||||
*/
|
||||
export class FileManager1Client {
|
||||
constructor() {
|
||||
this._signalsHandler = new Utils.GlobalSignalsHandler();
|
||||
this._cancellable = new Gio.Cancellable();
|
||||
|
||||
this._windowsByPath = new Map();
|
||||
this._windowsByLocation = new Map();
|
||||
this._proxy = new FileManager1Proxy(Gio.DBus.session,
|
||||
'org.freedesktop.FileManager1',
|
||||
'/org/freedesktop/FileManager1',
|
||||
(initable, error) => {
|
||||
// Use async construction to avoid blocking on errors.
|
||||
if (error) {
|
||||
if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
||||
global.log(error);
|
||||
} else {
|
||||
this._updateWindows();
|
||||
this._updateLocationMap();
|
||||
}
|
||||
}, this._cancellable);
|
||||
|
||||
this._signalsHandler.add([
|
||||
this._proxy,
|
||||
'g-properties-changed',
|
||||
this._onPropertyChanged.bind(this),
|
||||
], [
|
||||
// We must additionally listen for Screen events to know when to
|
||||
// rebuild our location map when the set of available windows changes.
|
||||
global.workspaceManager,
|
||||
'workspace-added',
|
||||
() => this._onWindowsChanged(),
|
||||
], [
|
||||
global.workspaceManager,
|
||||
'workspace-removed',
|
||||
() => this._onWindowsChanged(),
|
||||
], [
|
||||
global.display,
|
||||
'window-entered-monitor',
|
||||
() => this._onWindowsChanged(),
|
||||
], [
|
||||
global.display,
|
||||
'window-left-monitor',
|
||||
() => this._onWindowsChanged(),
|
||||
]);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this._windowsUpdateIdle) {
|
||||
GLib.source_remove(this._windowsUpdateIdle);
|
||||
delete this._windowsUpdateIdle;
|
||||
}
|
||||
this._cancellable.cancel();
|
||||
this._signalsHandler.destroy();
|
||||
this._windowsByLocation.clear();
|
||||
this._windowsByPath.clear();
|
||||
this._proxy = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of windows that are showing a location or
|
||||
* sub-directories of that location.
|
||||
*
|
||||
* @param location
|
||||
*/
|
||||
getWindows(location) {
|
||||
if (!location)
|
||||
return [];
|
||||
|
||||
location += location.endsWith('/') ? '' : '/';
|
||||
const windows = [];
|
||||
this._windowsByLocation.forEach((wins, l) => {
|
||||
if (l.startsWith(location))
|
||||
windows.push(...wins);
|
||||
});
|
||||
return [...new Set(windows)];
|
||||
}
|
||||
|
||||
_onPropertyChanged(proxy, changed, _invalidated) {
|
||||
const property = changed.unpack();
|
||||
if (property &&
|
||||
('OpenWindowsWithLocations' in property))
|
||||
this._updateLocationMap();
|
||||
}
|
||||
|
||||
_updateWindows() {
|
||||
const oldSize = this._windowsByPath.size;
|
||||
const oldPaths = this._windowsByPath.keys();
|
||||
this._windowsByPath = Utils.getWindowsByObjectPath();
|
||||
|
||||
if (oldSize !== this._windowsByPath.size)
|
||||
return true;
|
||||
|
||||
return [...oldPaths].some(path => !this._windowsByPath.has(path));
|
||||
}
|
||||
|
||||
_onWindowsChanged() {
|
||||
if (this._windowsUpdateIdle)
|
||||
return;
|
||||
|
||||
this._windowsUpdateIdle = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
|
||||
if (this._updateWindows())
|
||||
this._updateLocationMap();
|
||||
|
||||
delete this._windowsUpdateIdle;
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
}
|
||||
|
||||
_updateLocationMap() {
|
||||
const properties = this._proxy.get_cached_property_names();
|
||||
if (!properties) {
|
||||
// Nothing to check yet.
|
||||
return;
|
||||
}
|
||||
|
||||
if (properties.includes('OpenWindowsWithLocations'))
|
||||
this._updateFromPaths();
|
||||
}
|
||||
|
||||
_locationMapsEquals(mapA, mapB) {
|
||||
if (mapA.size !== mapB.size)
|
||||
return false;
|
||||
|
||||
const setsEquals = (a, b) => a.size === b.size &&
|
||||
[...a].every(value => b.has(value));
|
||||
|
||||
for (const [key, val] of mapA) {
|
||||
const windowsSet = mapB.get(key);
|
||||
if (!windowsSet || !setsEquals(windowsSet, val))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
_updateFromPaths() {
|
||||
const locationsByWindowsPath = this._proxy.OpenWindowsWithLocations;
|
||||
|
||||
const windowsByLocation = new Map();
|
||||
this._signalsHandler.removeWithLabel(Labels.WINDOWS);
|
||||
|
||||
Object.entries(locationsByWindowsPath).forEach(([windowPath, locations]) => {
|
||||
locations.forEach(location => {
|
||||
const win = this._windowsByPath.get(windowPath);
|
||||
const windowGroup = win ? [win] : [];
|
||||
|
||||
win?.foreach_transient(w => windowGroup.push(w) || true);
|
||||
|
||||
windowGroup.forEach(window => {
|
||||
location += location.endsWith('/') ? '' : '/';
|
||||
// Use a set to deduplicate when a window has a
|
||||
// location open in multiple tabs.
|
||||
const windows = windowsByLocation.get(location) || new Set();
|
||||
windows.add(window);
|
||||
|
||||
if (windows.size === 1)
|
||||
windowsByLocation.set(location, windows);
|
||||
|
||||
this._signalsHandler.addWithLabel(Labels.WINDOWS, window,
|
||||
'unmanaged', () => {
|
||||
const wins = this._windowsByLocation.get(location);
|
||||
wins.delete(window);
|
||||
if (!wins.size)
|
||||
this._windowsByLocation.delete(location);
|
||||
this.emit('windows-changed');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (!this._locationMapsEquals(this._windowsByLocation, windowsByLocation)) {
|
||||
this._windowsByLocation = windowsByLocation;
|
||||
this.emit('windows-changed');
|
||||
}
|
||||
}
|
||||
}
|
||||
Signals.addSignalMethods(FileManager1Client.prototype);
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
export * as AppIconIndicators from './appIconIndicators.js';
|
||||
export * as AppIcons from './appIcons.js';
|
||||
export * as AppIconsDecorator from './appIconsDecorator.js';
|
||||
export * as AppSpread from './appSpread.js';
|
||||
export * as DockDash from './dash.js';
|
||||
export * as DBusMenuUtils from './dbusmenuUtils.js';
|
||||
export * as DesktopIconsIntegration from './desktopIconsIntegration.js';
|
||||
export * as Docking from './docking.js';
|
||||
export * as Extension from './extension.js';
|
||||
export * as FileManager1API from './fileManager1API.js';
|
||||
export * as Intellihide from './intellihide.js';
|
||||
export * as LauncherAPI from './launcherAPI.js';
|
||||
export * as Locations from './locations.js';
|
||||
export * as NotificationsMonitor from './notificationsMonitor.js';
|
||||
export * as Theming from './theming.js';
|
||||
export * as Utils from './utils.js';
|
||||
export * as WindowPreview from './windowPreview.js';
|
||||
+340
@@ -0,0 +1,340 @@
|
||||
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
||||
|
||||
import {
|
||||
GLib,
|
||||
Meta,
|
||||
} from './dependencies/gi.js';
|
||||
|
||||
import {
|
||||
Docking,
|
||||
Utils,
|
||||
} from './imports.js';
|
||||
|
||||
const {signals: Signals} = imports;
|
||||
|
||||
// A good compromise between reactivity and efficiency; to be tuned.
|
||||
const INTELLIHIDE_CHECK_INTERVAL = 100;
|
||||
|
||||
const OverlapStatus = Object.freeze({
|
||||
UNDEFINED: -1,
|
||||
FALSE: 0,
|
||||
TRUE: 1,
|
||||
});
|
||||
|
||||
const IntellihideMode = Object.freeze({
|
||||
ALL_WINDOWS: 0,
|
||||
FOCUS_APPLICATION_WINDOWS: 1,
|
||||
MAXIMIZED_WINDOWS: 2,
|
||||
ALWAYS_ON_TOP: 3,
|
||||
});
|
||||
|
||||
// List of windows type taken into account. Order is important (keep the original
|
||||
// enum order).
|
||||
const handledWindowTypes = [
|
||||
Meta.WindowType.NORMAL,
|
||||
Meta.WindowType.DOCK,
|
||||
Meta.WindowType.DIALOG,
|
||||
Meta.WindowType.MODAL_DIALOG,
|
||||
Meta.WindowType.TOOLBAR,
|
||||
Meta.WindowType.MENU,
|
||||
Meta.WindowType.UTILITY,
|
||||
Meta.WindowType.SPLASHSCREEN,
|
||||
Meta.WindowType.DROPDOWN_MENU,
|
||||
];
|
||||
|
||||
// List of applications, ignore windows of these applications in considering intellihide
|
||||
const ignoreApps = ['com.rastersoft.ding', 'com.desktop.ding'];
|
||||
|
||||
/**
|
||||
* A rough and ugly implementation of the intellihide behaviour.
|
||||
* Intallihide object: emit 'status-changed' signal when the overlap of windows
|
||||
* with the provided targetBoxClutter.ActorBox changes;
|
||||
*/
|
||||
export class Intellihide {
|
||||
constructor(monitorIndex) {
|
||||
// Load settings
|
||||
this._monitorIndex = monitorIndex;
|
||||
|
||||
this._signalsHandler = new Utils.GlobalSignalsHandler();
|
||||
this._focusApp = null; // The application whose window is focused.
|
||||
this._topApp = null; // The application whose window is on top on the monitor with the dock.
|
||||
|
||||
this._isEnabled = false;
|
||||
this._status = OverlapStatus.UNDEFINED;
|
||||
this._targetBox = null;
|
||||
|
||||
this._checkOverlapTimeoutContinue = false;
|
||||
this._checkOverlapTimeoutId = 0;
|
||||
|
||||
this._trackedWindows = new Map();
|
||||
|
||||
// Connect global signals
|
||||
this._signalsHandler.add([
|
||||
// Add signals on windows created from now on
|
||||
global.display,
|
||||
'window-created',
|
||||
this._windowCreated.bind(this),
|
||||
], [
|
||||
// triggered for instance when the window list order changes,
|
||||
// included when the workspace is switched
|
||||
global.display,
|
||||
'restacked',
|
||||
this._checkOverlap.bind(this),
|
||||
], [
|
||||
// when windows are alwasy on top, the focus window can change
|
||||
// without the windows being restacked. Thus monitor window focus change.
|
||||
Docking.DockManager.windowTracker,
|
||||
'notify::focus-app',
|
||||
this._checkOverlap.bind(this),
|
||||
], [
|
||||
// update wne monitor changes, for instance in multimonitor when monitor are attached
|
||||
Utils.getMonitorManager(),
|
||||
'monitors-changed',
|
||||
this._checkOverlap.bind(this),
|
||||
]);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// Disconnect global signals
|
||||
this._signalsHandler.destroy();
|
||||
|
||||
// Remove residual windows signals
|
||||
this.disable();
|
||||
}
|
||||
|
||||
enable() {
|
||||
this._isEnabled = true;
|
||||
this._status = OverlapStatus.UNDEFINED;
|
||||
global.get_window_actors().forEach(function (wa) {
|
||||
this._addWindowSignals(wa);
|
||||
}, this);
|
||||
this._doCheckOverlap();
|
||||
}
|
||||
|
||||
disable() {
|
||||
this._isEnabled = false;
|
||||
|
||||
for (const wa of this._trackedWindows.keys())
|
||||
this._removeWindowSignals(wa);
|
||||
|
||||
this._trackedWindows.clear();
|
||||
|
||||
if (this._checkOverlapTimeoutId > 0) {
|
||||
GLib.source_remove(this._checkOverlapTimeoutId);
|
||||
this._checkOverlapTimeoutId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_windowCreated(display, metaWindow) {
|
||||
this._addWindowSignals(metaWindow.get_compositor_private());
|
||||
this._doCheckOverlap();
|
||||
}
|
||||
|
||||
_addWindowSignals(wa) {
|
||||
if (!this._handledWindow(wa))
|
||||
return;
|
||||
|
||||
this._trackedWindows.set(wa, [
|
||||
wa.connect('notify::allocation', () => this._checkOverlap()),
|
||||
wa.connect('destroy', () => this._removeWindowSignals(wa)),
|
||||
]);
|
||||
}
|
||||
|
||||
_removeWindowSignals(wa) {
|
||||
const signalIds = this._trackedWindows.get(wa);
|
||||
if (signalIds) {
|
||||
signalIds.forEach(id => wa.disconnect(id));
|
||||
this._trackedWindows.delete(wa);
|
||||
}
|
||||
}
|
||||
|
||||
updateTargetBox(box) {
|
||||
this._targetBox = box;
|
||||
this._checkOverlap();
|
||||
}
|
||||
|
||||
forceUpdate() {
|
||||
this._status = OverlapStatus.UNDEFINED;
|
||||
this._doCheckOverlap();
|
||||
}
|
||||
|
||||
getOverlapStatus() {
|
||||
return this._status === OverlapStatus.TRUE;
|
||||
}
|
||||
|
||||
_checkOverlap() {
|
||||
if (!this._isEnabled || !this._targetBox)
|
||||
return;
|
||||
|
||||
/* Limit the number of calls to the doCheckOverlap function */
|
||||
if (this._checkOverlapTimeoutId) {
|
||||
this._checkOverlapTimeoutContinue = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this._doCheckOverlap();
|
||||
|
||||
this._checkOverlapTimeoutId = GLib.timeout_add(
|
||||
GLib.PRIORITY_DEFAULT, INTELLIHIDE_CHECK_INTERVAL, () => {
|
||||
this._doCheckOverlap();
|
||||
if (this._checkOverlapTimeoutContinue) {
|
||||
this._checkOverlapTimeoutContinue = false;
|
||||
return GLib.SOURCE_CONTINUE;
|
||||
} else {
|
||||
this._checkOverlapTimeoutId = 0;
|
||||
return GLib.SOURCE_REMOVE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_doCheckOverlap() {
|
||||
if (!this._isEnabled || !this._targetBox)
|
||||
return;
|
||||
|
||||
let overlaps = OverlapStatus.FALSE;
|
||||
let windows = global.get_window_actors().filter(wa => this._handledWindow(wa));
|
||||
|
||||
if (windows.length > 0) {
|
||||
/*
|
||||
* Get the top window on the monitor where the dock is placed.
|
||||
* The idea is that we dont want to overlap with the windows of the topmost application,
|
||||
* event is it's not the focused app -- for instance because in multimonitor the user
|
||||
* select a window in the secondary monitor.
|
||||
*/
|
||||
|
||||
let topWindow = null;
|
||||
for (let i = windows.length - 1; i >= 0; i--) {
|
||||
const metaWin = windows[i].get_meta_window();
|
||||
if (metaWin.get_monitor() === this._monitorIndex) {
|
||||
topWindow = metaWin;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (topWindow) {
|
||||
const {windowTracker} = Docking.DockManager;
|
||||
this._topApp = windowTracker.get_window_app(topWindow);
|
||||
// If there isn't a focused app, use that of the window on top
|
||||
this._focusApp = windowTracker.focus_app || this._topApp;
|
||||
|
||||
windows = windows.filter(this._intellihideFilterInteresting, this);
|
||||
|
||||
for (let i = 0; i < windows.length; i++) {
|
||||
const win = windows[i].get_meta_window();
|
||||
|
||||
if (win) {
|
||||
const rect = win.get_frame_rect();
|
||||
|
||||
const test = (rect.x < this._targetBox.x2) &&
|
||||
(rect.x + rect.width >= this._targetBox.x1) &&
|
||||
(rect.y < this._targetBox.y2) &&
|
||||
(rect.y + rect.height >= this._targetBox.y1);
|
||||
|
||||
if (test) {
|
||||
overlaps = OverlapStatus.TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this._status !== overlaps) {
|
||||
this._status = overlaps;
|
||||
this.emit('status-changed', this._status);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter interesting windows to be considered for intellihide.
|
||||
// Consider all windows visible on the current workspace.
|
||||
// Optionally skip windows of other applications
|
||||
_intellihideFilterInteresting(wa) {
|
||||
const metaWin = wa.get_meta_window();
|
||||
const currentWorkspace = global.workspace_manager.get_active_workspace_index();
|
||||
const workspace = metaWin.get_workspace();
|
||||
const workspaceIndex = workspace.index();
|
||||
|
||||
// Depending on the intellihide mode, exclude non-relevent windows
|
||||
switch (Docking.DockManager.settings.intellihideMode) {
|
||||
case IntellihideMode.ALL_WINDOWS:
|
||||
// Do nothing
|
||||
break;
|
||||
|
||||
case IntellihideMode.FOCUS_APPLICATION_WINDOWS:
|
||||
// Skip windows of other apps
|
||||
if (this._focusApp) {
|
||||
// The DropDownTerminal extension is not an application per se
|
||||
// so we match its window by wm class instead
|
||||
if (metaWin.get_wm_class() === 'DropDownTerminalWindow')
|
||||
return true;
|
||||
|
||||
const {windowTracker} = Docking.DockManager;
|
||||
const currentApp = windowTracker.get_window_app(metaWin);
|
||||
const focusWindow = global.display.get_focus_window();
|
||||
|
||||
// Consider half maximized windows side by side
|
||||
// and windows which are alwayson top
|
||||
if (currentApp !== this._focusApp && currentApp !== this._topApp &&
|
||||
!((focusWindow && focusWindow.maximized_vertically &&
|
||||
!focusWindow.maximized_horizontally) &&
|
||||
(metaWin.maximized_vertically && !metaWin.maximized_horizontally) &&
|
||||
metaWin.get_monitor() === focusWindow.get_monitor()) &&
|
||||
!metaWin.is_above())
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case IntellihideMode.MAXIMIZED_WINDOWS:
|
||||
// Skip unmaximized windows
|
||||
if (!metaWin.maximized_vertically && !metaWin.maximized_horizontally && !metaWin.fullscreen)
|
||||
return false;
|
||||
break;
|
||||
|
||||
case IntellihideMode.ALWAYS_ON_TOP:
|
||||
// Always on top, except for fullscreen windows
|
||||
if (this._focusApp) {
|
||||
const {focusWindow} = global.display;
|
||||
if (!focusWindow?.fullscreen)
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (workspaceIndex === currentWorkspace && metaWin.showing_on_its_workspace())
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter windows by type
|
||||
// inspired by Opacify@gnome-shell.localdomain.pl
|
||||
_handledWindow(wa) {
|
||||
const metaWindow = wa.get_meta_window();
|
||||
|
||||
if (!metaWindow)
|
||||
return false;
|
||||
|
||||
// The DING extension desktop window needs to be excluded
|
||||
// so we match its window by application id and window property.
|
||||
const wmApp = metaWindow.get_gtk_application_id();
|
||||
if (ignoreApps.includes(wmApp) && metaWindow.is_skip_taskbar())
|
||||
return false;
|
||||
|
||||
// The DropDownTerminal extension uses the POPUP_MENU window type hint
|
||||
// so we match its window by wm class instead
|
||||
if (metaWindow.get_wm_class() === 'DropDownTerminalWindow')
|
||||
return true;
|
||||
|
||||
const wtype = metaWindow.get_window_type();
|
||||
for (let i = 0; i < handledWindowTypes.length; i++) {
|
||||
const hwtype = handledWindowTypes[i];
|
||||
if (hwtype === wtype)
|
||||
return true;
|
||||
else if (hwtype > wtype)
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Signals.addSignalMethods(Intellihide.prototype);
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
||||
|
||||
import {Gio} from './dependencies/gi.js';
|
||||
import {DBusMenuUtils} from './imports.js';
|
||||
|
||||
const DBusMenu = await DBusMenuUtils.haveDBusMenu();
|
||||
|
||||
export class LauncherEntryRemoteModel {
|
||||
constructor() {
|
||||
this._entrySourceStacks = new Map();
|
||||
this._remoteMaps = new Map();
|
||||
|
||||
this._launcher_entry_dbus_signal_id =
|
||||
Gio.DBus.session.signal_subscribe(null, // sender
|
||||
'com.canonical.Unity.LauncherEntry', // iface
|
||||
'Update', // member
|
||||
null, // path
|
||||
null, // arg0
|
||||
Gio.DBusSignalFlags.NONE,
|
||||
(_connection, senderName, _objectPath, _interfaceName, _signalName, parameters) =>
|
||||
this._onUpdate(senderName, ...parameters.deep_unpack()));
|
||||
|
||||
this._dbus_name_owner_changed_signal_id =
|
||||
Gio.DBus.session.signal_subscribe('org.freedesktop.DBus', // sender
|
||||
'org.freedesktop.DBus', // interface
|
||||
'NameOwnerChanged', // member
|
||||
'/org/freedesktop/DBus', // path
|
||||
null, // arg0
|
||||
Gio.DBusSignalFlags.NONE,
|
||||
(connection, _senderName, _objectPath, _interfaceName, _signalName, parameters) =>
|
||||
this._onDBusNameChange(...parameters.deep_unpack().slice(1)));
|
||||
|
||||
this._acquireUnityDBus();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this._launcher_entry_dbus_signal_id)
|
||||
Gio.DBus.session.signal_unsubscribe(this._launcher_entry_dbus_signal_id);
|
||||
|
||||
|
||||
if (this._dbus_name_owner_changed_signal_id)
|
||||
Gio.DBus.session.signal_unsubscribe(this._dbus_name_owner_changed_signal_id);
|
||||
|
||||
|
||||
this._releaseUnityDBus();
|
||||
}
|
||||
|
||||
_lookupStackById(appId) {
|
||||
let sourceStack = this._entrySourceStacks.get(appId);
|
||||
if (!sourceStack) {
|
||||
sourceStack = new PropertySourceStack(new LauncherEntry(),
|
||||
launcherEntryDefaults);
|
||||
this._entrySourceStacks.set(appId, sourceStack);
|
||||
}
|
||||
|
||||
return sourceStack;
|
||||
}
|
||||
|
||||
lookupById(appId) {
|
||||
return this._lookupStackById(appId).target;
|
||||
}
|
||||
|
||||
_acquireUnityDBus() {
|
||||
if (!this._unity_bus_id) {
|
||||
this._unity_bus_id = Gio.DBus.session.own_name('com.canonical.Unity',
|
||||
Gio.BusNameOwnerFlags.ALLOW_REPLACEMENT | Gio.BusNameOwnerFlags.REPLACE,
|
||||
null, () => (this._unity_bus_id = 0));
|
||||
}
|
||||
}
|
||||
|
||||
_releaseUnityDBus() {
|
||||
if (this._unity_bus_id) {
|
||||
Gio.DBus.session.unown_name(this._unity_bus_id);
|
||||
this._unity_bus_id = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_onDBusNameChange(before, after) {
|
||||
if (!before || !this._remoteMaps.size)
|
||||
return;
|
||||
|
||||
const remoteMap = this._remoteMaps.get(before);
|
||||
if (!remoteMap)
|
||||
return;
|
||||
|
||||
this._remoteMaps.delete(before);
|
||||
if (after && !this._remoteMaps.has(after)) {
|
||||
this._remoteMaps.set(after, remoteMap);
|
||||
} else {
|
||||
for (const [appId, remote] of remoteMap) {
|
||||
const sourceStack = this._entrySourceStacks.get(appId);
|
||||
const changed = sourceStack.remove(remote);
|
||||
if (changed)
|
||||
sourceStack.target._emitChangedEvents(changed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onUpdate(senderName, appUri, properties) {
|
||||
if (!senderName)
|
||||
return;
|
||||
|
||||
|
||||
const appId = appUri.replace(/(^\w+:|^)\/\//, '');
|
||||
if (!appId)
|
||||
return;
|
||||
|
||||
|
||||
let remoteMap = this._remoteMaps.get(senderName);
|
||||
if (!remoteMap)
|
||||
this._remoteMaps.set(senderName, remoteMap = new Map());
|
||||
|
||||
let remote = remoteMap.get(appId);
|
||||
if (!remote)
|
||||
remoteMap.set(appId, remote = Object.assign({}, launcherEntryDefaults));
|
||||
|
||||
for (const name in properties) {
|
||||
if (name === 'quicklist' && DBusMenu) {
|
||||
const quicklistPath = properties[name].unpack();
|
||||
if (quicklistPath &&
|
||||
(!remote._quicklistMenuClient ||
|
||||
remote._quicklistMenuClient.dbus_object !== quicklistPath)) {
|
||||
remote.quicklist = null;
|
||||
let menuClient = remote._quicklistMenuClient;
|
||||
if (menuClient) {
|
||||
menuClient.disconnect(menuClient._rootChangedHandlerId);
|
||||
menuClient.dbus_object = quicklistPath;
|
||||
} else {
|
||||
// This property should not be enumerable
|
||||
Object.defineProperty(remote, '_quicklistMenuClient', {
|
||||
writable: true,
|
||||
value: menuClient = new DBusMenu.Client({
|
||||
dbus_name: senderName,
|
||||
dbus_object: quicklistPath,
|
||||
}),
|
||||
});
|
||||
}
|
||||
const handler = () => {
|
||||
const root = menuClient.get_root();
|
||||
if (remote.quicklist !== root) {
|
||||
remote.quicklist = root;
|
||||
if (sourceStack.isTop(remote)) {
|
||||
sourceStack.target.quicklist = root;
|
||||
sourceStack.target._emitChangedEvents(['quicklist']);
|
||||
}
|
||||
}
|
||||
};
|
||||
menuClient._rootChangedHandlerId =
|
||||
menuClient.connect(DBusMenu.CLIENT_SIGNAL_ROOT_CHANGED, handler);
|
||||
}
|
||||
} else {
|
||||
remote[name] = properties[name].unpack();
|
||||
}
|
||||
}
|
||||
|
||||
const sourceStack = this._lookupStackById(appId);
|
||||
sourceStack.target._emitChangedEvents(sourceStack.update(remote));
|
||||
}
|
||||
}
|
||||
|
||||
const launcherEntryDefaults = Object.freeze({
|
||||
count: 0,
|
||||
progress: 0,
|
||||
urgent: false,
|
||||
updating: false,
|
||||
quicklist: null,
|
||||
'count-visible': false,
|
||||
'progress-visible': false,
|
||||
});
|
||||
|
||||
const LauncherEntry = class DashToDockLauncherEntry {
|
||||
constructor() {
|
||||
this._connections = new Map();
|
||||
this._handlers = new Map();
|
||||
this._nextId = 0;
|
||||
}
|
||||
|
||||
connect(eventNames, callback) {
|
||||
if (typeof eventNames === 'string')
|
||||
eventNames = [eventNames];
|
||||
|
||||
callback(this, this);
|
||||
const id = this._nextId++;
|
||||
const handler = {id, callback};
|
||||
eventNames.forEach(name => {
|
||||
let handlerList = this._handlers.get(name);
|
||||
if (!handlerList)
|
||||
this._handlers.set(name, handlerList = []);
|
||||
|
||||
handlerList.push(handler);
|
||||
});
|
||||
this._connections.set(id, eventNames);
|
||||
return id;
|
||||
}
|
||||
|
||||
disconnect(id) {
|
||||
const eventNames = this._connections.get(id);
|
||||
if (!eventNames)
|
||||
return;
|
||||
|
||||
this._connections.delete(id);
|
||||
eventNames.forEach(name => {
|
||||
const handlerList = this._handlers.get(name);
|
||||
if (handlerList) {
|
||||
for (let i = 0, iMax = handlerList.length; i < iMax; i++) {
|
||||
if (handlerList[i].id === id) {
|
||||
handlerList.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_emitChangedEvents(propertyNames) {
|
||||
const handlers = new Set();
|
||||
propertyNames.forEach(name => {
|
||||
const handlerList = this._handlers.get(`${name}-changed`);
|
||||
if (handlerList) {
|
||||
for (let i = 0, iMax = handlerList.length; i < iMax; i++)
|
||||
handlers.add(handlerList[i]);
|
||||
}
|
||||
});
|
||||
Array.from(handlers).sort((x, y) => x.id - y.id).forEach(handler => handler.callback(this, this));
|
||||
}
|
||||
};
|
||||
|
||||
for (const [name, defaultValue] of Object.entries(launcherEntryDefaults)) {
|
||||
const jsName = name.replaceAll('-', '_');
|
||||
LauncherEntry.prototype[jsName] = defaultValue;
|
||||
if (jsName !== name) {
|
||||
Object.defineProperty(LauncherEntry.prototype, name, {
|
||||
get() {
|
||||
return this[jsName];
|
||||
},
|
||||
set(value) {
|
||||
this[jsName] = value;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const PropertySourceStack = class DashToDockPropertySourceStack {
|
||||
constructor(target, bottom) {
|
||||
this.target = target;
|
||||
this._bottom = bottom;
|
||||
this._stack = [];
|
||||
}
|
||||
|
||||
isTop(source) {
|
||||
return this._stack.length > 0 && this._stack[this._stack.length - 1] === source;
|
||||
}
|
||||
|
||||
update(source) {
|
||||
if (!this.isTop(source)) {
|
||||
this.remove(source);
|
||||
this._stack.push(source);
|
||||
}
|
||||
return this._assignFrom(source);
|
||||
}
|
||||
|
||||
remove(source) {
|
||||
const stack = this._stack;
|
||||
const top = stack[stack.length - 1];
|
||||
if (top === source) {
|
||||
stack.length--;
|
||||
return this._assignFrom(stack.length > 0 ? stack[stack.length - 1] : this._bottom);
|
||||
}
|
||||
for (let i = 0, iMax = stack.length; i < iMax; i++) {
|
||||
if (stack[i] === source) {
|
||||
stack.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_assignFrom(source) {
|
||||
const changedProperties = [];
|
||||
for (const name in source) {
|
||||
if (this.target[name] !== source[name]) {
|
||||
this.target[name] = source[name];
|
||||
changedProperties.push(name);
|
||||
}
|
||||
}
|
||||
return changedProperties;
|
||||
}
|
||||
};
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Loaded 100 of 128 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user