Top-Bar-Organizer/src/extension.ts

201 lines
8.4 KiB
TypeScript

"use strict";
import St from "gi://St"
import type Gio from "gi://Gio"
import * as Main from "resource:///org/gnome/shell/ui/main.js";
import * as Panel from "resource:///org/gnome/shell/ui/panel.js";
import { Extension } from "resource:///org/gnome/shell/extensions/extension.js";
import BoxOrderManager from "./extensionModules/BoxOrderManager.js";
import type { Box } from "./extensionModules/BoxOrderManager.js";
export interface CustomPanel extends Panel.Panel {
_leftBox: St.BoxLayout;
_centerBox: St.BoxLayout;
_rightBox: St.BoxLayout;
}
export default class TopBarOrganizerExtension extends Extension {
_settings!: Gio.Settings;
_boxOrderManager!: BoxOrderManager;
_settingsHandlerIds!: number[];
enable(): void {
this._settings = this.getSettings();
this._boxOrderManager = new BoxOrderManager({}, this._settings);
/// Stuff to do on startup(extension enable).
// Initially handle new top bar items and order top bar boxes.
this.#handleNewItemsAndOrderTopBar();
// Overwrite the `Panel._addToPanelBox` method with one handling new
// items.
this.#overwritePanelAddToPanelBox();
// Handle AppIndicators getting ready, to handle new AppIndicator items.
this._boxOrderManager.connect("appIndicatorReady", () => {
this.#handleNewItemsAndOrderTopBar();
});
// Handle changes of settings.
this._settingsHandlerIds = [];
const addSettingsChangeHandler = (settingsName: string) => {
const handlerId = this._settings.connect(`changed::${settingsName}`, () => {
this.#handleNewItemsAndOrderTopBar();
});
this._settingsHandlerIds.push(handlerId);
};
addSettingsChangeHandler("left-box-order");
addSettingsChangeHandler("center-box-order");
addSettingsChangeHandler("right-box-order");
addSettingsChangeHandler("hide");
addSettingsChangeHandler("show");
}
disable(): void {
// Revert the overwrite of `Panel._addToPanelBox`.
// @ts-ignore
Panel.Panel.prototype._addToPanelBox = Panel.Panel.prototype._originalAddToPanelBox;
// Set `Panel._originalAddToPanelBox` to `undefined`.
// @ts-ignore
Panel.Panel.prototype._originalAddToPanelBox = undefined;
// Disconnect signals.
for (const handlerId of this._settingsHandlerIds) {
this._settings.disconnect(handlerId);
}
this._boxOrderManager.disconnectSignals();
// @ts-ignore
this._settings = null;
// @ts-ignore
this._boxOrderManager = null;
}
////////////////////////////////////////////////////////////////////////////
/// Methods used on extension enable. ///
////////////////////////////////////////////////////////////////////////////
/**
* Overwrite `Panel._addToPanelBox` with a custom method, which simply calls
* the original one and handles new items and orders the top bar afterwards.
*/
#overwritePanelAddToPanelBox(): void {
// Add the original `Panel._addToPanelBox` method as
// `Panel._originalAddToPanelBox`.
// @ts-ignore
Panel.Panel.prototype._originalAddToPanelBox = Panel.Panel.prototype._addToPanelBox;
const handleNewItemsAndOrderTopBar = () => {
this.#handleNewItemsAndOrderTopBar();
};
// Overwrite `Panel._addToPanelBox`.
Panel.Panel.prototype._addToPanelBox = function(role, indicator, position, box) {
// Simply call the original `_addToPanelBox` and order the top bar
// and handle new items afterwards.
// @ts-ignore
this._originalAddToPanelBox(role, indicator, position, box);
handleNewItemsAndOrderTopBar();
};
}
////////////////////////////////////////////////////////////////////////////
/// Helper methods holding logic needed by other methods. ///
////////////////////////////////////////////////////////////////////////////
/**
* This method orders the top bar items of the specified box according to
* the configured box orders.
* @param {Box} box - The box to order.
*/
#orderTopBarItems(box: Box): 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 valid box order.
const validBoxOrder = this._boxOrderManager.getValidBoxOrder(box);
// Get the relevant box of `Main.panel`.
let panelBox = (Main.panel as CustomPanel)[`_${box}Box`];
/// Go through the items of the validBoxOrder and order the GNOME Shell
/// top bar box accordingly.
for (let i = 0; i < validBoxOrder.length; i++) {
const item = validBoxOrder[i];
// Get the indicator container associated with the current role.
const associatedIndicatorContainer = (Main.panel.statusArea as any)[item.role]?.container;
if (!(associatedIndicatorContainer instanceof St.Bin)) {
// TODO: maybe add logging
continue;
}
// Save whether or not the indicator container is visible.
const isVisible = associatedIndicatorContainer.visible;
const parent = associatedIndicatorContainer.get_parent();
if (parent !== null) {
parent.remove_child(associatedIndicatorContainer);
}
if (box === "right") {
// If the target panel box is the right panel box, insert the
// indicator container at index `-1`, which just adds it to the
// end (correct order is ensured, since `validBoxOrder` is
// sorted correctly and we're looping over it in order).
// This way unaccounted-for indicator containers will be at the
// left, which is preferred, since the box is logically
// right-to-left.
// The same applies for indicator containers, which are just
// temporarily unaccounted for (like for indicator containers of
// not yet ready app indicators), since them being at the right
// for a probably temporary stay causes all the indicator
// containers to shift.
panelBox.insert_child_at_index(associatedIndicatorContainer, -1);
} else {
panelBox.insert_child_at_index(associatedIndicatorContainer, i);
}
// Hide the indicator container...
// - ...if it wasn't visible before and the hide property of the
// item is "default".
// - if the hide property of the item is "hide".
// In all other cases have the item show.
// An e.g. screen recording indicator still wouldn't show tho, since
// this here acts on the indicator container, but a screen recording
// indicator is hidden on the indicator level.
if ((!isVisible && item.hide === "default") ||
item.hide === "hide") {
associatedIndicatorContainer.hide();
}
}
// To handle the case, where the box order got set to a permutation
// of an outdated box order, it would be wise, if the caller updated the
// box order now to include the items present in the top bar.
}
/**
* This method handles all new items currently present in the top bar and
* orders the items of all top bar boxes.
*/
#handleNewItemsAndOrderTopBar(): 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;
}
this._boxOrderManager.saveNewTopBarItems();
this.#orderTopBarItems("left");
this.#orderTopBarItems("center");
this.#orderTopBarItems("right");
// In `this.#orderTopBarItems` it says to update the box orders to
// include potentially new items, since the ordering might have been
// based on an outdated box order. However, since we already handle new
// top bar items at the beginning of this method, this isn't a concern.
}
}