"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. } }