Compare commits

..

15 Commits
v13 ... main

Author SHA1 Message Date
June
140c635081
other: bump to version 15 2025-10-03 17:23:05 +02:00
June
a92ef52034
feature: support GNOME Shell version 49
Checked the source of 49.0 and tested in Fedora 43 Beta and there don't
seem to be any changes relevant to the functionality of this extension.
2025-10-03 17:22:03 +02:00
June
4071b79974
docs: add newer, cut down and commented panel.js from GNOME Shell 49.0 2025-10-03 16:36:09 +02:00
June
a477d3b95a
fix: set selected visibility option to settings value on options open 2025-07-15 19:35:48 +02:00
June
bcb61b51ac
other: bump to version 14 2025-07-09 02:09:59 +02:00
June
943d0d1fe7
other: add GitLab issue template for reporting a bug
Having this template hopefully results in bug reports with more
(relevant) information from the get-go.
2025-07-09 01:23:10 +02:00
June
fdbacdd683
feature: add settings UI for control. how to affect an items visibility
Add settings UI for controlling how the extension should affect a top
bar items visibility (whether to try to forcefully hide or show an item
or not affect its visibility at all).
2025-07-08 02:55:57 +02:00
June
0d51b81041
refactor: more nicely get the data for panelBox in #orderTopBarItems 2025-06-12 03:24:18 +02:00
June
114e1335d1
refactor: remove unneces. check for empty array in #getResolvedBoxOrder
Remove unnecessary check for empty array in #getResolvedBoxOrder as
nothing happens when the array is empty anyway.
2025-06-12 01:30:43 +02:00
June
1e87992081
refactor: directly create set of indicator cont. in getValidBoxOrder
Skip the unecessary intermediate variable and directly create a set.
Also remove the "ToDo: simplify" comment as I don't see how this logic
can be simplified more really.
2025-06-12 01:26:44 +02:00
June
5a09b1a2c8
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-12 00:56:10 +02:00
June
ff75debabc
fix: have onDrop handler methods correctly return booleans
Have the onDrop methods, used as handlers for Gtk.DropTarget drop
signals, return booleans as required by Gtk.DropTarget.
https://docs.gtk.org/gtk4/signal.DropTarget.drop.html
This change doesn't seem to have a practical impact, but its good to
follow the API correctly anyway.
2025-06-11 00:55:10 +02:00
June
979e770057
update: properly handle top bar items of Task Up UltraLite extension
Since the Task Up UltraLite extension creates a bunch of top bar items
as part of its functionality, the Top Bar Organizer settings would get
spammed with items, making them hard to navigate and making it
practically impossible to manage the top bar items of the Task Up
UltraLite extension itself. Therefore introduce functionality for
properly handling the Task Up UltraLite top bar items, by grouping them
internally and just exposing a single Top Bar Organizer settings item
for all the Task Up UltraLite items, which then allows to manage the
Task Up UltraLite top bar items nicely.

This fixes #25:
https://gitlab.gnome.org/june/top-bar-organizer/-/issues/25

Task Up UltraLite extension:
https://extensions.gnome.org/extension/7700/task-up-ultralite/
2025-06-10 03:54:30 +02:00
June
185a48c857
fix: use row title to make settings window not break for long item names
Use the title of the PrefsBoxOrderItemRow (AdwActionRow) for the item
name instead of a label in the prefix. Aside from generally being more
correct, item names now wrap correctly, avoiding the settings window
breaking (being cut off by default to the right with even the close
button not showing, until resizing) with long item names.
2025-06-09 19:53:12 +02:00
June
a58ddc6146
other: add GNOME 48.2 panel source code file to .eslintignore 2025-06-09 18:33:10 +02:00
22 changed files with 1748 additions and 238 deletions

View File

@ -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

View File

@ -4,3 +4,4 @@
/docs/panel_45.0_2023-09-26.js
/docs/panel_46.4_2024-09-11.js
/docs/panel_47.rc_2024-09-12.js
/docs/panel_48.2_2025-06-08.js

1
.gitignore vendored
View File

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

View File

@ -0,0 +1,49 @@
### Bug Description
<!--
Description of the issue you're experiencing.
-->
### Steps to Reproduce
<!--
Steps to reproduce the issue.
1. Open ...
2. Click on ...
...
-->
### What behavior did you observe?
<!--
What actually happened that was unexpected?
-->
### What behavior did you expect?
<!--
What did you expect to happen instead?
-->
### Further Information
<!--
If applicable: Screenshots and/or screencasts, which show the issue.
-->
<!--
If applicable: Logs, which show relevant information like error messages.
- For core extension issues, open the terminal and run: journalctl -f, then reproduce the issue and paste the relevant messages here in a code block (```).
- For extension preferences issues, open the terminal and run: journalctl -f -o cat /usr/bin/gjs, then reproduce the issue and paste the relevant messages here in a code block (```).
-->
### System Information
- Operating System: <!-- What operating system are you using? -->
- GNOME Version: <!-- What GNOME version are you running? Open a terminal and run: gnome-shell --version, then paste the output here. -->
Enabled Extensions:
<!--
Which extensions do you have enabled? Open a terminal and run: gnome-extensions list --enabled --details, then paste the output here in a code block (```).
-->

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"

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="PrefsBoxOrderItemOptionsDialog" parent="AdwDialog">
<!-- Same as the default-width of AdwPreferencesWindow.-->
<property name="content-width">640</property>
<child>
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar"></object>
</child>
<property name="content">
<object class="AdwPreferencesPage">
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwComboRow" id="visibility-row">
<property name="title">Visibility</property>
<property name="subtitle">Forcefully hide or show an item or just don't affect its visibility. This option applies every time the top bar gets reordered, an items visiblity might be influenced by other factors and this option might not work for every item.</property>
<property name="model">
<object class="GtkStringList">
<items>
<item>Default</item>
<item>Forcefully Hide</item>
<item>Forcefully Show</item>
</items>
</object>
</property>
<signal name="notify::selected-item" handler="onVisibilityRowSelectionChanged"/>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
</template>
</interface>

View File

@ -1,12 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="PrefsBoxOrderItemRow" parent="AdwActionRow">
<child type="prefix">
<object class="GtkLabel" id="item-name-display-label">
<property name="halign">start</property>
<property name="hexpand">True</property>
</object>
</child>
<child type="prefix">
<object class="GtkImage">
<property name="icon-name">list-drag-handle-symbolic</property>
@ -52,6 +46,12 @@
<attribute name="action">row.move-down</attribute>
</item>
</section>
<section>
<item>
<attribute name="label">Options</attribute>
<attribute name="action">row.options</attribute>
</item>
</section>
<section>
<item>
<attribute name="label">Forget</attribute>

View File

@ -0,0 +1,321 @@
// My annotated and cut down js/ui/panel.js from gnome-shell/49.0.
// All annotations are what I guessed, interpreted and copied while reading the
// code and comparing to other panel.js versions and might be wrong. They are
// prefixed with "Annotation:" to indicate that they're my comments, not
// comments that orginally existed.
// Taken from: https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/49.0/js/ui/panel.js
// On: 2025-10-03
// License: This code is licensed under GPLv2.
// Taken from: https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/49.0/js/ui/sessionMode.js
// On: 2025-10-03
// License: This code is licensed under GPLv2.
// I'm using the word "item" to refer to the thing, which gets added to the top
// (menu)bar / panel, where an item has a role/name and an indicator.
// Annotation: [...] Cut out bunch of stuff here, which isn't relevant for this
// Extension.
import Clutter from 'gi://Clutter';
// Annotation: [...] Cut out bunch of stuff here, which isn't relevant for this
// Extension.
import GObject from 'gi://GObject';
// Annotation: [...] Cut out bunch of stuff here, which isn't relevant for this
// Extension.
import St from 'gi://St';
// Annotation: [...] Cut out bunch of stuff here, which isn't relevant for this
// Extension.
import * as CtrlAltTab from './ctrlAltTab.js';
// Annotation: [...] Cut out bunch of stuff here, which isn't relevant for this
// Extension.
import * as PopupMenu from './popupMenu.js';
import * as PanelMenu from './panelMenu.js';
// Annotation: [...] Cut out bunch of stuff here, which isn't relevant for this
// Extension.
import * as Main from './main.js';
// Annotation: [...] Cut out bunch of stuff here, which isn't relevant for this
// Extension.
import {DateMenuButton} from './dateMenu.js';
import {ATIndicator} from './status/accessibility.js';
import {InputSourceIndicator} from './status/keyboard.js';
import {DwellClickIndicator} from './status/dwellClick.js';
import {ScreenRecordingIndicator, ScreenSharingIndicator} from './status/remoteAccess.js';
// Annotation: [...] Cut out bunch of stuff here, which isn't relevant for this
// Extension.
// Of note (for PANEL_ITEM_IMPLEMENTATIONS):
// const ActivitiesButton = [...]
// const QuickSettings = [...]
// Compared to panel_48.2_2025-06-08.js: AppMenuButton got removed.
// Compared to panel_48.2_2025-06-08.js: AppMenuButton got removed.
const PANEL_ITEM_IMPLEMENTATIONS = {
'activities': ActivitiesButton,
'quickSettings': QuickSettings,
'dateMenu': DateMenuButton,
'a11y': ATIndicator,
'keyboard': InputSourceIndicator,
'dwellClick': DwellClickIndicator,
'screenRecording': ScreenRecordingIndicator,
'screenSharing': ScreenSharingIndicator,
};
export const Panel = GObject.registerClass(
class Panel extends St.Widget {
// Annotation: Initializes the top (menu)bar / panel.
// Does relevant stuff like:
// - Defining this._leftBox, this._centerBox and this._rightBox.
// - Finally calling this._updatePanel().
// Compared to panel_48.2_2025-06-08.js: Nothing changed.
_init() {
super._init({
name: 'panel',
reactive: true,
});
this.set_offscreen_redirect(Clutter.OffscreenRedirect.ALWAYS);
this._sessionStyle = null;
this.statusArea = {};
this.menuManager = new PopupMenu.PopupMenuManager(this);
this._leftBox = new St.BoxLayout({name: 'panelLeft'});
this.add_child(this._leftBox);
this._centerBox = new St.BoxLayout({name: 'panelCenter'});
this.add_child(this._centerBox);
this._rightBox = new St.BoxLayout({name: 'panelRight'});
this.add_child(this._rightBox);
this.connect('button-press-event', this._onButtonPress.bind(this));
this.connect('touch-event', this._onTouchEvent.bind(this));
Main.overview.connectObject('showing',
() => this.add_style_pseudo_class('overview'),
this);
Main.overview.connectObject('hiding',
() => this.remove_style_pseudo_class('overview'),
this);
Main.layoutManager.panelBox.add_child(this);
Main.ctrlAltTabManager.addGroup(this,
_('Top Bar'), 'shell-focus-top-bar-symbolic',
{sortGroup: CtrlAltTab.SortGroup.TOP});
Main.sessionMode.connectObject('updated',
this._updatePanel.bind(this),
this);
global.display.connectObject('workareas-changed',
() => this.queue_relayout(),
this);
this._updatePanel();
}
// Annotation: [...] Cut out bunch of stuff here, which isn't relevant for
// this Extension.
// Annotation: Gets called by this._init() to populate the top (menu)bar /
// panel initially.
//
// It does the following relevant stuff:
// - Calls this._hideIndicators()
// - Calls this._updateBox() for this._leftBox, this._centerBox and
// this._rightBox with panel.left, panel.center and panel.right to
// populate the boxes with items defined in panel.left, panel.center and
// panel.right.
//
// panel.left, panel.center and panel.right get set via the line let panel
// = Main.sessionMode.panel, which uses the panel of Mains (js/ui/main.js)
// instance of SessionMode (js/ui/sessionMode.js).
//
// And in js/ui/sessionMode.js (49.0, 2025-10-03) you have different modes
// with different panel configuration. For example the "user" mode with:
// panel: {
// left: ['activities'],
// center: ['dateMenu'],
// right: ['screenRecording', 'screenSharing', 'dwellClick', 'a11y', 'keyboard', 'quickSettings'],
// }
//
// This way this function populates the top (menu)bar / panel with the
// default stuff you see on a fresh Gnome.
//
// Compared to panel_48.2_2025-06-08.js: Nothing changed.
_updatePanel() {
let panel = Main.sessionMode.panel;
this._hideIndicators();
this._updateBox(panel.left, this._leftBox);
this._updateBox(panel.center, this._centerBox);
this._updateBox(panel.right, this._rightBox);
if (panel.left.includes('dateMenu'))
Main.messageTray.bannerAlignment = Clutter.ActorAlign.START;
else if (panel.right.includes('dateMenu'))
Main.messageTray.bannerAlignment = Clutter.ActorAlign.END;
// Default to center if there is no dateMenu
else
Main.messageTray.bannerAlignment = Clutter.ActorAlign.CENTER;
if (this._sessionStyle)
this.remove_style_class_name(this._sessionStyle);
this._sessionStyle = Main.sessionMode.panelStyle;
if (this._sessionStyle)
this.add_style_class_name(this._sessionStyle);
}
// Annotation: This function hides all items, which are in the top (menu)bar
// panel and in PANEL_ITEM_IMPLEMENTATIONS.
//
// Compared to panel_48.2_2025-06-08.js: Nothing changed.
_hideIndicators() {
for (let role in PANEL_ITEM_IMPLEMENTATIONS) {
let indicator = this.statusArea[role];
if (!indicator)
continue;
indicator.container.hide();
}
}
// Annotation: This function takes a role (of an item) and returns a
// corresponding indicator, if either of two things are true:
// - The indicator is already in this.statusArea.
// Then it just returns the indicator by using this.statusArea.
// - The role is in PANEL_ITEM_IMPLEMENTATIONS.
// Then it creates a new indicator, adds it to this.statusArea and returns
// it.
//
// Compared to panel_48.2_2025-06-08.js: Nothing changed.
_ensureIndicator(role) {
let indicator = this.statusArea[role];
if (!indicator) {
let constructor = PANEL_ITEM_IMPLEMENTATIONS[role];
if (!constructor) {
// This icon is not implemented (this is a bug)
return null;
}
indicator = new constructor(this);
this.statusArea[role] = indicator;
}
return indicator;
}
// Annotation: This function takes a list of items (or rather their roles)
// and adds the indicators of those items to a box (like this._leftBox)
// using this._ensureIndicator() to get the indicator corresponding to the
// given role.
// So only items with roles this._ensureIndicator() knows, get added.
//
// Compared to panel_48.2_2025-06-08.js: Nothing changed.
_updateBox(elements, box) {
let nChildren = box.get_n_children();
for (let i = 0; i < elements.length; i++) {
let role = elements[i];
let indicator = this._ensureIndicator(role);
if (indicator == null)
continue;
this._addToPanelBox(role, indicator, i + nChildren, box);
}
}
// Annotation: This function adds the given item to the specified top
// (menu)bar / panel box and connects to "destroy" and "menu-set" events.
//
// It takes the following arguments:
// - role: The name of the item to add.
// - indicator: The indicator of the item to add.
// - position: Where in the box to add the item.
// - box: The box to add the item to.
// Can be one of the following:
// - this._leftBox
// - this._centerBox
// - this._rightBox
//
// Compared to panel_48.2_2025-06-08.js: Nothing changed.
_addToPanelBox(role, indicator, position, box) {
let container = indicator.container;
container.show();
let parent = container.get_parent();
if (parent)
parent.remove_child(container);
box.insert_child_at_index(container, position);
this.statusArea[role] = indicator;
let destroyId = indicator.connect('destroy', emitter => {
delete this.statusArea[role];
emitter.disconnect(destroyId);
});
indicator.connect('menu-set', this._onMenuSet.bind(this));
this._onMenuSet(indicator);
}
// Annotation: This function allows you to add an item to the top (menu)bar
// / panel.
// While per default it adds the item to the status area (the right box of
// the top bar), you can specify the box and add the item to any of the
// three boxes of the top bar.
// To add an item to the top bar, you need to give its role and indicator.
//
// This function takes the following arguments:
// - role: A name for the item to add.
// - indicator: The indicator for the item to add (must be an instance of
// PanelMenu.Button).
// - position: Where in the box to add the item.
// - box: The box to add the item to.
// Can be one of the following:
// - "left": referring to this._leftBox
// - "center": referring to this._centerBox
// - "right": referring to this._rightBox
// These boxes are what you see in the top bar as the left, right and
// center sections.
//
// Finally this function just calls this._addToPanelBox() for the actual
// work, so it basically just makes sure the input to this._addToPanelBox()
// is correct.
//
// Compared to panel_48.2_2025-06-08.js: Nothing changed.
addToStatusArea(role, indicator, position, box) {
if (this.statusArea[role])
throw new Error(`Extension point conflict: there is already a status indicator for role ${role}`);
if (!(indicator instanceof PanelMenu.Button))
throw new TypeError('Status indicator must be an instance of PanelMenu.Button');
position ??= 0;
let boxes = {
left: this._leftBox,
center: this._centerBox,
right: this._rightBox,
};
let boxContainer = boxes[box] || this._rightBox;
this.statusArea[role] = indicator;
this._addToPanelBox(role, indicator, position, boxContainer);
return indicator;
}
// Of note:
// _onMenuSet(indicator) { [...] }
// Annotation: [...] Cut out bunch of stuff here, which isn't relevant for
// this Extension.
});

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

View File

@ -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 \

View File

@ -1,13 +1,27 @@
"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 {
enable() {
_settings!: Gio.Settings;
_boxOrderManager!: BoxOrderManager;
_settingsHandlerIds!: number[];
enable(): void {
this._settings = this.getSettings();
this._boxOrderManager = new BoxOrderManager({}, this._settings);
@ -26,7 +40,7 @@ export default class TopBarOrganizerExtension extends Extension {
// Handle changes of settings.
this._settingsHandlerIds = [];
const addSettingsChangeHandler = (settingsName) => {
const addSettingsChangeHandler = (settingsName: string) => {
const handlerId = this._settings.connect(`changed::${settingsName}`, () => {
this.#handleNewItemsAndOrderTopBar();
});
@ -39,10 +53,12 @@ export default class TopBarOrganizerExtension extends Extension {
addSettingsChangeHandler("show");
}
disable() {
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.
@ -51,7 +67,9 @@ export default class TopBarOrganizerExtension extends Extension {
}
this._boxOrderManager.disconnectSignals();
// @ts-ignore
this._settings = null;
// @ts-ignore
this._boxOrderManager = null;
}
@ -63,9 +81,10 @@ 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() {
#overwritePanelAddToPanelBox(): void {
// Add the original `Panel._addToPanelBox` method as
// `Panel._originalAddToPanelBox`.
// @ts-ignore
Panel.Panel.prototype._originalAddToPanelBox = Panel.Panel.prototype._addToPanelBox;
const handleNewItemsAndOrderTopBar = () => {
@ -76,6 +95,7 @@ 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();
};
@ -88,9 +108,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 {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
// is "user".
if(Main.sessionMode.currentMode !== "user" && Main.sessionMode.parentMode !== "user") {
@ -101,30 +121,26 @@ export default class TopBarOrganizerExtension extends Extension {
const validBoxOrder = this._boxOrderManager.getValidBoxOrder(box);
// Get the relevant box of `Main.panel`.
let panelBox;
switch (box) {
case "left":
panelBox = Main.panel._leftBox;
break;
case "center":
panelBox = Main.panel._centerBox;
break;
case "right":
panelBox = Main.panel._rightBox;
break;
}
let panelBox = (Main.panel as CustomPanel)[`_${box}Box`];
/// Go through the items of the validBoxOrder and order the GNOME Shell
/// top bar box accordingly.
for (let i = 0; i < validBoxOrder.length; i++) {
const item = validBoxOrder[i];
// Get the indicator container associated with the current role.
const associatedIndicatorContainer = Main.panel.statusArea[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.
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 the target panel box is the right panel box, insert the
// indicator container at index `-1`, which just adds it to the
@ -165,7 +181,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() {
#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") {

View File

@ -1,23 +1,29 @@
"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.
* It takes care of handling AppIndicator items and resolving from the internal
* item settings identifiers to roles.
* 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.
*/
@ -30,45 +36,42 @@ export default class BoxOrderManager extends GObject.Object {
}, this);
}
#appIndicatorReadyHandlerIdMap;
#appIndicatorItemSettingsIdToRolesMap;
#settings;
// 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) {
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 {string} box - The top bar box for which to get the box order.
* Must be one of the following values:
* - "left"
* - "center"
* - "right"
* @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) {
#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 {string} box - The top bar box for which to save the box order.
* Must be one of the following values:
* - "left"
* - "center"
* - "right"
* @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, boxOrder) {
#saveBoxOrder(box: Box, boxOrder: string[]): void {
const currentBoxOrder = this.#getBoxOrder(box);
// Only save the given box order to settings, if it is different, to
@ -88,14 +91,18 @@ 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 {string} indicatorContainer - The container of the indicator of the
* @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, role) {
const appIndicator = indicatorContainer.get_child()._indicator;
#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) {
@ -134,19 +141,36 @@ export default class BoxOrderManager extends GObject.Object {
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 items got resolved using their roles, meaning they might be
* present multiple times or not at all depending on the roles stored.
* 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 {string} box - The top bar box for which to get the resolved box order.
* Must be one of the following values:
* - "left"
* - "center"
* - "right"
* @param {Box} box - The top bar box for which to get the resolved box order.
* @returns {ResolvedBoxOrderItem[]} - The resolved box order.
*/
#getResolvedBoxOrder(box) {
#getResolvedBoxOrder(box: Box): ResolvedBoxOrderItem[] {
let boxOrder = this.#getBoxOrder(box);
const itemsToHide = this.#settings.get_strv("hide");
@ -170,9 +194,11 @@ export default class BoxOrderManager extends GObject.Object {
}
// If the items settings identifier doesn't indicate that the item
// is an AppIndicator/KStatusNotifierItem item, then its identifier
// is the role and it can just be added to the resolved box order.
if (!itemSettingsId.startsWith("appindicator-kstatusnotifieritem-")) {
// 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;
@ -181,16 +207,16 @@ export default class BoxOrderManager extends GObject.Object {
// If the items settings identifier indicates otherwise, then handle
// the item specially.
// Get the roles roles associated with the items settings id.
let roles = this.#appIndicatorItemSettingsIdToRolesMap.get(resolvedBoxOrderItem.settingsId);
// If there are no roles associated, continue.
if (!roles) {
continue;
// 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;
}
// Otherwise create a new resolved box order item for each role and
// add it to the resolved box order.
// 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;
@ -206,53 +232,50 @@ 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() {
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
* items got resolved and where only items are included, which are in some
* GNOME Shell top bar box.
* 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 {string} box - The top bar box to return the valid box order for.
* Must be one of the following values:
* - "left"
* - "center"
* - "right"
* @param {Box} box - The top bar box to return the valid box order for.
* @returns {ResolvedBoxOrderItem[]} - The valid box order.
*/
getValidBoxOrder(box) {
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.
const indicatorContainers = [
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.
const indicatorContainerSet = new Set(indicatorContainers);
// 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 = new Set([
(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));
// 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 = [];
let validBoxOrder: ResolvedBoxOrderItem[] = [];
for (const item of resolvedBoxOrder) {
// Get the indicator container associated with the items 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;
}
if (indicatorContainerSet.has(associatedIndicatorContainer)) {
if (indicatorContainers.has(associatedIndicatorContainer)) {
validBoxOrder.push(item);
}
}
@ -264,7 +287,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() {
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") {
@ -280,24 +303,31 @@ 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();
for (const role in Main.panel.statusArea) {
indicatorContainerRoleMap.set(Main.panel.statusArea[role].container, role);
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._leftBox.get_children(),
center: Main.panel._centerBox.get_children(),
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._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
// 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) {
// First get the role associated with the current indicator
// container.
@ -308,18 +338,25 @@ export default class BoxOrderManager extends GObject.Object {
// 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
if (role.startsWith("appindicator-")) {
// 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;
}

View File

@ -2,8 +2,8 @@
"uuid": "top-bar-organizer@julian.gse.jsts.xyz",
"name": "Top Bar Organizer",
"description": "Organize the items of the top (menu)bar.",
"version": 13,
"shell-version": [ "45", "46", "47", "48" ],
"version": 15,
"shell-version": [ "45", "46", "47", "48", "49" ],
"settings-schema": "org.gnome.shell.extensions.top-bar-organizer",
"url": "https://gitlab.gnome.org/june/top-bar-organizer"
}

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,
(defaultGdkDisplay as Gdk.Display),
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,
(defaultGdkDisplay as Gdk.Display),
provider
);
});

View File

@ -0,0 +1,69 @@
"use strict";
import GObject from "gi://GObject";
import Adw from "gi://Adw";
import GLib from "gi://GLib";
import type Gio from "gi://Gio";
import type Gtk from "gi://Gtk";
import { ExtensionPreferences } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";
export default class PrefsBoxOrderItemOptionsDialog extends Adw.Dialog {
static {
GObject.registerClass({
GTypeName: "PrefsBoxOrderItemOptionsDialog",
Template: GLib.uri_resolve_relative(import.meta.url, "../ui/prefs-box-order-item-options-dialog.ui", GLib.UriFlags.NONE),
InternalChildren: [
"visibility-row",
],
}, this);
}
declare _visibility_row: Adw.ComboRow;
#settings: Gio.Settings;
item: string;
constructor(params = {}, item: string) {
super(params);
// Associate `this` with an item.
this.item = item;
// Load the settings.
this.#settings = ExtensionPreferences.lookupByURL(import.meta.url)!.getSettings();
// Set the selected visibility row choice to the settings value.
const itemsToHide = new Set(this.#settings.get_strv("hide"));
const itemsToShow = new Set(this.#settings.get_strv("show"));
if (itemsToHide.has(this.item)) {
this._visibility_row.set_selected(1);
} else if (itemsToShow.has(this.item)) {
this._visibility_row.set_selected(2);
} else {
this._visibility_row.set_selected(0);
}
}
onVisibilityRowSelectionChanged(): void {
const visibility = (this._visibility_row.get_selected_item() as Gtk.StringObject).get_string();
const itemsToHide = new Set(this.#settings.get_strv("hide"));
const itemsToShow = new Set(this.#settings.get_strv("show"));
switch (visibility) {
case "Forcefully Hide":
itemsToHide.add(this.item)
itemsToShow.delete(this.item);
break;
case "Forcefully Show":
itemsToHide.delete(this.item)
itemsToShow.add(this.item);
break;
case "Default":
itemsToHide.delete(this.item)
itemsToShow.delete(this.item);
break;
}
this.#settings.set_strv("hide", Array.from(itemsToHide));
this.#settings.set_strv("show", Array.from(itemsToShow));
}
}

View File

@ -6,14 +6,14 @@ import GObject from "gi://GObject";
import Adw from "gi://Adw";
import GLib from "gi://GLib";
import PrefsBoxOrderItemOptionsDialog from "./PrefsBoxOrderItemOptionsDialog.js";
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),
InternalChildren: [
"item-name-display-label",
],
Signals: {
"move": {
param_types: [GObject.TYPE_STRING],
@ -21,44 +21,50 @@ export default class PrefsBoxOrderItemRow extends Adw.ActionRow {
},
}, this);
this.install_action("row.forget", null, (self, _actionName, _param) => {
const parentListBox = self.get_parent();
parentListBox.removeRow(self);
const parentListBox = self.get_parent() as PrefsBoxOrderListBox;
parentListBox.removeRow(self as PrefsBoxOrderItemRow);
parentListBox.saveBoxOrderToSettings();
parentListBox.determineRowMoveActionEnable();
});
this.install_action("row.options", null, (self, _actionName, _param) => {
const itemOptionsDialog = new PrefsBoxOrderItemOptionsDialog({
// Get the title from self as the constructor of
// PrefsBoxOrderItemRow already processes the item name into a
// nice title.
title: (self as PrefsBoxOrderItemRow).get_title()
}, (self as PrefsBoxOrderItemRow).item);
itemOptionsDialog.present(self);
});
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"));
}
#drag_starting_point_x;
#drag_starting_point_y;
item: string;
#drag_starting_point_x?: number;
#drag_starting_point_y?: number;
constructor(params = {}, item) {
constructor(params = {}, item: string) {
super(params);
this.#associateItem(item);
}
/**
* Associate `this` with an item.
* @param {String} item
*/
#associateItem(item) {
// Associate `this` with an item.
this.item = item;
if (item.startsWith("appindicator-kstatusnotifieritem-")) {
// Set `this._item_name_display_label` to something nicer, if the
// associated item is an AppIndicator/KStatusNotifierItem item.
this._item_name_display_label.set_label(item.replace("appindicator-kstatusnotifieritem-", ""));
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._item_name_display_label.set_label(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();
value.init(PrefsBoxOrderItemRow);
value.init(PrefsBoxOrderItemRow.$gtype);
value.set_object(this);
this.#drag_starting_point_x = x;
@ -66,7 +72,7 @@ export default class PrefsBoxOrderItemRow extends Adw.ActionRow {
return Gdk.ContentProvider.new_for_value(value);
}
onDragBegin(_source, drag) {
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);
@ -77,20 +83,32 @@ 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);
}
}
// Handle a new drop on `this` properly.
// `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 (value === this) {
return;
return false;
}
// Get the GtkListBoxes of `this` and the drop value.
const ownListBox = this.get_parent();
const valueListBox = value.get_parent();
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();
@ -137,5 +155,7 @@ export default class PrefsBoxOrderItemRow extends Adw.ActionRow {
valueListBox.saveBoxOrderToSettings();
valueListBox.determineRowMoveActionEnable();
}
return true;
}
}

View File

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

View File

@ -4,6 +4,9 @@ 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({
@ -14,10 +17,18 @@ export default class PrefsBoxOrderListEmptyPlaceholder extends Gtk.Box {
// Handle a new drop on `this` properly.
// `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.
const ownListBox = this.get_parent();
const valueListBox = value.get_parent();
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);
@ -31,5 +42,7 @@ export default class PrefsBoxOrderListEmptyPlaceholder extends Gtk.Box {
ownListBox.determineRowMoveActionEnable();
valueListBox.saveBoxOrderToSettings();
valueListBox.determineRowMoveActionEnable();
return true;
}
}

View File

@ -1,5 +1,6 @@
"use strict";
import Gdk from "gi://Gdk";
import Gtk from "gi://Gtk";
import GObject from "gi://GObject";
import Adw from "gi://Adw";
@ -7,6 +8,7 @@ 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
@ -25,6 +27,11 @@ export default class PrefsPage extends Adw.PreferencesPage {
}, 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);
@ -37,23 +44,27 @@ 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() {
#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());
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();
// 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) => {
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
// widget, then scroll up.
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
// widget, then scroll down.
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 = () => {
scrollManager.stopScrollAll();
this._dndEnded = true;
scrollManager.stopScrollAll();
};
controller.connect("leave", () => {
stopScrollAllAtDNDEnd();
@ -76,7 +85,14 @@ export default class PrefsPage extends Adw.PreferencesPage {
// Make use of `this._dndEnded` to setup stopScrollAtDNDEnd only
// once per DND operation.
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", () => {
stopScrollAllAtDNDEnd();
});
@ -93,7 +109,7 @@ export default class PrefsPage extends Adw.PreferencesPage {
this.add_controller(controller);
}
onRowMove(listBox, row, direction) {
onRowMove(listBox: PrefsBoxOrderListBox, row: PrefsBoxOrderItemRow, direction: string): void {
const rowPosition = row.get_index();
if (direction === "up") { // If the direction of the move is up.

View File

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