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 @@ + + + + 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")); }