diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
new file mode 100644
index 0000000..2caef4d
--- /dev/null
+++ b/.gitlab/issue_templates/Bug.md
@@ -0,0 +1,49 @@
+### Bug Description
+
+
+
+### Steps to Reproduce
+
+
+
+### What behavior did you observe?
+
+
+
+### What behavior did you expect?
+
+
+
+### Further Information
+
+
+
+
+
+### System Information
+
+- Operating System:
+- GNOME Version:
+
+Enabled Extensions:
+
+
diff --git a/data/ui/prefs-box-order-item-options-dialog.ui b/data/ui/prefs-box-order-item-options-dialog.ui
new file mode 100644
index 0000000..d953d24
--- /dev/null
+++ b/data/ui/prefs-box-order-item-options-dialog.ui
@@ -0,0 +1,38 @@
+
+
+
+
+ 640
+
+
+
+
+
diff --git a/data/ui/prefs-box-order-item-row.ui b/data/ui/prefs-box-order-item-row.ui
index 0debab6..dd06b32 100644
--- a/data/ui/prefs-box-order-item-row.ui
+++ b/data/ui/prefs-box-order-item-row.ui
@@ -46,6 +46,12 @@
row.move-down
+
+
+ Options
+ row.options
+
+ Forget
diff --git a/docs/panel_49.0_2025-10-03.js b/docs/panel_49.0_2025-10-03.js
new file mode 100644
index 0000000..449c038
--- /dev/null
+++ b/docs/panel_49.0_2025-10-03.js
@@ -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.
+});
diff --git a/src/extension.ts b/src/extension.ts
index 4d99243..026de7e 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -121,18 +121,7 @@ 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 as CustomPanel)._leftBox;
- break;
- case "center":
- panelBox = (Main.panel as CustomPanel)._centerBox;
- break;
- case "right":
- panelBox = (Main.panel as CustomPanel)._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.
diff --git a/src/metadata.json b/src/metadata.json
index a12d4c0..11b0cce 100644
--- a/src/metadata.json
+++ b/src/metadata.json
@@ -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"
}
diff --git a/src/prefsModules/PrefsBoxOrderItemOptionsDialog.ts b/src/prefsModules/PrefsBoxOrderItemOptionsDialog.ts
new file mode 100644
index 0000000..736a773
--- /dev/null
+++ b/src/prefsModules/PrefsBoxOrderItemOptionsDialog.ts
@@ -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));
+ }
+}
diff --git a/src/prefsModules/PrefsBoxOrderItemRow.ts b/src/prefsModules/PrefsBoxOrderItemRow.ts
index fa691f8..3da4d6b 100644
--- a/src/prefsModules/PrefsBoxOrderItemRow.ts
+++ b/src/prefsModules/PrefsBoxOrderItemRow.ts
@@ -6,6 +6,7 @@ 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 {
@@ -25,6 +26,15 @@ export default class PrefsBoxOrderItemRow extends Adw.ActionRow {
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"));
}