Compare commits

...

1 Commits

Author SHA1 Message Date
June
9a6474b947
refactor: switch to TypeScript
Aside from introducing a bunch of type annotations and other code
adjustments, also add explicit type checking where necessary.

Inline #associateItem into the constructor in PrefsBoxOrderItemRow as
the method sets this.item and:
> Note that the field needs to be initialized in the constructor itself.
> TypeScript does not analyze methods you invoke from the constructor to
> detect initializations, because a derived class might override those
> methods and fail to initialize the members.
https://www.typescriptlang.org/docs/handbook/2/classes.html

Explicitly ensure we actually have a Gdk.Drag in #setupDNDScroll in
PrefsPage and explicitly only scroll when a DND operation is properly
set up. Even tho previously not having a Gdk.Drag in #setupDNDScroll
would probably just error out the callback and probably be just fine
then, handling this explicitly is at least nicer.

Also see the guide on using TypeScript for GNOME Shell Extensions, which
was followed for this work to some degree:
https://gjs.guide/extensions/development/typescript.html
2025-06-11 22:12:42 +02:00
15 changed files with 1198 additions and 194 deletions

View File

@ -6,7 +6,7 @@ insert_final_newline = true
indent_style = space indent_style = space
charset = utf-8 charset = utf-8
[*.{js,json}] [*.{js,ts,json}]
indent_size = 4 indent_size = 4
trim_trailing_whitespace = true trim_trailing_whitespace = true

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/node_modules/ /node_modules/
/dist/
top-bar-organizer@julian.gse.jsts.xyz.shell-extension.zip top-bar-organizer@julian.gse.jsts.xyz.shell-extension.zip

4
ambient.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
import "@girs/gjs"
import "@girs/gjs/dom"
import "@girs/gnome-shell/ambient"
import "@girs/gnome-shell/extensions/global"

1002
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@
"name": "top-bar-organizer", "name": "top-bar-organizer",
"version": "1.0.0", "version": "1.0.0",
"description": "A Gnome Shell Extension for organizing your Gnome Shell top bar.", "description": "A Gnome Shell Extension for organizing your Gnome Shell top bar.",
"type": "module",
"directories": { "directories": {
"doc": "docs" "doc": "docs"
}, },
@ -12,6 +13,12 @@
"author": "June", "author": "June",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"devDependencies": { "devDependencies": {
"eslint": "^8.50.0" "eslint": "^8.57.1",
"eslint-plugin-jsdoc": "^50.7.1",
"typescript": "^5.8.3"
},
"dependencies": {
"@girs/gjs": "^4.0.0-beta.23",
"@girs/gnome-shell": "^48.0.2"
} }
} }

View File

@ -4,7 +4,11 @@ set -e
REAL_BASE_DIR=$( dirname $( readlink -f "$0" )) REAL_BASE_DIR=$( dirname $( readlink -f "$0" ))
gnome-extensions pack "$REAL_BASE_DIR/src" \ rm -rf "$REAL_BASE_DIR/dist"
cd "$REAL_BASE_DIR"
npx tsc
cp "$REAL_BASE_DIR/src/metadata.json" "$REAL_BASE_DIR/dist/metadata.json"
gnome-extensions pack "$REAL_BASE_DIR/dist" \
--force \ --force \
--extra-source extensionModules \ --extra-source extensionModules \
--extra-source prefsModules \ --extra-source prefsModules \

View File

@ -1,13 +1,27 @@
"use strict"; "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 Main from "resource:///org/gnome/shell/ui/main.js";
import * as Panel from "resource:///org/gnome/shell/ui/panel.js"; import * as Panel from "resource:///org/gnome/shell/ui/panel.js";
import { Extension } from "resource:///org/gnome/shell/extensions/extension.js"; import { Extension } from "resource:///org/gnome/shell/extensions/extension.js";
import BoxOrderManager from "./extensionModules/BoxOrderManager.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 { export default class TopBarOrganizerExtension extends Extension {
enable() { _settings!: Gio.Settings;
_boxOrderManager!: BoxOrderManager;
_settingsHandlerIds!: number[];
enable(): void {
this._settings = this.getSettings(); this._settings = this.getSettings();
this._boxOrderManager = new BoxOrderManager({}, this._settings); this._boxOrderManager = new BoxOrderManager({}, this._settings);
@ -26,7 +40,7 @@ export default class TopBarOrganizerExtension extends Extension {
// Handle changes of settings. // Handle changes of settings.
this._settingsHandlerIds = []; this._settingsHandlerIds = [];
const addSettingsChangeHandler = (settingsName) => { const addSettingsChangeHandler = (settingsName: string) => {
const handlerId = this._settings.connect(`changed::${settingsName}`, () => { const handlerId = this._settings.connect(`changed::${settingsName}`, () => {
this.#handleNewItemsAndOrderTopBar(); this.#handleNewItemsAndOrderTopBar();
}); });
@ -39,10 +53,12 @@ export default class TopBarOrganizerExtension extends Extension {
addSettingsChangeHandler("show"); addSettingsChangeHandler("show");
} }
disable() { disable(): void {
// Revert the overwrite of `Panel._addToPanelBox`. // Revert the overwrite of `Panel._addToPanelBox`.
// @ts-ignore
Panel.Panel.prototype._addToPanelBox = Panel.Panel.prototype._originalAddToPanelBox; Panel.Panel.prototype._addToPanelBox = Panel.Panel.prototype._originalAddToPanelBox;
// Set `Panel._originalAddToPanelBox` to `undefined`. // Set `Panel._originalAddToPanelBox` to `undefined`.
// @ts-ignore
Panel.Panel.prototype._originalAddToPanelBox = undefined; Panel.Panel.prototype._originalAddToPanelBox = undefined;
// Disconnect signals. // Disconnect signals.
@ -51,7 +67,9 @@ export default class TopBarOrganizerExtension extends Extension {
} }
this._boxOrderManager.disconnectSignals(); this._boxOrderManager.disconnectSignals();
// @ts-ignore
this._settings = null; this._settings = null;
// @ts-ignore
this._boxOrderManager = null; this._boxOrderManager = null;
} }
@ -63,9 +81,10 @@ export default class TopBarOrganizerExtension extends Extension {
* Overwrite `Panel._addToPanelBox` with a custom method, which simply calls * Overwrite `Panel._addToPanelBox` with a custom method, which simply calls
* the original one and handles new items and orders the top bar afterwards. * the original one and handles new items and orders the top bar afterwards.
*/ */
#overwritePanelAddToPanelBox() { #overwritePanelAddToPanelBox(): void {
// Add the original `Panel._addToPanelBox` method as // Add the original `Panel._addToPanelBox` method as
// `Panel._originalAddToPanelBox`. // `Panel._originalAddToPanelBox`.
// @ts-ignore
Panel.Panel.prototype._originalAddToPanelBox = Panel.Panel.prototype._addToPanelBox; Panel.Panel.prototype._originalAddToPanelBox = Panel.Panel.prototype._addToPanelBox;
const handleNewItemsAndOrderTopBar = () => { const handleNewItemsAndOrderTopBar = () => {
@ -76,6 +95,7 @@ export default class TopBarOrganizerExtension extends Extension {
Panel.Panel.prototype._addToPanelBox = function(role, indicator, position, box) { Panel.Panel.prototype._addToPanelBox = function(role, indicator, position, box) {
// Simply call the original `_addToPanelBox` and order the top bar // Simply call the original `_addToPanelBox` and order the top bar
// and handle new items afterwards. // and handle new items afterwards.
// @ts-ignore
this._originalAddToPanelBox(role, indicator, position, box); this._originalAddToPanelBox(role, indicator, position, box);
handleNewItemsAndOrderTopBar(); handleNewItemsAndOrderTopBar();
}; };
@ -88,9 +108,9 @@ export default class TopBarOrganizerExtension extends Extension {
/** /**
* This method orders the top bar items of the specified box according to * This method orders the top bar items of the specified box according to
* the configured box orders. * the configured box orders.
* @param {string} box - The box to order. * @param {Box} box - The box to order.
*/ */
#orderTopBarItems(box) { #orderTopBarItems(box: Box): void {
// Only run, when the session mode is "user" or the parent session mode // Only run, when the session mode is "user" or the parent session mode
// is "user". // is "user".
if(Main.sessionMode.currentMode !== "user" && Main.sessionMode.parentMode !== "user") { if(Main.sessionMode.currentMode !== "user" && Main.sessionMode.parentMode !== "user") {
@ -104,13 +124,13 @@ export default class TopBarOrganizerExtension extends Extension {
let panelBox; let panelBox;
switch (box) { switch (box) {
case "left": case "left":
panelBox = Main.panel._leftBox; panelBox = (Main.panel as CustomPanel)._leftBox;
break; break;
case "center": case "center":
panelBox = Main.panel._centerBox; panelBox = (Main.panel as CustomPanel)._centerBox;
break; break;
case "right": case "right":
panelBox = Main.panel._rightBox; panelBox = (Main.panel as CustomPanel)._rightBox;
break; break;
} }
@ -119,12 +139,19 @@ export default class TopBarOrganizerExtension extends Extension {
for (let i = 0; i < validBoxOrder.length; i++) { for (let i = 0; i < validBoxOrder.length; i++) {
const item = validBoxOrder[i]; const item = validBoxOrder[i];
// Get the indicator container associated with the current role. // Get the indicator container associated with the current role.
const associatedIndicatorContainer = Main.panel.statusArea[item.role].container; 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. // Save whether or not the indicator container is visible.
const isVisible = associatedIndicatorContainer.visible; const isVisible = associatedIndicatorContainer.visible;
associatedIndicatorContainer.get_parent().remove_child(associatedIndicatorContainer); const parent = associatedIndicatorContainer.get_parent();
if (parent !== null) {
parent.remove_child(associatedIndicatorContainer);
}
if (box === "right") { if (box === "right") {
// If the target panel box is the right panel box, insert the // If the target panel box is the right panel box, insert the
// indicator container at index `-1`, which just adds it to the // indicator container at index `-1`, which just adds it to the
@ -165,7 +192,7 @@ export default class TopBarOrganizerExtension extends Extension {
* This method handles all new items currently present in the top bar and * This method handles all new items currently present in the top bar and
* orders the items of all top bar boxes. * orders the items of all top bar boxes.
*/ */
#handleNewItemsAndOrderTopBar() { #handleNewItemsAndOrderTopBar(): void {
// Only run, when the session mode is "user" or the parent session mode // Only run, when the session mode is "user" or the parent session mode
// is "user". // is "user".
if(Main.sessionMode.currentMode !== "user" && Main.sessionMode.parentMode !== "user") { if(Main.sessionMode.currentMode !== "user" && Main.sessionMode.parentMode !== "user") {

View File

@ -1,18 +1,24 @@
"use strict"; "use strict";
import GObject from "gi://GObject"; 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 * 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 * A resolved box order item containing the items role, settings identifier and
* additional information. * additional information.
* @typedef {Object} ResolvedBoxOrderItem
* @property {string} settingsId - The settings identifier of the item.
* @property {string} role - The role of the item.
* @property {string} hide - Whether the item should be (forcefully) hidden
* (hide), shown (show) or just be left as is (default).
*/ */
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. * This class provides an interfaces to the box orders stored in settings.
@ -30,12 +36,15 @@ export default class BoxOrderManager extends GObject.Object {
}, this); }, this);
} }
#appIndicatorReadyHandlerIdMap; // Can't have type guarantees here, since this is working with types from
#appIndicatorItemSettingsIdToRolesMap; // the KStatusNotifier/AppIndicator extension.
#taskUpUltraLiteItemRoles; #appIndicatorReadyHandlerIdMap: Map<any, any>;
#settings; #appIndicatorItemSettingsIdToRolesMap: Map<string, string[]>;
#taskUpUltraLiteItemRoles: string[];
#settings: Gio.Settings;
constructor(params = {}, settings) { constructor(params = {}, settings: Gio.Settings) {
// @ts-ignore Params should be passed, see: https://gjs.guide/guides/gobject/subclassing.html#subclassing-gobject
super(params); super(params);
this.#appIndicatorReadyHandlerIdMap = new Map(); this.#appIndicatorReadyHandlerIdMap = new Map();
@ -47,30 +56,22 @@ export default class BoxOrderManager extends GObject.Object {
/** /**
* Gets a box order for the given top bar box from settings. * Gets a box order for the given top bar box from settings.
* @param {string} box - The top bar box for which to get the box order. * @param {Box} box - The top bar box for which to get the box order.
* Must be one of the following values:
* - "left"
* - "center"
* - "right"
* @returns {string[]} - The box order consisting of an array of item * @returns {string[]} - The box order consisting of an array of item
* settings identifiers. * settings identifiers.
*/ */
#getBoxOrder(box) { #getBoxOrder(box: Box): string[] {
return this.#settings.get_strv(`${box}-box-order`); return this.#settings.get_strv(`${box}-box-order`);
} }
/** /**
* Save the given box order to settings, making sure to only save a changed * Save the given box order to settings, making sure to only save a changed
* box order, to avoid loops when listening on settings changes. * box order, to avoid loops when listening on settings changes.
* @param {string} box - The top bar box for which to save the box order. * @param {Box} box - The top bar box for which to save the box order.
* Must be one of the following values:
* - "left"
* - "center"
* - "right"
* @param {string[]} boxOrder - The box order to save. Must be an array of * @param {string[]} boxOrder - The box order to save. Must be an array of
* item settings identifiers. * item settings identifiers.
*/ */
#saveBoxOrder(box, boxOrder) { #saveBoxOrder(box: Box, boxOrder: string[]): void {
const currentBoxOrder = this.#getBoxOrder(box); const currentBoxOrder = this.#getBoxOrder(box);
// Only save the given box order to settings, if it is different, to // Only save the given box order to settings, if it is different, to
@ -90,14 +91,18 @@ export default class BoxOrderManager extends GObject.Object {
* then also makes sure that once the app indicators "ready" signal emits, * then also makes sure that once the app indicators "ready" signal emits,
* this classes "appIndicatorReady" signal emits as well, such that it and * this classes "appIndicatorReady" signal emits as well, such that it and
* other methods can be called again to properly handle the item. * other methods can be called again to properly handle the item.
* @param {string} indicatorContainer - The container of the indicator of the * @param {St.Bin} indicatorContainer - The container of the indicator of the
* AppIndicator/KStatusNotifierItem item. * AppIndicator/KStatusNotifierItem item.
* @param {string} role - The role of the AppIndicator/KStatusNotifierItem * @param {string} role - The role of the AppIndicator/KStatusNotifierItem
* item. * item.
* @returns {string} The derived items settings identifier. * @returns {string} The derived items settings identifier.
*/ */
#handleAppIndicatorItem(indicatorContainer, role) { #handleAppIndicatorItem(indicatorContainer: St.Bin, role: string): string {
const appIndicator = indicatorContainer.get_child()._indicator; // 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; let application = appIndicator.id;
if (!application && this.#appIndicatorReadyHandlerIdMap) { if (!application && this.#appIndicatorReadyHandlerIdMap) {
@ -146,7 +151,7 @@ export default class BoxOrderManager extends GObject.Object {
* @param {string} role - The role of the Task Up UltraLite item. * @param {string} role - The role of the Task Up UltraLite item.
* @returns {string} The settings identifier to use. * @returns {string} The settings identifier to use.
*/ */
#handleTaskUpUltraLiteItem(role) { #handleTaskUpUltraLiteItem(role: string): string {
const roles = this.#taskUpUltraLiteItemRoles; const roles = this.#taskUpUltraLiteItemRoles;
if (!roles.includes(role)) { if (!roles.includes(role)) {
@ -162,14 +167,10 @@ export default class BoxOrderManager extends GObject.Object {
* meaning they might be present multiple times or not at all depending on * meaning they might be present multiple times or not at all depending on
* the roles stored. * the roles stored.
* The items of the box order also have additional information stored. * The items of the box order also have additional information stored.
* @param {string} box - The top bar box for which to get the resolved box order. * @param {Box} box - The top bar box for which to get the resolved box order.
* Must be one of the following values:
* - "left"
* - "center"
* - "right"
* @returns {ResolvedBoxOrderItem[]} - The resolved box order. * @returns {ResolvedBoxOrderItem[]} - The resolved box order.
*/ */
#getResolvedBoxOrder(box) { #getResolvedBoxOrder(box: Box): ResolvedBoxOrderItem[] {
let boxOrder = this.#getBoxOrder(box); let boxOrder = this.#getBoxOrder(box);
const itemsToHide = this.#settings.get_strv("hide"); const itemsToHide = this.#settings.get_strv("hide");
@ -207,15 +208,15 @@ export default class BoxOrderManager extends GObject.Object {
// the item specially. // the item specially.
// Get the roles associated with the items settings id. // Get the roles associated with the items settings id.
let roles = []; let roles: string[] = [];
if (itemSettingsId.startsWith("appindicator-kstatusnotifieritem-")) { if (itemSettingsId.startsWith("appindicator-kstatusnotifieritem-")) {
roles = this.#appIndicatorItemSettingsIdToRolesMap.get(resolvedBoxOrderItem.settingsId); roles = this.#appIndicatorItemSettingsIdToRolesMap.get(resolvedBoxOrderItem.settingsId) ?? [];
} else if (itemSettingsId === "item-role-group-task-up-ultralite") { } else if (itemSettingsId === "item-role-group-task-up-ultralite") {
roles = this.#taskUpUltraLiteItemRoles; roles = this.#taskUpUltraLiteItemRoles;
} }
// If there are no roles associated, continue. // If there are no roles associated, continue.
if (!roles) { if (roles.length === 0) {
continue; continue;
} }
@ -236,12 +237,13 @@ export default class BoxOrderManager extends GObject.Object {
* This is typically used before nulling an instance of this class to make * This is typically used before nulling an instance of this class to make
* sure all signals are disconnected. * sure all signals are disconnected.
*/ */
disconnectSignals() { disconnectSignals(): void {
for (const [handlerId, appIndicator] of this.#appIndicatorReadyHandlerIdMap) { for (const [handlerId, appIndicator] of this.#appIndicatorReadyHandlerIdMap) {
if (handlerId && appIndicator?.signalHandlerIsConnected(handlerId)) { if (handlerId && appIndicator?.signalHandlerIsConnected(handlerId)) {
appIndicator.disconnect(handlerId); appIndicator.disconnect(handlerId);
} }
} }
// @ts-ignore
this.#appIndicatorReadyHandlerIdMap = null; this.#appIndicatorReadyHandlerIdMap = null;
} }
@ -250,25 +252,23 @@ export default class BoxOrderManager extends GObject.Object {
* and Task Up UltraLite items got resolved and where only items are * and Task Up UltraLite items got resolved and where only items are
* included, which are in some GNOME Shell top bar box. * included, which are in some GNOME Shell top bar box.
* The items of the box order also have additional information stored. * The items of the box order also have additional information stored.
* @param {string} box - The top bar box to return the valid box order for. * @param {Box} box - The top bar box to return the valid box order for.
* Must be one of the following values:
* - "left"
* - "center"
* - "right"
* @returns {ResolvedBoxOrderItem[]} - The valid box order. * @returns {ResolvedBoxOrderItem[]} - The valid box order.
*/ */
getValidBoxOrder(box) { getValidBoxOrder(box: Box): ResolvedBoxOrderItem[] {
// Get a resolved box order. // Get a resolved box order.
let resolvedBoxOrder = this.#getResolvedBoxOrder(box); let resolvedBoxOrder = this.#getResolvedBoxOrder(box);
// ToDo: simplify. // ToDo: simplify.
// Get the indicator containers (of the items) currently present in the // Get the indicator containers (of the items) currently present in the
// GNOME Shell top bar. // 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 = [ const indicatorContainers = [
Main.panel._leftBox.get_children(), (Main.panel as CustomPanel)._leftBox.get_children(),
Main.panel._centerBox.get_children(), (Main.panel as CustomPanel)._centerBox.get_children(),
Main.panel._rightBox.get_children(), (Main.panel as CustomPanel)._rightBox.get_children(),
].flat(); ].flat().filter(ic => ic instanceof St.Bin);
// Create an indicator containers set from the indicator containers for // Create an indicator containers set from the indicator containers for
// fast easy access. // fast easy access.
@ -277,10 +277,13 @@ export default class BoxOrderManager extends GObject.Object {
// Go through the resolved box order and only add items to the valid box // 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 // order, where their indicator is currently present in the GNOME Shell
// top bar. // top bar.
let validBoxOrder = []; let validBoxOrder: ResolvedBoxOrderItem[] = [];
for (const item of resolvedBoxOrder) { for (const item of resolvedBoxOrder) {
// Get the indicator container associated with the items role. const associatedIndicatorContainer = (Main.panel.statusArea as any)[item.role]?.container;
const associatedIndicatorContainer = Main.panel.statusArea[item.role]?.container; if (!(associatedIndicatorContainer instanceof St.Bin)) {
// TODO: maybe add logging
continue;
}
if (indicatorContainerSet.has(associatedIndicatorContainer)) { if (indicatorContainerSet.has(associatedIndicatorContainer)) {
validBoxOrder.push(item); validBoxOrder.push(item);
@ -294,7 +297,7 @@ export default class BoxOrderManager extends GObject.Object {
* This method saves all new items currently present in the GNOME Shell top * This method saves all new items currently present in the GNOME Shell top
* bar to the settings. * bar to the settings.
*/ */
saveNewTopBarItems() { saveNewTopBarItems(): void {
// Only run, when the session mode is "user" or the parent session mode // Only run, when the session mode is "user" or the parent session mode
// is "user". // is "user".
if (Main.sessionMode.currentMode !== "user" && Main.sessionMode.parentMode !== "user") { if (Main.sessionMode.currentMode !== "user" && Main.sessionMode.parentMode !== "user") {
@ -310,24 +313,31 @@ export default class BoxOrderManager extends GObject.Object {
// Get roles (of items) currently present in the GNOME Shell top bar and // Get roles (of items) currently present in the GNOME Shell top bar and
// index them using their associated indicator container. // index them using their associated indicator container.
let indicatorContainerRoleMap = new Map(); let indicatorContainerRoleMap = new Map<St.Bin, string>();
for (const role in Main.panel.statusArea) { for (const role in (Main.panel.statusArea as any)) {
indicatorContainerRoleMap.set(Main.panel.statusArea[role].container, role); 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 // Get the indicator containers (of the items) currently present in the
// GNOME Shell top bar boxes. // 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 = { const boxIndicatorContainers = {
left: Main.panel._leftBox.get_children(), left: (Main.panel as CustomPanel)._leftBox.get_children().filter(ic => ic instanceof St.Bin),
center: Main.panel._centerBox.get_children(), 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 // Reverse this array, since the items in the left and center box
// are logically LTR, while the items in the right box are RTL. // are logically LTR, while the items in the right box are RTL.
right: Main.panel._rightBox.get_children().reverse(), 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 // This function goes through the indicator containers of the given box
// and adds new item settings identifiers to the given box order. // and adds new item settings identifiers to the given box order.
const addNewItemSettingsIdsToBoxOrder = (indicatorContainers, boxOrder, box) => { const addNewItemSettingsIdsToBoxOrder = (indicatorContainers: St.Bin[], boxOrder: string[], box: Box) => {
for (const indicatorContainer of indicatorContainers) { for (const indicatorContainer of indicatorContainers) {
// First get the role associated with the current indicator // First get the role associated with the current indicator
// container. // container.
@ -345,6 +355,9 @@ export default class BoxOrderManager extends GObject.Object {
try { try {
itemSettingsId = this.#handleAppIndicatorItem(indicatorContainer, role); itemSettingsId = this.#handleAppIndicatorItem(indicatorContainer, role);
} catch (e) { } catch (e) {
if (!(e instanceof Error)) {
throw(e);
}
if (e.message !== "Application can't be determined.") { if (e.message !== "Application can't be determined.") {
throw(e); throw(e);
} }

View File

@ -13,7 +13,7 @@ export default class TopBarOrganizerPreferences extends ExtensionPreferences {
provider.load_from_path(this.metadata.dir.get_path() + "/css/prefs.css"); provider.load_from_path(this.metadata.dir.get_path() + "/css/prefs.css");
const defaultGdkDisplay = Gdk.Display.get_default(); const defaultGdkDisplay = Gdk.Display.get_default();
Gtk.StyleContext.add_provider_for_display( Gtk.StyleContext.add_provider_for_display(
defaultGdkDisplay, (defaultGdkDisplay as Gdk.Display),
provider, provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
); );
@ -22,7 +22,7 @@ export default class TopBarOrganizerPreferences extends ExtensionPreferences {
prefsPage.connect("destroy", () => { prefsPage.connect("destroy", () => {
Gtk.StyleContext.remove_provider_for_display( Gtk.StyleContext.remove_provider_for_display(
defaultGdkDisplay, (defaultGdkDisplay as Gdk.Display),
provider provider
); );
}); });

View File

@ -6,6 +6,8 @@ import GObject from "gi://GObject";
import Adw from "gi://Adw"; import Adw from "gi://Adw";
import GLib from "gi://GLib"; import GLib from "gi://GLib";
import type PrefsBoxOrderListBox from "./PrefsBoxOrderListBox.js";
export default class PrefsBoxOrderItemRow extends Adw.ActionRow { export default class PrefsBoxOrderItemRow extends Adw.ActionRow {
static { static {
GObject.registerClass({ GObject.registerClass({
@ -18,8 +20,8 @@ export default class PrefsBoxOrderItemRow extends Adw.ActionRow {
}, },
}, this); }, this);
this.install_action("row.forget", null, (self, _actionName, _param) => { this.install_action("row.forget", null, (self, _actionName, _param) => {
const parentListBox = self.get_parent(); const parentListBox = self.get_parent() as PrefsBoxOrderListBox;
parentListBox.removeRow(self); parentListBox.removeRow(self as PrefsBoxOrderItemRow);
parentListBox.saveBoxOrderToSettings(); parentListBox.saveBoxOrderToSettings();
parentListBox.determineRowMoveActionEnable(); parentListBox.determineRowMoveActionEnable();
}); });
@ -27,39 +29,32 @@ export default class PrefsBoxOrderItemRow extends Adw.ActionRow {
this.install_action("row.move-down", null, (self, _actionName, _param) => self.emit("move", "down")); this.install_action("row.move-down", null, (self, _actionName, _param) => self.emit("move", "down"));
} }
#drag_starting_point_x; item: string;
#drag_starting_point_y; #drag_starting_point_x?: number;
#drag_starting_point_y?: number;
constructor(params = {}, item) { constructor(params = {}, item: string) {
super(params); super(params);
this.#associateItem(item); // Associate `this` with an item.
}
/**
* Associate `this` with an item.
* @param {String} item
*/
#associateItem(item) {
this.item = item; this.item = item;
if (this.item.startsWith("appindicator-kstatusnotifieritem-")) {
if (item.startsWith("appindicator-kstatusnotifieritem-")) {
// Set the title to something nicer, if the associated item is an // Set the title to something nicer, if the associated item is an
// AppIndicator/KStatusNotifierItem item. // AppIndicator/KStatusNotifierItem item.
this.set_title(item.replace("appindicator-kstatusnotifieritem-", "")); this.set_title(this.item.replace("appindicator-kstatusnotifieritem-", ""));
} else if (item === "item-role-group-task-up-ultralite") { } else if (this.item === "item-role-group-task-up-ultralite") {
// Set the title to something nicer, if the item in question is the // Set the title to something nicer, if the item in question is the
// Task Up UltraLite item role group. // Task Up UltraLite item role group.
this.set_title("Task Up UltraLite Items"); this.set_title("Task Up UltraLite Items");
} else { } else {
// Otherwise just set it to `item`. // Otherwise just set it to `item`.
this.set_title(item); this.set_title(this.item);
} }
} }
onDragPrepare(_source, x, y) { onDragPrepare(_source: Gtk.DragSource, x: number, y: number): Gdk.ContentProvider {
const value = new GObject.Value(); const value = new GObject.Value();
value.init(PrefsBoxOrderItemRow); value.init(PrefsBoxOrderItemRow.$gtype);
value.set_object(this); value.set_object(this);
this.#drag_starting_point_x = x; this.#drag_starting_point_x = x;
@ -67,7 +62,7 @@ export default class PrefsBoxOrderItemRow extends Adw.ActionRow {
return Gdk.ContentProvider.new_for_value(value); return Gdk.ContentProvider.new_for_value(value);
} }
onDragBegin(_source, drag) { onDragBegin(_source: Gtk.DragSource, drag: Gdk.Drag): void {
let dragWidget = new Gtk.ListBox(); let dragWidget = new Gtk.ListBox();
let allocation = this.get_allocation(); let allocation = this.get_allocation();
dragWidget.set_size_request(allocation.width, allocation.height); dragWidget.set_size_request(allocation.width, allocation.height);
@ -78,20 +73,32 @@ export default class PrefsBoxOrderItemRow extends Adw.ActionRow {
let currentDragIcon = Gtk.DragIcon.get_for_drag(drag); let currentDragIcon = Gtk.DragIcon.get_for_drag(drag);
currentDragIcon.set_child(dragWidget); currentDragIcon.set_child(dragWidget);
// Even tho this should always be the case, ensure the values for the hotspot aren't undefined.
if (typeof this.#drag_starting_point_x !== "undefined" &&
typeof this.#drag_starting_point_y !== "undefined") {
drag.set_hotspot(this.#drag_starting_point_x, this.#drag_starting_point_y); drag.set_hotspot(this.#drag_starting_point_x, this.#drag_starting_point_y);
} }
}
// Handle a new drop on `this` properly. // Handle a new drop on `this` properly.
// `value` is the thing getting dropped. // `value` is the thing getting dropped.
onDrop(_target, value, _x, _y) { onDrop(_target: Gtk.DropTarget, value: any, _x: number, _y: number): boolean {
// According to the type annotations of Gtk.DropTarget, value is of type
// GObject.Value, so ensure the one we work with is of type
// PrefsBoxOrderItemRow.
if (!(value instanceof PrefsBoxOrderItemRow)) {
// TODO: maybe add logging
return false;
}
// If `this` got dropped onto itself, do nothing. // If `this` got dropped onto itself, do nothing.
if (value === this) { if (value === this) {
return false; return false;
} }
// Get the GtkListBoxes of `this` and the drop value. // Get the GtkListBoxes of `this` and the drop value.
const ownListBox = this.get_parent(); const ownListBox = this.get_parent() as PrefsBoxOrderListBox;
const valueListBox = value.get_parent(); const valueListBox = value.get_parent() as PrefsBoxOrderListBox;
// Get the position of `this` and the drop value. // Get the position of `this` and the drop value.
const ownPosition = this.get_index(); const ownPosition = this.get_index();

View File

@ -3,6 +3,7 @@
import Gtk from "gi://Gtk"; import Gtk from "gi://Gtk";
import GObject from "gi://GObject"; import GObject from "gi://GObject";
import GLib from "gi://GLib"; import GLib from "gi://GLib";
import type Gio from "gi://Gio";
import { ExtensionPreferences } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js"; import { ExtensionPreferences } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";
@ -25,14 +26,15 @@ export default class PrefsBoxOrderListBox extends Gtk.ListBox {
}, },
Signals: { Signals: {
"row-move": { "row-move": {
param_types: [PrefsBoxOrderItemRow, GObject.TYPE_STRING], param_types: [PrefsBoxOrderItemRow.$gtype, GObject.TYPE_STRING],
}, },
}, },
}, this); }, this);
} }
#settings; _boxOrder!: string;
#rowSignalHandlerIds = new Map(); #settings: Gio.Settings;
#rowSignalHandlerIds = new Map<PrefsBoxOrderItemRow, number[]>();
/** /**
* @param {Object} params * @param {Object} params
@ -41,18 +43,18 @@ export default class PrefsBoxOrderListBox extends Gtk.ListBox {
super(params); super(params);
// Load the settings. // Load the settings.
this.#settings = ExtensionPreferences.lookupByURL(import.meta.url).getSettings(); this.#settings = ExtensionPreferences.lookupByURL(import.meta.url)!.getSettings();
// Add a placeholder widget for the case, where no GtkListBoxRows are // Add a placeholder widget for the case, where no GtkListBoxRows are
// present. // present.
this.set_placeholder(new PrefsBoxOrderListEmptyPlaceholder()); this.set_placeholder(new PrefsBoxOrderListEmptyPlaceholder());
} }
get boxOrder() { get boxOrder(): string {
return this._boxOrder; return this._boxOrder;
} }
set boxOrder(value) { set boxOrder(value: string) {
this._boxOrder = value; this._boxOrder = value;
// Get the actual box order for the given box order name from settings. // Get the actual box order for the given box order name from settings.
@ -73,10 +75,10 @@ export default class PrefsBoxOrderListBox extends Gtk.ListBox {
* position. * position.
* Also handles stuff like connecting signals. * Also handles stuff like connecting signals.
*/ */
insertRow(row, position) { insertRow(row: PrefsBoxOrderItemRow, position: number): void {
this.insert(row, position); this.insert(row, position);
const signalHandlerIds = []; const signalHandlerIds: number[] = [];
signalHandlerIds.push(row.connect("move", (row, direction) => { signalHandlerIds.push(row.connect("move", (row, direction) => {
this.emit("row-move", row, direction); this.emit("row-move", row, direction);
})); }));
@ -88,8 +90,8 @@ export default class PrefsBoxOrderListBox extends Gtk.ListBox {
* Removes the given PrefsBoxOrderItemRow from this list box. * Removes the given PrefsBoxOrderItemRow from this list box.
* Also handles stuff like disconnecting signals. * Also handles stuff like disconnecting signals.
*/ */
removeRow(row) { removeRow(row: PrefsBoxOrderItemRow): void {
const signalHandlerIds = this.#rowSignalHandlerIds.get(row); const signalHandlerIds = this.#rowSignalHandlerIds.get(row) ?? [];
for (const id of signalHandlerIds) { for (const id of signalHandlerIds) {
row.disconnect(id); row.disconnect(id);
@ -102,11 +104,11 @@ export default class PrefsBoxOrderListBox extends Gtk.ListBox {
* Saves the box order represented by `this` (and its * Saves the box order represented by `this` (and its
* `PrefsBoxOrderItemRows`) to settings. * `PrefsBoxOrderItemRows`) to settings.
*/ */
saveBoxOrderToSettings() { saveBoxOrderToSettings(): void {
let currentBoxOrder = []; let currentBoxOrder: string[] = [];
for (let potentialPrefsBoxOrderItemRow of this) { for (let potentialPrefsBoxOrderItemRow of this) {
// Only process PrefsBoxOrderItemRows. // Only process PrefsBoxOrderItemRows.
if (potentialPrefsBoxOrderItemRow.constructor.$gtype.name !== "PrefsBoxOrderItemRow") { if (!(potentialPrefsBoxOrderItemRow instanceof PrefsBoxOrderItemRow)) {
continue; continue;
} }
@ -120,10 +122,10 @@ export default class PrefsBoxOrderListBox extends Gtk.ListBox {
* Determines whether or not each move action of each PrefsBoxOrderItemRow * Determines whether or not each move action of each PrefsBoxOrderItemRow
* should be enabled or disabled. * should be enabled or disabled.
*/ */
determineRowMoveActionEnable() { determineRowMoveActionEnable(): void {
for (let potentialPrefsBoxOrderItemRow of this) { for (let potentialPrefsBoxOrderItemRow of this) {
// Only process PrefsBoxOrderItemRows. // Only process PrefsBoxOrderItemRows.
if (potentialPrefsBoxOrderItemRow.constructor.$gtype.name !== "PrefsBoxOrderItemRow") { if (!(potentialPrefsBoxOrderItemRow instanceof PrefsBoxOrderItemRow)) {
continue; continue;
} }

View File

@ -4,6 +4,9 @@ import Gtk from "gi://Gtk";
import GObject from "gi://GObject"; import GObject from "gi://GObject";
import GLib from "gi://GLib"; import GLib from "gi://GLib";
import PrefsBoxOrderItemRow from "./PrefsBoxOrderItemRow.js";
import type PrefsBoxOrderListBox from "./PrefsBoxOrderListBox.js";
export default class PrefsBoxOrderListEmptyPlaceholder extends Gtk.Box { export default class PrefsBoxOrderListEmptyPlaceholder extends Gtk.Box {
static { static {
GObject.registerClass({ GObject.registerClass({
@ -14,10 +17,18 @@ export default class PrefsBoxOrderListEmptyPlaceholder extends Gtk.Box {
// Handle a new drop on `this` properly. // Handle a new drop on `this` properly.
// `value` is the thing getting dropped. // `value` is the thing getting dropped.
onDrop(_target, value, _x, _y) { onDrop(_target: Gtk.DropTarget, value: any, _x: number, _y: number): boolean {
// According to the type annotations of Gtk.DropTarget, value is of type
// GObject.Value, so ensure the one we work with is of type
// PrefsBoxOrderItemRow.
if (!(value instanceof PrefsBoxOrderItemRow)) {
// TODO: maybe add logging
return false;
}
// Get the GtkListBoxes of `this` and the drop value. // Get the GtkListBoxes of `this` and the drop value.
const ownListBox = this.get_parent(); const ownListBox = this.get_parent() as PrefsBoxOrderListBox;
const valueListBox = value.get_parent(); const valueListBox = value.get_parent() as PrefsBoxOrderListBox;
// Remove the drop value from its list box. // Remove the drop value from its list box.
valueListBox.removeRow(value); valueListBox.removeRow(value);

View File

@ -1,5 +1,6 @@
"use strict"; "use strict";
import Gdk from "gi://Gdk";
import Gtk from "gi://Gtk"; import Gtk from "gi://Gtk";
import GObject from "gi://GObject"; import GObject from "gi://GObject";
import Adw from "gi://Adw"; import Adw from "gi://Adw";
@ -7,6 +8,7 @@ import GLib from "gi://GLib";
import ScrollManager from "./ScrollManager.js"; import ScrollManager from "./ScrollManager.js";
import PrefsBoxOrderListEmptyPlaceholder from "./PrefsBoxOrderListEmptyPlaceholder.js"; import PrefsBoxOrderListEmptyPlaceholder from "./PrefsBoxOrderListEmptyPlaceholder.js";
import type PrefsBoxOrderItemRow from "./PrefsBoxOrderItemRow.js";
// Imports to make UI file work. // Imports to make UI file work.
// eslint-disable-next-line // eslint-disable-next-line
@ -25,6 +27,11 @@ export default class PrefsPage extends Adw.PreferencesPage {
}, this); }, this);
} }
_dndEnded?: boolean;
_left_box_order_list_box!: PrefsBoxOrderListBox;
_center_box_order_list_box!: PrefsBoxOrderListBox;
_right_box_order_list_box!: PrefsBoxOrderListBox;
constructor(params = {}) { constructor(params = {}) {
super(params); super(params);
@ -37,23 +44,27 @@ export default class PrefsPage extends Adw.PreferencesPage {
* operation is in progress and the user has their cursor either in the * operation is in progress and the user has their cursor either in the
* upper or lower 10% of this widget respectively. * upper or lower 10% of this widget respectively.
*/ */
#setupDNDScroll() { #setupDNDScroll(): void {
// Pass `this.get_first_child()` to the ScrollManager, since this // Pass `this.get_first_child()` to the ScrollManager, since this
// `PrefsPage` extends an `Adw.PreferencesPage` and the first child of // `PrefsPage` extends an `Adw.PreferencesPage` and the first child of
// an `Adw.PreferencesPage` is the built-in `Gtk.ScrolledWindow`. // an `Adw.PreferencesPage` is the built-in `Gtk.ScrolledWindow`.
const scrollManager = new ScrollManager(this.get_first_child()); const scrollManager = new ScrollManager(this.get_first_child() as Gtk.ScrolledWindow);
/// Setup GtkDropControllerMotion event controller and make use of its /// Setup GtkDropControllerMotion event controller and make use of its
/// events. /// events.
let controller = new Gtk.DropControllerMotion(); let controller = new Gtk.DropControllerMotion();
// Scroll, when the pointer is in the right places. // Make sure scrolling stops, when DND operation ends.
this._dndEnded = true;
// Scroll, when the pointer is in the right places and a DND operation
// is properly set up (this._dndEnded is false).
controller.connect("motion", (_, _x, y) => { controller.connect("motion", (_, _x, y) => {
if (y <= this.get_allocated_height() * 0.1) { if ((y <= this.get_allocated_height() * 0.1) && !this._dndEnded) {
// If the pointer is currently in the upper ten percent of this // If the pointer is currently in the upper ten percent of this
// widget, then scroll up. // widget, then scroll up.
scrollManager.startScrollUp(); scrollManager.startScrollUp();
} else if (y >= this.get_allocated_height() * 0.9) { } else if ((y >= this.get_allocated_height() * 0.9) && !this._dndEnded) {
// If the pointer is currently in the lower ten percent of this // If the pointer is currently in the lower ten percent of this
// widget, then scroll down. // widget, then scroll down.
scrollManager.startScrollDown(); scrollManager.startScrollDown();
@ -63,11 +74,9 @@ export default class PrefsPage extends Adw.PreferencesPage {
} }
}); });
// Make sure scrolling stops, when DND operation ends.
this._dndEnded = true;
const stopScrollAllAtDNDEnd = () => { const stopScrollAllAtDNDEnd = () => {
scrollManager.stopScrollAll();
this._dndEnded = true; this._dndEnded = true;
scrollManager.stopScrollAll();
}; };
controller.connect("leave", () => { controller.connect("leave", () => {
stopScrollAllAtDNDEnd(); stopScrollAllAtDNDEnd();
@ -76,7 +85,14 @@ export default class PrefsPage extends Adw.PreferencesPage {
// Make use of `this._dndEnded` to setup stopScrollAtDNDEnd only // Make use of `this._dndEnded` to setup stopScrollAtDNDEnd only
// once per DND operation. // once per DND operation.
if (this._dndEnded) { if (this._dndEnded) {
let drag = controller.get_drop().get_drag(); const drag = controller.get_drop()?.get_drag() ?? null;
// Ensure we have a Gdk.Drag.
// If this is not the case for whatever reason, then don't start
// DND scrolling and just return.
if (!(drag instanceof Gdk.Drag)) {
// TODO: maybe add logging
return;
}
drag.connect("drop-performed", () => { drag.connect("drop-performed", () => {
stopScrollAllAtDNDEnd(); stopScrollAllAtDNDEnd();
}); });
@ -93,7 +109,7 @@ export default class PrefsPage extends Adw.PreferencesPage {
this.add_controller(controller); this.add_controller(controller);
} }
onRowMove(listBox, row, direction) { onRowMove(listBox: PrefsBoxOrderListBox, row: PrefsBoxOrderItemRow, direction: string): void {
const rowPosition = row.get_index(); const rowPosition = row.get_index();
if (direction === "up") { // If the direction of the move is up. if (direction === "up") { // If the direction of the move is up.

View File

@ -1,23 +1,21 @@
"use strict"; "use strict";
import GLib from "gi://GLib"; import GLib from "gi://GLib";
import type Gtk from "gi://Gtk";
export default class ScrollManager { export default class ScrollManager {
#gtkScrolledWindow; #gtkScrolledWindow: Gtk.ScrolledWindow;
#scrollUp; #scrollUp: boolean;
#scrollDown; #scrollDown: boolean;
/** constructor(gtkScrolledWindow: Gtk.ScrolledWindow) {
* @param {Gtk.ScrolledWindow} gtkScrolledWindow
*/
constructor(gtkScrolledWindow) {
this.#gtkScrolledWindow = gtkScrolledWindow; this.#gtkScrolledWindow = gtkScrolledWindow;
this.#scrollUp = false; this.#scrollUp = false;
this.#scrollDown = false; this.#scrollDown = false;
} }
startScrollUp() { startScrollUp(): void {
// If the scroll up is already started, don't do anything. // If the scroll up is already started, don't do anything.
if (this.#scrollUp) { if (this.#scrollUp) {
return; return;
@ -44,7 +42,7 @@ export default class ScrollManager {
}); });
} }
startScrollDown() { startScrollDown(): void {
// If the scroll down is already started, don't do anything. // If the scroll down is already started, don't do anything.
if (this.#scrollDown) { if (this.#scrollDown) {
return; return;
@ -74,15 +72,15 @@ export default class ScrollManager {
}); });
} }
stopScrollUp() { stopScrollUp(): void {
this.#scrollUp = false; this.#scrollUp = false;
} }
stopScrollDown() { stopScrollDown(): void {
this.#scrollDown = false; this.#scrollDown = false;
} }
stopScrollAll() { stopScrollAll(): void {
this.stopScrollUp(); this.stopScrollUp();
this.stopScrollDown(); this.stopScrollDown();
} }

22
tsconfig.json Normal file
View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"sourceMap": false,
"strict": true,
// To preserve imports for UI files.
"verbatimModuleSyntax": true,
"target": "es2022",
"lib": [
"ES2022"
],
},
"include": [
"ambient.d.ts",
],
"files": [
"src/extension.ts",
"src/prefs.ts"
]
}