mirror of
https://gitlab.gnome.org/june/top-bar-organizer.git
synced 2026-11-10 12:00:51 +01:00
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
This commit is contained in:
parent
ff75debabc
commit
5a09b1a2c8
15 changed files with 1196 additions and 192 deletions
|
|
@ -6,7 +6,7 @@ insert_final_newline = true
|
|||
indent_style = space
|
||||
charset = utf-8
|
||||
|
||||
[*.{js,json}]
|
||||
[*.{js,ts,json}]
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
|||
/node_modules/
|
||||
/dist/
|
||||
top-bar-organizer@julian.gse.jsts.xyz.shell-extension.zip
|
||||
|
|
|
|||
4
ambient.d.ts
vendored
Normal file
4
ambient.d.ts
vendored
Normal 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
1002
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -2,6 +2,7 @@
|
|||
"name": "top-bar-organizer",
|
||||
"version": "1.0.0",
|
||||
"description": "A Gnome Shell Extension for organizing your Gnome Shell top bar.",
|
||||
"type": "module",
|
||||
"directories": {
|
||||
"doc": "docs"
|
||||
},
|
||||
|
|
@ -12,6 +13,12 @@
|
|||
"author": "June",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,11 @@ set -e
|
|||
|
||||
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 \
|
||||
--extra-source extensionModules \
|
||||
--extra-source prefsModules \
|
||||
|
|
|
|||
211
src/extension.ts
Normal file
211
src/extension.ts
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
"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;
|
||||
switch (box) {
|
||||
case "left":
|
||||
panelBox = (Main.panel as CustomPanel)._leftBox;
|
||||
break;
|
||||
case "center":
|
||||
panelBox = (Main.panel as CustomPanel)._centerBox;
|
||||
break;
|
||||
case "right":
|
||||
panelBox = (Main.panel as CustomPanel)._rightBox;
|
||||
break;
|
||||
}
|
||||
|
||||
/// 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.
|
||||
}
|
||||
}
|
||||
398
src/extensionModules/BoxOrderManager.ts
Normal file
398
src/extensionModules/BoxOrderManager.ts
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
"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;
|
||||
}
|
||||
|
||||
// If there are no roles associated, continue.
|
||||
if (roles.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise 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);
|
||||
|
||||
// ToDo: simplify.
|
||||
// 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 = [
|
||||
(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);
|
||||
|
||||
// Create an indicator containers set from the indicator containers for
|
||||
// fast easy access.
|
||||
const indicatorContainerSet = new Set(indicatorContainers);
|
||||
|
||||
// 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 (indicatorContainerSet.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);
|
||||
}
|
||||
}
|
||||
32
src/prefs.ts
Normal file
32
src/prefs.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
"use strict";
|
||||
|
||||
import Gtk from "gi://Gtk";
|
||||
import Gdk from "gi://Gdk";
|
||||
|
||||
import { ExtensionPreferences } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";
|
||||
|
||||
import PrefsPage from "./prefsModules/PrefsPage.js";
|
||||
|
||||
export default class TopBarOrganizerPreferences extends ExtensionPreferences {
|
||||
getPreferencesWidget() {
|
||||
const provider = new Gtk.CssProvider();
|
||||
provider.load_from_path(this.metadata.dir.get_path() + "/css/prefs.css");
|
||||
const defaultGdkDisplay = Gdk.Display.get_default();
|
||||
Gtk.StyleContext.add_provider_for_display(
|
||||
(defaultGdkDisplay as Gdk.Display),
|
||||
provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
||||
);
|
||||
|
||||
const prefsPage = new PrefsPage();
|
||||
|
||||
prefsPage.connect("destroy", () => {
|
||||
Gtk.StyleContext.remove_provider_for_display(
|
||||
(defaultGdkDisplay as Gdk.Display),
|
||||
provider
|
||||
);
|
||||
});
|
||||
|
||||
return prefsPage;
|
||||
}
|
||||
}
|
||||
151
src/prefsModules/PrefsBoxOrderItemRow.ts
Normal file
151
src/prefsModules/PrefsBoxOrderItemRow.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
"use strict";
|
||||
|
||||
import Gtk from "gi://Gtk";
|
||||
import Gdk from "gi://Gdk";
|
||||
import GObject from "gi://GObject";
|
||||
import Adw from "gi://Adw";
|
||||
import GLib from "gi://GLib";
|
||||
|
||||
import type PrefsBoxOrderListBox from "./PrefsBoxOrderListBox.js";
|
||||
|
||||
export default class PrefsBoxOrderItemRow extends Adw.ActionRow {
|
||||
static {
|
||||
GObject.registerClass({
|
||||
GTypeName: "PrefsBoxOrderItemRow",
|
||||
Template: GLib.uri_resolve_relative(import.meta.url, "../ui/prefs-box-order-item-row.ui", GLib.UriFlags.NONE),
|
||||
Signals: {
|
||||
"move": {
|
||||
param_types: [GObject.TYPE_STRING],
|
||||
},
|
||||
},
|
||||
}, this);
|
||||
this.install_action("row.forget", null, (self, _actionName, _param) => {
|
||||
const parentListBox = self.get_parent() as PrefsBoxOrderListBox;
|
||||
parentListBox.removeRow(self as PrefsBoxOrderItemRow);
|
||||
parentListBox.saveBoxOrderToSettings();
|
||||
parentListBox.determineRowMoveActionEnable();
|
||||
});
|
||||
this.install_action("row.move-up", null, (self, _actionName, _param) => self.emit("move", "up"));
|
||||
this.install_action("row.move-down", null, (self, _actionName, _param) => self.emit("move", "down"));
|
||||
}
|
||||
|
||||
item: string;
|
||||
#drag_starting_point_x?: number;
|
||||
#drag_starting_point_y?: number;
|
||||
|
||||
constructor(params = {}, item: string) {
|
||||
super(params);
|
||||
|
||||
// Associate `this` with an item.
|
||||
this.item = item;
|
||||
if (this.item.startsWith("appindicator-kstatusnotifieritem-")) {
|
||||
// Set the title to something nicer, if the associated item is an
|
||||
// AppIndicator/KStatusNotifierItem item.
|
||||
this.set_title(this.item.replace("appindicator-kstatusnotifieritem-", ""));
|
||||
} else if (this.item === "item-role-group-task-up-ultralite") {
|
||||
// Set the title to something nicer, if the item in question is the
|
||||
// Task Up UltraLite item role group.
|
||||
this.set_title("Task Up UltraLite Items");
|
||||
} else {
|
||||
// Otherwise just set it to `item`.
|
||||
this.set_title(this.item);
|
||||
}
|
||||
}
|
||||
|
||||
onDragPrepare(_source: Gtk.DragSource, x: number, y: number): Gdk.ContentProvider {
|
||||
const value = new GObject.Value();
|
||||
value.init(PrefsBoxOrderItemRow.$gtype);
|
||||
value.set_object(this);
|
||||
|
||||
this.#drag_starting_point_x = x;
|
||||
this.#drag_starting_point_y = y;
|
||||
return Gdk.ContentProvider.new_for_value(value);
|
||||
}
|
||||
|
||||
onDragBegin(_source: Gtk.DragSource, drag: Gdk.Drag): void {
|
||||
let dragWidget = new Gtk.ListBox();
|
||||
let allocation = this.get_allocation();
|
||||
dragWidget.set_size_request(allocation.width, allocation.height);
|
||||
|
||||
let dragPrefsBoxOrderItemRow = new PrefsBoxOrderItemRow({}, this.item);
|
||||
dragWidget.append(dragPrefsBoxOrderItemRow);
|
||||
dragWidget.drag_highlight_row(dragPrefsBoxOrderItemRow);
|
||||
|
||||
let currentDragIcon = Gtk.DragIcon.get_for_drag(drag);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle a new drop on `this` properly.
|
||||
// `value` is the thing getting dropped.
|
||||
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 (value === this) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the GtkListBoxes of `this` and the drop value.
|
||||
const ownListBox = this.get_parent() as PrefsBoxOrderListBox;
|
||||
const valueListBox = value.get_parent() as PrefsBoxOrderListBox;
|
||||
|
||||
// Get the position of `this` and the drop value.
|
||||
const ownPosition = this.get_index();
|
||||
const valuePosition = value.get_index();
|
||||
|
||||
// Remove the drop value from its list box.
|
||||
valueListBox.removeRow(value);
|
||||
|
||||
// Since an element got potentially removed from the list of `this`,
|
||||
// get the position of `this` again.
|
||||
const updatedOwnPosition = this.get_index();
|
||||
|
||||
if (ownListBox !== valueListBox) {
|
||||
// First handle the case where `this` and the drop value are in
|
||||
// different list boxes.
|
||||
if ((ownListBox.boxOrder === "right-box-order" && valueListBox.boxOrder === "left-box-order")
|
||||
|| (ownListBox.boxOrder === "right-box-order" && valueListBox.boxOrder === "center-box-order")
|
||||
|| (ownListBox.boxOrder === "center-box-order" && valueListBox.boxOrder === "left-box-order")) {
|
||||
// If the list box of the drop value comes before the list
|
||||
// box of `this`, add the drop value after `this`.
|
||||
ownListBox.insertRow(value, updatedOwnPosition + 1);
|
||||
} else {
|
||||
// Otherwise, add the drop value where `this` currently is.
|
||||
ownListBox.insertRow(value, updatedOwnPosition);
|
||||
}
|
||||
} else {
|
||||
if (valuePosition < ownPosition) {
|
||||
// If the drop value was before `this`, add the drop value
|
||||
// after `this`.
|
||||
ownListBox.insertRow(value, updatedOwnPosition + 1);
|
||||
} else {
|
||||
// Otherwise, add the drop value where `this` currently is.
|
||||
ownListBox.insertRow(value, updatedOwnPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/// Finally save the box order(/s) to settings and make sure move
|
||||
/// actions are correctly enabled/disabled.
|
||||
ownListBox.saveBoxOrderToSettings();
|
||||
ownListBox.determineRowMoveActionEnable();
|
||||
// If the list boxes of `this` and the drop value were different, handle
|
||||
// the former list box of the drop value as well.
|
||||
if (ownListBox !== valueListBox) {
|
||||
valueListBox.saveBoxOrderToSettings();
|
||||
valueListBox.determineRowMoveActionEnable();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
152
src/prefsModules/PrefsBoxOrderListBox.ts
Normal file
152
src/prefsModules/PrefsBoxOrderListBox.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
"use strict";
|
||||
|
||||
import Gtk from "gi://Gtk";
|
||||
import GObject from "gi://GObject";
|
||||
import GLib from "gi://GLib";
|
||||
import type Gio from "gi://Gio";
|
||||
|
||||
import { ExtensionPreferences } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";
|
||||
|
||||
import PrefsBoxOrderItemRow from "./PrefsBoxOrderItemRow.js";
|
||||
import PrefsBoxOrderListEmptyPlaceholder from "./PrefsBoxOrderListEmptyPlaceholder.js";
|
||||
|
||||
export default class PrefsBoxOrderListBox extends Gtk.ListBox {
|
||||
static {
|
||||
GObject.registerClass({
|
||||
GTypeName: "PrefsBoxOrderListBox",
|
||||
Template: GLib.uri_resolve_relative(import.meta.url, "../ui/prefs-box-order-list-box.ui", GLib.UriFlags.NONE),
|
||||
Properties: {
|
||||
BoxOrder: GObject.ParamSpec.string(
|
||||
"box-order",
|
||||
"Box Order",
|
||||
"The box order this PrefsBoxOrderListBox is associated with.",
|
||||
GObject.ParamFlags.READWRITE,
|
||||
""
|
||||
),
|
||||
},
|
||||
Signals: {
|
||||
"row-move": {
|
||||
param_types: [PrefsBoxOrderItemRow.$gtype, GObject.TYPE_STRING],
|
||||
},
|
||||
},
|
||||
}, this);
|
||||
}
|
||||
|
||||
_boxOrder!: string;
|
||||
#settings: Gio.Settings;
|
||||
#rowSignalHandlerIds = new Map<PrefsBoxOrderItemRow, number[]>();
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
*/
|
||||
constructor(params = {}) {
|
||||
super(params);
|
||||
|
||||
// Load the settings.
|
||||
this.#settings = ExtensionPreferences.lookupByURL(import.meta.url)!.getSettings();
|
||||
|
||||
// Add a placeholder widget for the case, where no GtkListBoxRows are
|
||||
// present.
|
||||
this.set_placeholder(new PrefsBoxOrderListEmptyPlaceholder());
|
||||
}
|
||||
|
||||
get boxOrder(): string {
|
||||
return this._boxOrder;
|
||||
}
|
||||
|
||||
set boxOrder(value: string) {
|
||||
this._boxOrder = value;
|
||||
|
||||
// Get the actual box order for the given box order name from settings.
|
||||
const boxOrder = this.#settings.get_strv(this._boxOrder);
|
||||
// Populate this GtkListBox with GtkListBoxRows for the items of the
|
||||
// given configured box order.
|
||||
for (const item of boxOrder) {
|
||||
const row = new PrefsBoxOrderItemRow({}, item);
|
||||
this.insertRow(row, -1);
|
||||
}
|
||||
|
||||
this.determineRowMoveActionEnable();
|
||||
this.notify("box-order");
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given PrefsBoxOrderItemRow to this list box at the given
|
||||
* position.
|
||||
* Also handles stuff like connecting signals.
|
||||
*/
|
||||
insertRow(row: PrefsBoxOrderItemRow, position: number): void {
|
||||
this.insert(row, position);
|
||||
|
||||
const signalHandlerIds: number[] = [];
|
||||
signalHandlerIds.push(row.connect("move", (row, direction) => {
|
||||
this.emit("row-move", row, direction);
|
||||
}));
|
||||
|
||||
this.#rowSignalHandlerIds.set(row, signalHandlerIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given PrefsBoxOrderItemRow from this list box.
|
||||
* Also handles stuff like disconnecting signals.
|
||||
*/
|
||||
removeRow(row: PrefsBoxOrderItemRow): void {
|
||||
const signalHandlerIds = this.#rowSignalHandlerIds.get(row) ?? [];
|
||||
|
||||
for (const id of signalHandlerIds) {
|
||||
row.disconnect(id);
|
||||
}
|
||||
|
||||
this.remove(row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the box order represented by `this` (and its
|
||||
* `PrefsBoxOrderItemRows`) to settings.
|
||||
*/
|
||||
saveBoxOrderToSettings(): void {
|
||||
let currentBoxOrder: string[] = [];
|
||||
for (let potentialPrefsBoxOrderItemRow of this) {
|
||||
// Only process PrefsBoxOrderItemRows.
|
||||
if (!(potentialPrefsBoxOrderItemRow instanceof PrefsBoxOrderItemRow)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const item = potentialPrefsBoxOrderItemRow.item;
|
||||
currentBoxOrder.push(item);
|
||||
}
|
||||
this.#settings.set_strv(this.boxOrder, currentBoxOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether or not each move action of each PrefsBoxOrderItemRow
|
||||
* should be enabled or disabled.
|
||||
*/
|
||||
determineRowMoveActionEnable(): void {
|
||||
for (let potentialPrefsBoxOrderItemRow of this) {
|
||||
// Only process PrefsBoxOrderItemRows.
|
||||
if (!(potentialPrefsBoxOrderItemRow instanceof PrefsBoxOrderItemRow)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const row = potentialPrefsBoxOrderItemRow;
|
||||
|
||||
// If the current row is the topmost row in the topmost list box,
|
||||
// then disable the move-up action.
|
||||
if (row.get_index() === 0 && this.boxOrder === "left-box-order") {
|
||||
row.action_set_enabled("row.move-up", false);
|
||||
} else { // Else enable it.
|
||||
row.action_set_enabled("row.move-up", true);
|
||||
}
|
||||
|
||||
// If the current row is the bottommost row in the bottommost list
|
||||
// box, then disable the move-down action.
|
||||
const rowNextSibling = row.get_next_sibling();
|
||||
if ((rowNextSibling instanceof PrefsBoxOrderListEmptyPlaceholder || rowNextSibling === null) && this.boxOrder === "right-box-order") {
|
||||
row.action_set_enabled("row.move-down", false);
|
||||
} else { // Else enable it.
|
||||
row.action_set_enabled("row.move-down", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/prefsModules/PrefsBoxOrderListEmptyPlaceholder.ts
Normal file
48
src/prefsModules/PrefsBoxOrderListEmptyPlaceholder.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
"use strict";
|
||||
|
||||
import Gtk from "gi://Gtk";
|
||||
import GObject from "gi://GObject";
|
||||
import GLib from "gi://GLib";
|
||||
|
||||
import PrefsBoxOrderItemRow from "./PrefsBoxOrderItemRow.js";
|
||||
import type PrefsBoxOrderListBox from "./PrefsBoxOrderListBox.js";
|
||||
|
||||
export default class PrefsBoxOrderListEmptyPlaceholder extends Gtk.Box {
|
||||
static {
|
||||
GObject.registerClass({
|
||||
GTypeName: "PrefsBoxOrderListEmptyPlaceholder",
|
||||
Template: GLib.uri_resolve_relative(import.meta.url, "../ui/prefs-box-order-list-empty-placeholder.ui", GLib.UriFlags.NONE),
|
||||
}, this);
|
||||
}
|
||||
|
||||
// Handle a new drop on `this` properly.
|
||||
// `value` is the thing getting dropped.
|
||||
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.
|
||||
const ownListBox = this.get_parent() as PrefsBoxOrderListBox;
|
||||
const valueListBox = value.get_parent() as PrefsBoxOrderListBox;
|
||||
|
||||
// Remove the drop value from its list box.
|
||||
valueListBox.removeRow(value);
|
||||
|
||||
// Insert the drop value into the list box of `this`.
|
||||
ownListBox.insertRow(value, 0);
|
||||
|
||||
/// Finally save the box orders to settings and make sure move actions
|
||||
/// are correctly enabled/disabled.
|
||||
ownListBox.saveBoxOrderToSettings();
|
||||
ownListBox.determineRowMoveActionEnable();
|
||||
valueListBox.saveBoxOrderToSettings();
|
||||
valueListBox.determineRowMoveActionEnable();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
196
src/prefsModules/PrefsPage.ts
Normal file
196
src/prefsModules/PrefsPage.ts
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
"use strict";
|
||||
|
||||
import Gdk from "gi://Gdk";
|
||||
import Gtk from "gi://Gtk";
|
||||
import GObject from "gi://GObject";
|
||||
import Adw from "gi://Adw";
|
||||
import GLib from "gi://GLib";
|
||||
|
||||
import ScrollManager from "./ScrollManager.js";
|
||||
import PrefsBoxOrderListEmptyPlaceholder from "./PrefsBoxOrderListEmptyPlaceholder.js";
|
||||
import type PrefsBoxOrderItemRow from "./PrefsBoxOrderItemRow.js";
|
||||
|
||||
// Imports to make UI file work.
|
||||
// eslint-disable-next-line
|
||||
import PrefsBoxOrderListBox from "./PrefsBoxOrderListBox.js";
|
||||
|
||||
export default class PrefsPage extends Adw.PreferencesPage {
|
||||
static {
|
||||
GObject.registerClass({
|
||||
GTypeName: "PrefsPage",
|
||||
Template: GLib.uri_resolve_relative(import.meta.url, "../ui/prefs-page.ui", GLib.UriFlags.NONE),
|
||||
InternalChildren: [
|
||||
"left-box-order-list-box",
|
||||
"center-box-order-list-box",
|
||||
"right-box-order-list-box",
|
||||
],
|
||||
}, this);
|
||||
}
|
||||
|
||||
_dndEnded?: boolean;
|
||||
declare _left_box_order_list_box: PrefsBoxOrderListBox;
|
||||
declare _center_box_order_list_box: PrefsBoxOrderListBox;
|
||||
declare _right_box_order_list_box: PrefsBoxOrderListBox;
|
||||
|
||||
constructor(params = {}) {
|
||||
super(params);
|
||||
|
||||
this.#setupDNDScroll();
|
||||
}
|
||||
|
||||
/**
|
||||
* This function sets up Drag-and-Drop scrolling.
|
||||
* This means that scroll up or down is happening when a Drag-and-Drop
|
||||
* operation is in progress and the user has their cursor either in the
|
||||
* upper or lower 10% of this widget respectively.
|
||||
*/
|
||||
#setupDNDScroll(): void {
|
||||
// Pass `this.get_first_child()` to the ScrollManager, since this
|
||||
// `PrefsPage` extends an `Adw.PreferencesPage` and the first child of
|
||||
// an `Adw.PreferencesPage` is the built-in `Gtk.ScrolledWindow`.
|
||||
const scrollManager = new ScrollManager(this.get_first_child() as Gtk.ScrolledWindow);
|
||||
|
||||
/// Setup GtkDropControllerMotion event controller and make use of its
|
||||
/// events.
|
||||
let controller = new Gtk.DropControllerMotion();
|
||||
|
||||
// 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) => {
|
||||
if ((y <= this.get_allocated_height() * 0.1) && !this._dndEnded) {
|
||||
// If the pointer is currently in the upper ten percent of this
|
||||
// widget, then scroll up.
|
||||
scrollManager.startScrollUp();
|
||||
} else if ((y >= this.get_allocated_height() * 0.9) && !this._dndEnded) {
|
||||
// If the pointer is currently in the lower ten percent of this
|
||||
// widget, then scroll down.
|
||||
scrollManager.startScrollDown();
|
||||
} else {
|
||||
// Otherwise stop scrolling.
|
||||
scrollManager.stopScrollAll();
|
||||
}
|
||||
});
|
||||
|
||||
const stopScrollAllAtDNDEnd = () => {
|
||||
this._dndEnded = true;
|
||||
scrollManager.stopScrollAll();
|
||||
};
|
||||
controller.connect("leave", () => {
|
||||
stopScrollAllAtDNDEnd();
|
||||
});
|
||||
controller.connect("enter", () => {
|
||||
// Make use of `this._dndEnded` to setup stopScrollAtDNDEnd only
|
||||
// once per DND operation.
|
||||
if (this._dndEnded) {
|
||||
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", () => {
|
||||
stopScrollAllAtDNDEnd();
|
||||
});
|
||||
drag.connect("dnd-finished", () => {
|
||||
stopScrollAllAtDNDEnd();
|
||||
});
|
||||
drag.connect("cancel", () => {
|
||||
stopScrollAllAtDNDEnd();
|
||||
});
|
||||
this._dndEnded = false;
|
||||
}
|
||||
});
|
||||
|
||||
this.add_controller(controller);
|
||||
}
|
||||
|
||||
onRowMove(listBox: PrefsBoxOrderListBox, row: PrefsBoxOrderItemRow, direction: string): void {
|
||||
const rowPosition = row.get_index();
|
||||
|
||||
if (direction === "up") { // If the direction of the move is up.
|
||||
// Handle the case, where the row is the topmost row in the list box.
|
||||
if (rowPosition === 0) {
|
||||
switch (listBox.boxOrder) {
|
||||
// If the row is also in the topmost list box, then do
|
||||
// nothing and return.
|
||||
case "left-box-order":
|
||||
log("The row is already the topmost row in the topmost box order.");
|
||||
return;
|
||||
// If the row is in the center list box, then move it up to
|
||||
// the left one.
|
||||
case "center-box-order":
|
||||
listBox.removeRow(row);
|
||||
this._left_box_order_list_box.insertRow(row, -1);
|
||||
// First save the box order of the destination, then do
|
||||
// "a save for clean up".
|
||||
this._left_box_order_list_box.saveBoxOrderToSettings();
|
||||
this._left_box_order_list_box.determineRowMoveActionEnable();
|
||||
listBox.saveBoxOrderToSettings();
|
||||
listBox.determineRowMoveActionEnable();
|
||||
return;
|
||||
// If the row is in the right list box, then move it up to
|
||||
// the center one.
|
||||
case "right-box-order":
|
||||
listBox.removeRow(row);
|
||||
this._center_box_order_list_box.insertRow(row, -1);
|
||||
this._center_box_order_list_box.saveBoxOrderToSettings();
|
||||
this._center_box_order_list_box.determineRowMoveActionEnable();
|
||||
listBox.saveBoxOrderToSettings();
|
||||
listBox.determineRowMoveActionEnable();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Else just move the row up in the box.
|
||||
listBox.removeRow(row);
|
||||
listBox.insertRow(row, rowPosition - 1);
|
||||
listBox.saveBoxOrderToSettings();
|
||||
listBox.determineRowMoveActionEnable();
|
||||
return;
|
||||
} else { // Else the direction of the move must be down.
|
||||
// Handle the case, where the row is the bottommost row in the list box.
|
||||
const rowNextSibling = row.get_next_sibling();
|
||||
if (rowNextSibling instanceof PrefsBoxOrderListEmptyPlaceholder || rowNextSibling === null) {
|
||||
switch (listBox.boxOrder) {
|
||||
// If the row is also in the bottommost list box, then do
|
||||
// nothing and return.
|
||||
case "right-box-order":
|
||||
log("The row is already the bottommost row in the bottommost box order.");
|
||||
return;
|
||||
// If the row is in the center list box, then move it down
|
||||
// to the right one.
|
||||
case "center-box-order":
|
||||
listBox.removeRow(row);
|
||||
this._right_box_order_list_box.insertRow(row, 0);
|
||||
this._right_box_order_list_box.saveBoxOrderToSettings();
|
||||
this._right_box_order_list_box.determineRowMoveActionEnable();
|
||||
listBox.saveBoxOrderToSettings();
|
||||
listBox.determineRowMoveActionEnable();
|
||||
return;
|
||||
// If the row is in the left list box, then move it down to
|
||||
// the center one.
|
||||
case "left-box-order":
|
||||
listBox.removeRow(row);
|
||||
this._center_box_order_list_box.insertRow(row, 0);
|
||||
this._center_box_order_list_box.saveBoxOrderToSettings();
|
||||
this._center_box_order_list_box.determineRowMoveActionEnable();
|
||||
listBox.saveBoxOrderToSettings();
|
||||
listBox.determineRowMoveActionEnable();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Else just move the row down in the box.
|
||||
listBox.removeRow(row);
|
||||
listBox.insertRow(row, rowPosition + 1);
|
||||
listBox.saveBoxOrderToSettings();
|
||||
listBox.determineRowMoveActionEnable();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
87
src/prefsModules/ScrollManager.ts
Normal file
87
src/prefsModules/ScrollManager.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
"use strict";
|
||||
|
||||
import GLib from "gi://GLib";
|
||||
import type Gtk from "gi://Gtk";
|
||||
|
||||
export default class ScrollManager {
|
||||
#gtkScrolledWindow: Gtk.ScrolledWindow;
|
||||
#scrollUp: boolean;
|
||||
#scrollDown: boolean;
|
||||
|
||||
constructor(gtkScrolledWindow: Gtk.ScrolledWindow) {
|
||||
this.#gtkScrolledWindow = gtkScrolledWindow;
|
||||
|
||||
this.#scrollUp = false;
|
||||
this.#scrollDown = false;
|
||||
}
|
||||
|
||||
startScrollUp(): void {
|
||||
// If the scroll up is already started, don't do anything.
|
||||
if (this.#scrollUp) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure scroll down is stopped.
|
||||
this.stopScrollDown();
|
||||
|
||||
this.#scrollUp = true;
|
||||
|
||||
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 200, () => {
|
||||
// Set the new vadjustment value to either the current value minus a
|
||||
// step increment or to 0.
|
||||
const newVAdjustementValue = Math.max(this.#gtkScrolledWindow.vadjustment.get_value() - this.#gtkScrolledWindow.vadjustment.get_step_increment(), 0);
|
||||
|
||||
// If the new value is the old one, return and stop this interval.
|
||||
if (newVAdjustementValue === this.#gtkScrolledWindow.vadjustment.get_value()) {
|
||||
this.#scrollUp = false;
|
||||
return this.#scrollUp;
|
||||
}
|
||||
// Otherwise, update the value.
|
||||
this.#gtkScrolledWindow.vadjustment.set_value(newVAdjustementValue);
|
||||
return this.#scrollUp;
|
||||
});
|
||||
}
|
||||
|
||||
startScrollDown(): void {
|
||||
// If the scroll down is already started, don't do anything.
|
||||
if (this.#scrollDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure scroll up is stopped.
|
||||
this.stopScrollUp();
|
||||
|
||||
this.#scrollDown = true;
|
||||
|
||||
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 200, () => {
|
||||
// Set the new vadjusment value either to the curent value plus a
|
||||
// step increment or to the upper value minus the page size.
|
||||
const newVAdjustementValue = Math.min(
|
||||
this.#gtkScrolledWindow.vadjustment.get_value() + this.#gtkScrolledWindow.vadjustment.get_step_increment(),
|
||||
this.#gtkScrolledWindow.vadjustment.get_upper() - this.#gtkScrolledWindow.vadjustment.get_page_size()
|
||||
);
|
||||
|
||||
// If the new value is the old one, return and stop this interval.
|
||||
if (newVAdjustementValue === this.#gtkScrolledWindow.vadjustment.get_value()) {
|
||||
this.#scrollDown = false;
|
||||
return this.#scrollDown;
|
||||
}
|
||||
// Otherwise, update the value.
|
||||
this.#gtkScrolledWindow.vadjustment.set_value(newVAdjustementValue);
|
||||
return this.#scrollDown;
|
||||
});
|
||||
}
|
||||
|
||||
stopScrollUp(): void {
|
||||
this.#scrollUp = false;
|
||||
}
|
||||
|
||||
stopScrollDown(): void {
|
||||
this.#scrollDown = false;
|
||||
}
|
||||
|
||||
stopScrollAll(): void {
|
||||
this.stopScrollUp();
|
||||
this.stopScrollDown();
|
||||
}
|
||||
}
|
||||
22
tsconfig.json
Normal file
22
tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue