Compare commits

..

No commits in common. "9a6474b9474a0ea5ef502fd026d2a8cd36857b43" and "979e770057d761fea13be3e47655274fb6694947" have entirely different histories.

15 changed files with 192 additions and 1200 deletions

View File

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

1
.gitignore vendored
View File

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

4
ambient.d.ts vendored
View File

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

996
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,6 @@
"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"
},
@ -13,12 +12,6 @@
"author": "June",
"license": "GPL-3.0-or-later",
"devDependencies": {
"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"
"eslint": "^8.50.0"
}
}

View File

@ -4,11 +4,7 @@ set -e
REAL_BASE_DIR=$( dirname $( readlink -f "$0" ))
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" \
gnome-extensions pack "$REAL_BASE_DIR/src" \
--force \
--extra-source extensionModules \
--extra-source prefsModules \

View File

@ -1,27 +1,13 @@
"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 {
enable() {
this._settings = this.getSettings();
this._boxOrderManager = new BoxOrderManager({}, this._settings);
@ -40,7 +26,7 @@ export default class TopBarOrganizerExtension extends Extension {
// Handle changes of settings.
this._settingsHandlerIds = [];
const addSettingsChangeHandler = (settingsName: string) => {
const addSettingsChangeHandler = (settingsName) => {
const handlerId = this._settings.connect(`changed::${settingsName}`, () => {
this.#handleNewItemsAndOrderTopBar();
});
@ -53,12 +39,10 @@ export default class TopBarOrganizerExtension extends Extension {
addSettingsChangeHandler("show");
}
disable(): void {
disable() {
// 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.
@ -67,9 +51,7 @@ export default class TopBarOrganizerExtension extends Extension {
}
this._boxOrderManager.disconnectSignals();
// @ts-ignore
this._settings = null;
// @ts-ignore
this._boxOrderManager = null;
}
@ -81,10 +63,9 @@ export default class TopBarOrganizerExtension extends Extension {
* 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 {
#overwritePanelAddToPanelBox() {
// Add the original `Panel._addToPanelBox` method as
// `Panel._originalAddToPanelBox`.
// @ts-ignore
Panel.Panel.prototype._originalAddToPanelBox = Panel.Panel.prototype._addToPanelBox;
const handleNewItemsAndOrderTopBar = () => {
@ -95,7 +76,6 @@ export default class TopBarOrganizerExtension extends Extension {
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();
};
@ -108,9 +88,9 @@ export default class TopBarOrganizerExtension extends Extension {
/**
* This method orders the top bar items of the specified box according to
* the configured box orders.
* @param {Box} box - The box to order.
* @param {string} box - The box to order.
*/
#orderTopBarItems(box: Box): void {
#orderTopBarItems(box) {
// Only run, when the session mode is "user" or the parent session mode
// is "user".
if(Main.sessionMode.currentMode !== "user" && Main.sessionMode.parentMode !== "user") {
@ -124,13 +104,13 @@ export default class TopBarOrganizerExtension extends Extension {
let panelBox;
switch (box) {
case "left":
panelBox = (Main.panel as CustomPanel)._leftBox;
panelBox = Main.panel._leftBox;
break;
case "center":
panelBox = (Main.panel as CustomPanel)._centerBox;
panelBox = Main.panel._centerBox;
break;
case "right":
panelBox = (Main.panel as CustomPanel)._rightBox;
panelBox = Main.panel._rightBox;
break;
}
@ -139,19 +119,12 @@ export default class TopBarOrganizerExtension extends Extension {
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;
}
const associatedIndicatorContainer = Main.panel.statusArea[item.role].container;
// 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);
}
associatedIndicatorContainer.get_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
@ -192,7 +165,7 @@ export default class TopBarOrganizerExtension extends Extension {
* This method handles all new items currently present in the top bar and
* orders the items of all top bar boxes.
*/
#handleNewItemsAndOrderTopBar(): void {
#handleNewItemsAndOrderTopBar() {
// Only run, when the session mode is "user" or the parent session mode
// is "user".
if(Main.sessionMode.currentMode !== "user" && Main.sessionMode.parentMode !== "user") {

View File

@ -1,24 +1,18 @@
"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.
* @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.
@ -36,15 +30,12 @@ export default class BoxOrderManager extends GObject.Object {
}, 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;
#appIndicatorReadyHandlerIdMap;
#appIndicatorItemSettingsIdToRolesMap;
#taskUpUltraLiteItemRoles;
#settings;
constructor(params = {}, settings: Gio.Settings) {
// @ts-ignore Params should be passed, see: https://gjs.guide/guides/gobject/subclassing.html#subclassing-gobject
constructor(params = {}, settings) {
super(params);
this.#appIndicatorReadyHandlerIdMap = new Map();
@ -56,22 +47,30 @@ export default class BoxOrderManager extends GObject.Object {
/**
* 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.
* @param {string} 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
* settings identifiers.
*/
#getBoxOrder(box: Box): string[] {
#getBoxOrder(box) {
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} 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
* item settings identifiers.
*/
#saveBoxOrder(box: Box, boxOrder: string[]): void {
#saveBoxOrder(box, boxOrder) {
const currentBoxOrder = this.#getBoxOrder(box);
// Only save the given box order to settings, if it is different, to
@ -91,18 +90,14 @@ export default class BoxOrderManager extends GObject.Object {
* 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
* @param {string} 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;
#handleAppIndicatorItem(indicatorContainer, role) {
const appIndicator = indicatorContainer.get_child()._indicator;
let application = appIndicator.id;
if (!application && this.#appIndicatorReadyHandlerIdMap) {
@ -151,7 +146,7 @@ export default class BoxOrderManager extends GObject.Object {
* @param {string} role - The role of the Task Up UltraLite item.
* @returns {string} The settings identifier to use.
*/
#handleTaskUpUltraLiteItem(role: string): string {
#handleTaskUpUltraLiteItem(role) {
const roles = this.#taskUpUltraLiteItemRoles;
if (!roles.includes(role)) {
@ -167,10 +162,14 @@ export default class BoxOrderManager extends GObject.Object {
* 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.
* @param {string} 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.
*/
#getResolvedBoxOrder(box: Box): ResolvedBoxOrderItem[] {
#getResolvedBoxOrder(box) {
let boxOrder = this.#getBoxOrder(box);
const itemsToHide = this.#settings.get_strv("hide");
@ -208,15 +207,15 @@ export default class BoxOrderManager extends GObject.Object {
// the item specially.
// Get the roles associated with the items settings id.
let roles: string[] = [];
let roles = [];
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") {
roles = this.#taskUpUltraLiteItemRoles;
}
// If there are no roles associated, continue.
if (roles.length === 0) {
if (!roles) {
continue;
}
@ -237,13 +236,12 @@ export default class BoxOrderManager extends GObject.Object {
* This is typically used before nulling an instance of this class to make
* sure all signals are disconnected.
*/
disconnectSignals(): void {
disconnectSignals() {
for (const [handlerId, appIndicator] of this.#appIndicatorReadyHandlerIdMap) {
if (handlerId && appIndicator?.signalHandlerIsConnected(handlerId)) {
appIndicator.disconnect(handlerId);
}
}
// @ts-ignore
this.#appIndicatorReadyHandlerIdMap = null;
}
@ -252,23 +250,25 @@ export default class BoxOrderManager extends GObject.Object {
* 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.
* @param {string} 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.
*/
getValidBoxOrder(box: Box): ResolvedBoxOrderItem[] {
getValidBoxOrder(box) {
// 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);
Main.panel._leftBox.get_children(),
Main.panel._centerBox.get_children(),
Main.panel._rightBox.get_children(),
].flat();
// Create an indicator containers set from the indicator containers for
// fast easy access.
@ -277,13 +277,10 @@ export default class BoxOrderManager extends GObject.Object {
// 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[] = [];
let validBoxOrder = [];
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;
}
// Get the indicator container associated with the items role.
const associatedIndicatorContainer = Main.panel.statusArea[item.role]?.container;
if (indicatorContainerSet.has(associatedIndicatorContainer)) {
validBoxOrder.push(item);
@ -297,7 +294,7 @@ export default class BoxOrderManager extends GObject.Object {
* This method saves all new items currently present in the GNOME Shell top
* bar to the settings.
*/
saveNewTopBarItems(): void {
saveNewTopBarItems() {
// Only run, when the session mode is "user" or the parent session mode
// is "user".
if (Main.sessionMode.currentMode !== "user" && Main.sessionMode.parentMode !== "user") {
@ -313,31 +310,24 @@ export default class BoxOrderManager extends GObject.Object {
// 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);
let indicatorContainerRoleMap = new Map();
for (const role in Main.panel.statusArea) {
indicatorContainerRoleMap.set(Main.panel.statusArea[role].container, 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),
left: Main.panel._leftBox.get_children(),
center: Main.panel._centerBox.get_children(),
// 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(),
right: Main.panel._rightBox.get_children().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) => {
const addNewItemSettingsIdsToBoxOrder = (indicatorContainers, boxOrder, box) => {
for (const indicatorContainer of indicatorContainers) {
// First get the role associated with the current indicator
// container.
@ -355,9 +345,6 @@ export default class BoxOrderManager extends GObject.Object {
try {
itemSettingsId = this.#handleAppIndicatorItem(indicatorContainer, role);
} catch (e) {
if (!(e instanceof Error)) {
throw(e);
}
if (e.message !== "Application can't be determined.") {
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");
const defaultGdkDisplay = Gdk.Display.get_default();
Gtk.StyleContext.add_provider_for_display(
(defaultGdkDisplay as Gdk.Display),
defaultGdkDisplay,
provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
);
@ -22,7 +22,7 @@ export default class TopBarOrganizerPreferences extends ExtensionPreferences {
prefsPage.connect("destroy", () => {
Gtk.StyleContext.remove_provider_for_display(
(defaultGdkDisplay as Gdk.Display),
defaultGdkDisplay,
provider
);
});

View File

@ -6,8 +6,6 @@ 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({
@ -20,8 +18,8 @@ export default class PrefsBoxOrderItemRow extends Adw.ActionRow {
},
}, this);
this.install_action("row.forget", null, (self, _actionName, _param) => {
const parentListBox = self.get_parent() as PrefsBoxOrderListBox;
parentListBox.removeRow(self as PrefsBoxOrderItemRow);
const parentListBox = self.get_parent();
parentListBox.removeRow(self);
parentListBox.saveBoxOrderToSettings();
parentListBox.determineRowMoveActionEnable();
});
@ -29,32 +27,39 @@ export default class PrefsBoxOrderItemRow extends Adw.ActionRow {
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;
#drag_starting_point_x;
#drag_starting_point_y;
constructor(params = {}, item: string) {
constructor(params = {}, item) {
super(params);
// Associate `this` with an item.
this.#associateItem(item);
}
/**
* Associate `this` with an item.
* @param {String} item
*/
#associateItem(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
// AppIndicator/KStatusNotifierItem item.
this.set_title(this.item.replace("appindicator-kstatusnotifieritem-", ""));
} else if (this.item === "item-role-group-task-up-ultralite") {
this.set_title(item.replace("appindicator-kstatusnotifieritem-", ""));
} else if (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);
this.set_title(item);
}
}
onDragPrepare(_source: Gtk.DragSource, x: number, y: number): Gdk.ContentProvider {
onDragPrepare(_source, x, y) {
const value = new GObject.Value();
value.init(PrefsBoxOrderItemRow.$gtype);
value.init(PrefsBoxOrderItemRow);
value.set_object(this);
this.#drag_starting_point_x = x;
@ -62,7 +67,7 @@ export default class PrefsBoxOrderItemRow extends Adw.ActionRow {
return Gdk.ContentProvider.new_for_value(value);
}
onDragBegin(_source: Gtk.DragSource, drag: Gdk.Drag): void {
onDragBegin(_source, drag) {
let dragWidget = new Gtk.ListBox();
let allocation = this.get_allocation();
dragWidget.set_size_request(allocation.width, allocation.height);
@ -73,32 +78,20 @@ export default class PrefsBoxOrderItemRow extends Adw.ActionRow {
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);
}
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;
}
onDrop(_target, value, _x, _y) {
// If `this` got dropped onto itself, do nothing.
if (value === this) {
return false;
return;
}
// Get the GtkListBoxes of `this` and the drop value.
const ownListBox = this.get_parent() as PrefsBoxOrderListBox;
const valueListBox = value.get_parent() as PrefsBoxOrderListBox;
const ownListBox = this.get_parent();
const valueListBox = value.get_parent();
// Get the position of `this` and the drop value.
const ownPosition = this.get_index();
@ -145,7 +138,5 @@ export default class PrefsBoxOrderItemRow extends Adw.ActionRow {
valueListBox.saveBoxOrderToSettings();
valueListBox.determineRowMoveActionEnable();
}
return true;
}
}

View File

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

View File

@ -4,9 +4,6 @@ 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({
@ -17,18 +14,10 @@ export default class PrefsBoxOrderListEmptyPlaceholder extends Gtk.Box {
// 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;
}
onDrop(_target, value, _x, _y) {
// Get the GtkListBoxes of `this` and the drop value.
const ownListBox = this.get_parent() as PrefsBoxOrderListBox;
const valueListBox = value.get_parent() as PrefsBoxOrderListBox;
const ownListBox = this.get_parent();
const valueListBox = value.get_parent();
// Remove the drop value from its list box.
valueListBox.removeRow(value);
@ -42,7 +31,5 @@ export default class PrefsBoxOrderListEmptyPlaceholder extends Gtk.Box {
ownListBox.determineRowMoveActionEnable();
valueListBox.saveBoxOrderToSettings();
valueListBox.determineRowMoveActionEnable();
return true;
}
}

View File

@ -1,6 +1,5 @@
"use strict";
import Gdk from "gi://Gdk";
import Gtk from "gi://Gtk";
import GObject from "gi://GObject";
import Adw from "gi://Adw";
@ -8,7 +7,6 @@ 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
@ -27,11 +25,6 @@ export default class PrefsPage extends Adw.PreferencesPage {
}, this);
}
_dndEnded?: boolean;
_left_box_order_list_box!: PrefsBoxOrderListBox;
_center_box_order_list_box!: PrefsBoxOrderListBox;
_right_box_order_list_box!: PrefsBoxOrderListBox;
constructor(params = {}) {
super(params);
@ -44,27 +37,23 @@ export default class PrefsPage extends Adw.PreferencesPage {
* operation is in progress and the user has their cursor either in the
* upper or lower 10% of this widget respectively.
*/
#setupDNDScroll(): void {
#setupDNDScroll() {
// 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);
const scrollManager = new ScrollManager(this.get_first_child());
/// 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).
// Scroll, when the pointer is in the right places.
controller.connect("motion", (_, _x, y) => {
if ((y <= this.get_allocated_height() * 0.1) && !this._dndEnded) {
if (y <= this.get_allocated_height() * 0.1) {
// 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) {
} else if (y >= this.get_allocated_height() * 0.9) {
// If the pointer is currently in the lower ten percent of this
// widget, then scroll down.
scrollManager.startScrollDown();
@ -74,9 +63,11 @@ export default class PrefsPage extends Adw.PreferencesPage {
}
});
// Make sure scrolling stops, when DND operation ends.
this._dndEnded = true;
const stopScrollAllAtDNDEnd = () => {
this._dndEnded = true;
scrollManager.stopScrollAll();
this._dndEnded = true;
};
controller.connect("leave", () => {
stopScrollAllAtDNDEnd();
@ -85,14 +76,7 @@ export default class PrefsPage extends Adw.PreferencesPage {
// 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;
}
let drag = controller.get_drop().get_drag();
drag.connect("drop-performed", () => {
stopScrollAllAtDNDEnd();
});
@ -109,7 +93,7 @@ export default class PrefsPage extends Adw.PreferencesPage {
this.add_controller(controller);
}
onRowMove(listBox: PrefsBoxOrderListBox, row: PrefsBoxOrderItemRow, direction: string): void {
onRowMove(listBox, row, direction) {
const rowPosition = row.get_index();
if (direction === "up") { // If the direction of the move is up.

View File

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

View File

@ -1,22 +0,0 @@
{
"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"
]
}