mirror of
https://gitlab.gnome.org/june/top-bar-organizer.git
synced 2026-01-10 16:26:35 +01:00
Remove unnecessary check for empty array in #getResolvedBoxOrder as nothing happens when the array is empty anyway.
388 lines
17 KiB
TypeScript
388 lines
17 KiB
TypeScript
"use strict";
|
|
|
|
import GObject from "gi://GObject";
|
|
import St from "gi://St";
|
|
import type Gio from "gi://Gio";
|
|
|
|
import * as Main from "resource:///org/gnome/shell/ui/main.js";
|
|
|
|
import type { CustomPanel } from "../extension.js"
|
|
|
|
export type Box = "left" | "center" | "right";
|
|
type Hide = "hide" | "show" | "default";
|
|
/**
|
|
* A resolved box order item containing the items role, settings identifier and
|
|
* additional information.
|
|
*/
|
|
interface ResolvedBoxOrderItem {
|
|
settingsId: string // The settings identifier of the item.
|
|
role: string // The role of the item.
|
|
hide: Hide // Whether the item should be (forcefully) hidden, (forcefully) shown or just be left as is.
|
|
}
|
|
|
|
/**
|
|
* This class provides an interfaces to the box orders stored in settings.
|
|
* It takes care of handling AppIndicator and Task Up UltraLite items and
|
|
* resolving from the internal item settings identifiers to roles.
|
|
* In the end this results in convenient functions, which are directly useful in
|
|
* other extension code.
|
|
*/
|
|
export default class BoxOrderManager extends GObject.Object {
|
|
static {
|
|
GObject.registerClass({
|
|
Signals: {
|
|
"appIndicatorReady": {},
|
|
},
|
|
}, this);
|
|
}
|
|
|
|
// Can't have type guarantees here, since this is working with types from
|
|
// the KStatusNotifier/AppIndicator extension.
|
|
#appIndicatorReadyHandlerIdMap: Map<any, any>;
|
|
#appIndicatorItemSettingsIdToRolesMap: Map<string, string[]>;
|
|
#taskUpUltraLiteItemRoles: string[];
|
|
#settings: Gio.Settings;
|
|
|
|
constructor(params = {}, settings: Gio.Settings) {
|
|
// @ts-ignore Params should be passed, see: https://gjs.guide/guides/gobject/subclassing.html#subclassing-gobject
|
|
super(params);
|
|
|
|
this.#appIndicatorReadyHandlerIdMap = new Map();
|
|
this.#appIndicatorItemSettingsIdToRolesMap = new Map();
|
|
this.#taskUpUltraLiteItemRoles = [];
|
|
|
|
this.#settings = settings;
|
|
}
|
|
|
|
/**
|
|
* Gets a box order for the given top bar box from settings.
|
|
* @param {Box} box - The top bar box for which to get the box order.
|
|
* @returns {string[]} - The box order consisting of an array of item
|
|
* settings identifiers.
|
|
*/
|
|
#getBoxOrder(box: Box): string[] {
|
|
return this.#settings.get_strv(`${box}-box-order`);
|
|
}
|
|
|
|
/**
|
|
* Save the given box order to settings, making sure to only save a changed
|
|
* box order, to avoid loops when listening on settings changes.
|
|
* @param {Box} box - The top bar box for which to save the box order.
|
|
* @param {string[]} boxOrder - The box order to save. Must be an array of
|
|
* item settings identifiers.
|
|
*/
|
|
#saveBoxOrder(box: Box, boxOrder: string[]): void {
|
|
const currentBoxOrder = this.#getBoxOrder(box);
|
|
|
|
// Only save the given box order to settings, if it is different, to
|
|
// avoid loops when listening on settings changes.
|
|
if (JSON.stringify(boxOrder) !== JSON.stringify(currentBoxOrder)) {
|
|
this.#settings.set_strv(`${box}-box-order`, boxOrder);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles an AppIndicator/KStatusNotifierItem item by deriving a settings
|
|
* identifier and then associating the role of the given item to the items
|
|
* settings identifier.
|
|
* It then returns the derived settings identifier.
|
|
* In the case, where the settings identifier can't be derived, because the
|
|
* application can't be determined, this method throws an error. However it
|
|
* then also makes sure that once the app indicators "ready" signal emits,
|
|
* this classes "appIndicatorReady" signal emits as well, such that it and
|
|
* other methods can be called again to properly handle the item.
|
|
* @param {St.Bin} indicatorContainer - The container of the indicator of the
|
|
* AppIndicator/KStatusNotifierItem item.
|
|
* @param {string} role - The role of the AppIndicator/KStatusNotifierItem
|
|
* item.
|
|
* @returns {string} The derived items settings identifier.
|
|
*/
|
|
#handleAppIndicatorItem(indicatorContainer: St.Bin, role: string): string {
|
|
// Since this is working with types from the
|
|
// AppIndicator/KStatusNotifierItem extension, we loose a bunch of type
|
|
// safety here.
|
|
// https://github.com/ubuntu/gnome-shell-extension-appindicator
|
|
const appIndicator = (indicatorContainer.get_child() as any)._indicator;
|
|
let application = appIndicator.id;
|
|
|
|
if (!application && this.#appIndicatorReadyHandlerIdMap) {
|
|
const handlerId = appIndicator.connect("ready", () => {
|
|
this.emit("appIndicatorReady");
|
|
appIndicator.disconnect(handlerId);
|
|
this.#appIndicatorReadyHandlerIdMap.delete(handlerId);
|
|
});
|
|
this.#appIndicatorReadyHandlerIdMap.set(handlerId, appIndicator);
|
|
throw new Error("Application can't be determined.");
|
|
}
|
|
|
|
// Since the Dropbox client appends its PID to the id, drop the PID and
|
|
// the hyphen before it.
|
|
if (application.startsWith("dropbox-client-")) {
|
|
application = "dropbox-client";
|
|
}
|
|
|
|
// Derive the items settings identifier from the application name.
|
|
const itemSettingsId = `appindicator-kstatusnotifieritem-${application}`;
|
|
|
|
// Associate the role with the items settings identifier.
|
|
let roles = this.#appIndicatorItemSettingsIdToRolesMap.get(itemSettingsId);
|
|
if (roles) {
|
|
// If the settings identifier already has an array of associated
|
|
// roles, just add the role to it, if needed.
|
|
if (!roles.includes(role)) {
|
|
roles.push(role);
|
|
}
|
|
} else {
|
|
// Otherwise create a new array.
|
|
this.#appIndicatorItemSettingsIdToRolesMap.set(itemSettingsId, [role]);
|
|
}
|
|
|
|
// Return the item settings identifier.
|
|
return itemSettingsId;
|
|
}
|
|
|
|
/**
|
|
* Handles a Task Up UltraLite item by storing its role and returning the
|
|
* Task Up UltraLite settings identifier.
|
|
* This is needed since the Task Up UltraLite extension creates a bunch of
|
|
* top bar items as part of its functionality, so we want to group them
|
|
* under one identifier in the settings.
|
|
* https://extensions.gnome.org/extension/7700/task-up-ultralite/
|
|
* @param {string} role - The role of the Task Up UltraLite item.
|
|
* @returns {string} The settings identifier to use.
|
|
*/
|
|
#handleTaskUpUltraLiteItem(role: string): string {
|
|
const roles = this.#taskUpUltraLiteItemRoles;
|
|
|
|
if (!roles.includes(role)) {
|
|
roles.push(role);
|
|
}
|
|
|
|
return "item-role-group-task-up-ultralite";
|
|
}
|
|
|
|
/**
|
|
* Gets a resolved box order for the given top bar box, where all
|
|
* AppIndicator and Task Up UltraLite items got resolved using their roles,
|
|
* meaning they might be present multiple times or not at all depending on
|
|
* the roles stored.
|
|
* The items of the box order also have additional information stored.
|
|
* @param {Box} box - The top bar box for which to get the resolved box order.
|
|
* @returns {ResolvedBoxOrderItem[]} - The resolved box order.
|
|
*/
|
|
#getResolvedBoxOrder(box: Box): ResolvedBoxOrderItem[] {
|
|
let boxOrder = this.#getBoxOrder(box);
|
|
|
|
const itemsToHide = this.#settings.get_strv("hide");
|
|
const itemsToShow = this.#settings.get_strv("show");
|
|
|
|
let resolvedBoxOrder = [];
|
|
for (const itemSettingsId of boxOrder) {
|
|
const resolvedBoxOrderItem = {
|
|
settingsId: itemSettingsId,
|
|
role: "",
|
|
hide: "",
|
|
};
|
|
|
|
// Set the hide state of the item.
|
|
if (itemsToHide.includes(resolvedBoxOrderItem.settingsId)) {
|
|
resolvedBoxOrderItem.hide = "hide";
|
|
} else if (itemsToShow.includes(resolvedBoxOrderItem.settingsId)) {
|
|
resolvedBoxOrderItem.hide = "show";
|
|
} else {
|
|
resolvedBoxOrderItem.hide = "default";
|
|
}
|
|
|
|
// If the items settings identifier doesn't indicate that the item
|
|
// is an AppIndicator/KStatusNotifierItem item or the Task Up
|
|
// UltraLite item role group, then its identifier is the role and it
|
|
// can just be added to the resolved box order.
|
|
if (!itemSettingsId.startsWith("appindicator-kstatusnotifieritem-") &&
|
|
itemSettingsId !== "item-role-group-task-up-ultralite") {
|
|
resolvedBoxOrderItem.role = resolvedBoxOrderItem.settingsId;
|
|
resolvedBoxOrder.push(resolvedBoxOrderItem);
|
|
continue;
|
|
}
|
|
|
|
// If the items settings identifier indicates otherwise, then handle
|
|
// the item specially.
|
|
|
|
// Get the roles associated with the items settings id.
|
|
let roles: string[] = [];
|
|
if (itemSettingsId.startsWith("appindicator-kstatusnotifieritem-")) {
|
|
roles = this.#appIndicatorItemSettingsIdToRolesMap.get(resolvedBoxOrderItem.settingsId) ?? [];
|
|
} else if (itemSettingsId === "item-role-group-task-up-ultralite") {
|
|
roles = this.#taskUpUltraLiteItemRoles;
|
|
}
|
|
|
|
// Create a new resolved box order item for each role and add it to
|
|
// the resolved box order.
|
|
for (const role of roles) {
|
|
const newResolvedBoxOrderItem = JSON.parse(JSON.stringify(resolvedBoxOrderItem));
|
|
newResolvedBoxOrderItem.role = role;
|
|
resolvedBoxOrder.push(newResolvedBoxOrderItem);
|
|
}
|
|
}
|
|
|
|
return resolvedBoxOrder;
|
|
}
|
|
|
|
/**
|
|
* Disconnects all signals (and disables future signal connection).
|
|
* This is typically used before nulling an instance of this class to make
|
|
* sure all signals are disconnected.
|
|
*/
|
|
disconnectSignals(): void {
|
|
for (const [handlerId, appIndicator] of this.#appIndicatorReadyHandlerIdMap) {
|
|
if (handlerId && appIndicator?.signalHandlerIsConnected(handlerId)) {
|
|
appIndicator.disconnect(handlerId);
|
|
}
|
|
}
|
|
// @ts-ignore
|
|
this.#appIndicatorReadyHandlerIdMap = null;
|
|
}
|
|
|
|
/**
|
|
* Gets a valid box order for the given top bar box, where all AppIndicator
|
|
* and Task Up UltraLite items got resolved and where only items are
|
|
* included, which are in some GNOME Shell top bar box.
|
|
* The items of the box order also have additional information stored.
|
|
* @param {Box} box - The top bar box to return the valid box order for.
|
|
* @returns {ResolvedBoxOrderItem[]} - The valid box order.
|
|
*/
|
|
getValidBoxOrder(box: Box): ResolvedBoxOrderItem[] {
|
|
// Get a resolved box order.
|
|
let resolvedBoxOrder = this.#getResolvedBoxOrder(box);
|
|
|
|
// Get the indicator containers (of the items) currently present in the
|
|
// GNOME Shell top bar.
|
|
// They should be St.Bins (see link), so ensure that using a filter.
|
|
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/48.2/js/ui/panelMenu.js?ref_type=tags#L21
|
|
const indicatorContainers = new Set([
|
|
(Main.panel as CustomPanel)._leftBox.get_children(),
|
|
(Main.panel as CustomPanel)._centerBox.get_children(),
|
|
(Main.panel as CustomPanel)._rightBox.get_children(),
|
|
].flat().filter(ic => ic instanceof St.Bin));
|
|
|
|
// Go through the resolved box order and only add items to the valid box
|
|
// order, where their indicator is currently present in the GNOME Shell
|
|
// top bar.
|
|
let validBoxOrder: ResolvedBoxOrderItem[] = [];
|
|
for (const item of resolvedBoxOrder) {
|
|
const associatedIndicatorContainer = (Main.panel.statusArea as any)[item.role]?.container;
|
|
if (!(associatedIndicatorContainer instanceof St.Bin)) {
|
|
// TODO: maybe add logging
|
|
continue;
|
|
}
|
|
|
|
if (indicatorContainers.has(associatedIndicatorContainer)) {
|
|
validBoxOrder.push(item);
|
|
}
|
|
}
|
|
|
|
return validBoxOrder;
|
|
}
|
|
|
|
/**
|
|
* This method saves all new items currently present in the GNOME Shell top
|
|
* bar to the settings.
|
|
*/
|
|
saveNewTopBarItems(): void {
|
|
// Only run, when the session mode is "user" or the parent session mode
|
|
// is "user".
|
|
if (Main.sessionMode.currentMode !== "user" && Main.sessionMode.parentMode !== "user") {
|
|
return;
|
|
}
|
|
|
|
// Get the box orders.
|
|
const boxOrders = {
|
|
left: this.#getBoxOrder("left"),
|
|
center: this.#getBoxOrder("center"),
|
|
right: this.#getBoxOrder("right"),
|
|
};
|
|
|
|
// Get roles (of items) currently present in the GNOME Shell top bar and
|
|
// index them using their associated indicator container.
|
|
let indicatorContainerRoleMap = new Map<St.Bin, string>();
|
|
for (const role in (Main.panel.statusArea as any)) {
|
|
const associatedIndicatorContainer = (Main.panel.statusArea as any)[role]?.container;
|
|
if (!(associatedIndicatorContainer instanceof St.Bin)) {
|
|
// TODO: maybe add logging
|
|
continue;
|
|
}
|
|
indicatorContainerRoleMap.set(associatedIndicatorContainer, role);
|
|
}
|
|
|
|
// Get the indicator containers (of the items) currently present in the
|
|
// GNOME Shell top bar boxes.
|
|
// They should be St.Bins (see link), so ensure that using a filter.
|
|
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/48.2/js/ui/panelMenu.js?ref_type=tags#L21
|
|
const boxIndicatorContainers = {
|
|
left: (Main.panel as CustomPanel)._leftBox.get_children().filter(ic => ic instanceof St.Bin),
|
|
center: (Main.panel as CustomPanel)._centerBox.get_children().filter(ic => ic instanceof St.Bin),
|
|
// Reverse this array, since the items in the left and center box
|
|
// are logically LTR, while the items in the right box are RTL.
|
|
right: (Main.panel as CustomPanel)._rightBox.get_children().filter(ic => ic instanceof St.Bin).reverse(),
|
|
};
|
|
|
|
// This function goes through the indicator containers of the given box
|
|
// and adds new item settings identifiers to the given box order.
|
|
const addNewItemSettingsIdsToBoxOrder = (indicatorContainers: St.Bin[], boxOrder: string[], box: Box) => {
|
|
for (const indicatorContainer of indicatorContainers) {
|
|
// First get the role associated with the current indicator
|
|
// container.
|
|
let role = indicatorContainerRoleMap.get(indicatorContainer);
|
|
if (!role) {
|
|
continue;
|
|
}
|
|
|
|
// Then get a settings identifier for the item.
|
|
let itemSettingsId;
|
|
if (role.startsWith("appindicator-")) {
|
|
// If the role indicates that the item is an
|
|
// AppIndicator/KStatusNotifierItem item, then handle it
|
|
// differently.
|
|
try {
|
|
itemSettingsId = this.#handleAppIndicatorItem(indicatorContainer, role);
|
|
} catch (e) {
|
|
if (!(e instanceof Error)) {
|
|
throw(e);
|
|
}
|
|
if (e.message !== "Application can't be determined.") {
|
|
throw(e);
|
|
}
|
|
continue;
|
|
}
|
|
} else if (role.startsWith("task-button-")) {
|
|
// If the role indicates that the item is a Task Up
|
|
// UltraLite item, then handle it differently.
|
|
itemSettingsId = this.#handleTaskUpUltraLiteItem(role);
|
|
} else { // Otherwise just use the role as the settings identifier.
|
|
itemSettingsId = role;
|
|
}
|
|
|
|
// Add the items settings identifier to the box order, if it
|
|
// isn't in in one already.
|
|
if (!boxOrders.left.includes(itemSettingsId)
|
|
&& !boxOrders.center.includes(itemSettingsId)
|
|
&& !boxOrders.right.includes(itemSettingsId)) {
|
|
if (box === "right") {
|
|
// Add the items to the beginning for this array, since
|
|
// its RTL.
|
|
boxOrder.unshift(itemSettingsId);
|
|
} else {
|
|
boxOrder.push(itemSettingsId);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
addNewItemSettingsIdsToBoxOrder(boxIndicatorContainers.left, boxOrders.left, "left");
|
|
addNewItemSettingsIdsToBoxOrder(boxIndicatorContainers.center, boxOrders.center, "center");
|
|
addNewItemSettingsIdsToBoxOrder(boxIndicatorContainers.right, boxOrders.right, "right");
|
|
|
|
this.#saveBoxOrder("left", boxOrders.left);
|
|
this.#saveBoxOrder("center", boxOrders.center);
|
|
this.#saveBoxOrder("right", boxOrders.right);
|
|
}
|
|
}
|