Compare commits

...

114 Commits
v2 ... 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
June
b17a805035
other: bump version to 13 2025-06-09 00:24:02 +02:00
June
9c847c0988
feature: support GNOME Shell version 48
Checked the source of 48.2 and tested in Fedora 42 and there don't seem
to be any changes relevant to the functionality of this extension.
2025-06-09 00:22:48 +02:00
June
57e7fc51ea
docs: add newer, cut down and commented panel.js from GNOME Shell 48.2 2025-06-08 20:57:58 +02:00
June
f619ce4fa7
feature: make it possible to (forcefully) hide or show top bar items
This is only the core extension logic for now, settings UI still needs
to follow.
The logic only acts on the indicator container, not the indicator
itself, meaning that e.g. a screen recording indicator, which is hidden
on the indicator level, can be forcefully hidden, but not forcefully
shown. Because of that and because forcefully hiding it breaks controls
for screen recording, a potential settings implementation should exclude
visiblity controls for some elements like e.g. the screen recording
indicator.
2024-09-27 03:24:47 +02:00
June
80f394d2d4
refactor: move logic for loading and saving box orders to priv. methods
Move logic for loading and saving box orders from/to settings to private
methods to have more readable code.
2024-09-26 02:52:15 +02:00
June
cc088f443c
refactor: clearly dist. betw. an items role and the id used in settings
Clearly distinguish between an items role, which is present in GNOME
Shells top bar, call that a role, and the thing that is stored in the
settings for an item, call that an items settings identifier.
Refactor the code with these new names in mind to make it clearer.
Also make the comments clearer.
And finally simplify the code handling AppIndicator items by simply
using the items settings identifier as the key in the roles map and with
that getting rid of the confusing concept of a placeholder role, which
is only relevant for the box order stored in settings. Just declaring it
as an items settings identifier, which it is, is much clearer.
2024-09-26 02:23:28 +02:00
June
23baa41225
other: bump version to 12 2024-09-12 22:56:02 +02:00
June
07355dcf19
feature: support GNOME Shell version 47
Checked the source of 47.rc and tested in GNOME OS and there don't seem
to be any changes relevant to the functionality of this extension.
2024-09-12 22:55:05 +02:00
June
62fd014146
docs: add newer, cut down and commented panel.js from GNOME Shell 47.rc 2024-09-12 21:44:26 +02:00
June
b762d6db22
docs: fix description of release-ZIP upload process
The release-ZIP needs to be uploaded in the release notes section, not
in the release assets section (as that's apparently not possible).
2024-09-11 04:09:29 +02:00
June
b358d2046d
other: bump version to 11 2024-09-11 03:58:32 +02:00
June
ea3942057f
docs: enhance the git annotated tag and release notes templates
- Give them a better general structure by having a "Relevant and/or
  Breaking Changes" and an "Other Changes" section.
- Use more minimal markup in the git annotated tag template as that
  works better for annotated tags.
- Use nicer and more useful placeholder values.
- Get rid of unnecessary descriptions.
2024-09-11 03:58:32 +02:00
June
8fb92d1bb1
docs: exp. docs on creat. a new tag to cover how to create a new release
Also generally fix up the docs and the git_annotated_tag_template a bit,
make the tag template not include the shortlog anymore (as that doesn't
make that much sense) and also provide a release_notes_template.md.
2024-09-11 03:40:01 +02:00
June
1870a9510a
other: update my name 2024-09-11 03:16:46 +02:00
June
e04ac8a3ee
feature: support GNOME Shell version 46
There don't seem to be any changes relevant to the functionality of this
extension.
2024-09-11 03:02:02 +02:00
June
d88034f345
docs: add newer, cut down and commented panel.js from GNOME Shell 46.4 2024-09-11 02:51:20 +02:00
June
e374ad85b1
other: change commit msg format to use lowercase tags and desc. start 2024-09-11 02:51:20 +02:00
126ebfa9db
Other: Bump version to 10 2023-10-05 15:21:39 +02:00
c25f24b72f
Other: Make ESLint ignore unused import 2023-10-05 15:06:47 +02:00
7f86e92e85
Refactor: Clean up unused import 2023-10-05 14:59:46 +02:00
b98f047d22
Other: Add new version 45 panel source code to .eslintignore 2023-10-05 14:58:16 +02:00
d903274d73
Other: Require trailing commas for multiline for most
Require trailing commas for multiline for arrays, objects, imports and
exports and disallow trailing commas for functions.
Do this by updating the ESLint config and fixing new complaints.

The reason for this change are the resulting future cleaner version
diffs.

Also see:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Trailing_commas
https://eslint.org/docs/latest/rules/comma-dangle
2023-10-05 14:50:36 +02:00
88e510d54e
Other: Upgrade ESLint to latest version and run npm update 2023-10-05 14:18:11 +02:00
5ea8f4aabe
Refactor: Move GObject.registerClass calls into static init. blocks
Do that since it standardizes the code and also is just cleaner.
2023-10-04 03:58:21 +02:00
a77c6d2f2b
Refactor: Install forget action using install_action as well 2023-10-04 03:58:10 +02:00
9b7ab0614c
Feature: Add move up and down buttons to make the prefs keyboard access.
Don't use CONSTRUCT_ONLY anymore to be able to use private properties
(or any properties at all it seems).
2023-10-04 03:57:42 +02:00
a1188d5684
Breaking: Migrate extension to the new ESM system of GNOME 45
Migrate with the help of, among others, the following resources:
https://blogs.gnome.org/shell-dev/2023/09/02/extensions-in-gnome-45/
https://gjs.guide/extensions/upgrading/gnome-shell-45.html

Only support GNOME Shell version 45, since only 45 is compatible with
the new ESM system.

Since panel._originalAddToPanelBox is no longer valid, just overwrite
using the prototype on disable.

Add "sourceType": "module" to eslintrc.yml to get rid of:
"Parsing error: 'import' and 'export' may appear only with 'sourceType:
module'"
See here:
https://eslint.org/docs/latest/use/configure/language-options#specifying-parser-options
2023-10-04 03:57:35 +02:00
62e9609b2d
Docs: Add newer, cut down and commented panel.js from GNOME Shell 45.0 2023-09-26 23:25:06 +02:00
5d0aec5438
Docs: Mention community-maintained AUR package in README 2023-05-12 17:47:23 +02:00
ed10e7a14b
Other: Bump version to 9 2023-04-21 00:56:12 +02:00
0b3aff665a
Fix: Move provider add. to more correct place and remove prov. on dest.
See here for the relevant review:
https://extensions.gnome.org/review/40563
2023-04-21 00:46:59 +02:00
bf598f2a15
Other: Bump version to 8 2023-04-20 23:19:02 +02:00
51a89aec60
Feature: Support GNOME version 44 2023-04-20 23:17:06 +02:00
5c8523322b
Update: Improve drag handle styling by using GNOME Settings style
This also finally makes the style `<class name="drag-handle"/>` in
`prefs-box-order-item-row.ui` actually do something.
2023-04-20 12:33:30 +02:00
fa379607ee
Other: Bump version to 7 2023-02-07 12:52:03 +01:00
470ebf761d
Fix: Keep hidden items hidden 2023-02-07 12:52:03 +01:00
39face9957
Fix: Also interact with the panel, if the parent session mode is "user"
Top Bar Organizer used to only interact with the panel, when the current
session mode is "user". However Ubuntu uses "ubuntu" as its session
mode, which made Top Bar Organizer not work.
Fix this by also interacting with the panel, if the parent session mode
is "user".

See here for some docs on session modes:
https://gjs.guide/extensions/topics/session-modes.html
2023-02-07 12:52:03 +01:00
4be693a134
Docs: Add README with screenshot, extension desc. and installation notes 2023-01-30 18:10:52 +01:00
6bc40441a7
Other: Bump version to 6 2023-01-30 15:33:14 +01:00
40846915e7
Fix: Check, if signal handler is connected for handler id, before dc.
Do this to hopefully fix "No signal connection XX found" errors.
2023-01-30 15:28:58 +01:00
40cf8de1a8
Fix: Don't affect (or interact with) panel of unlock screen
The panel gets set to the state it should have for the unlock screen
before extension disable. Because of this the extension makes icons
show, which shouldn't show. So fix that by not ordering the panel, if
the current session mode isn't "user".
Also generally don't interact with the panel, if the current session
mode isn't "user".
2023-01-30 14:46:56 +01:00
680c2a97af
Other: Bump version to 5 2023-01-29 03:41:02 +01:00
1b5ba38617
Refactor: Rename this.settings to this._settings
Do this to indicate that this is a "private" field.
2023-01-29 03:24:17 +01:00
8586f095fc
Refactor: Reorder enable code and comment it, to make it more clear 2023-01-29 00:41:46 +01:00
d01fb434ee
Other: Remove unused typedef PositionAndBoxOverwrite 2023-01-29 00:34:53 +01:00
6e61014331
Refactor: Introduce #handleNewItemsAndOrderTopBar method
Introducing this method cleans up code, which did pretty much the same
thing, by replacing it with a call to this method.
2023-01-29 00:34:44 +01:00
c61776d8d4
Other: Remove unused typedef BoxOrders 2023-01-28 23:56:40 +01:00
58a078c15c
Refactor: Use private fields instead of pref. with _, where suitable 2023-01-28 23:51:43 +01:00
cedb54ed64
Other: Disallow omitting curly braces and enforce one true brace style
Do this by updating the ESLint config accordingly and also fix new
complaints.
2023-01-28 23:34:15 +01:00
8de94d6b01
Other: Add whitespace rules from GJS Style Guide and fix complaints
Add whitespace rules from GJS Style Guide to ESLint config and fix new
complaints.
See here for the GJS Style guide whitespace section:
https://gjs.guide/guides/gjs/style-guide.html#whitespace
2023-01-28 23:13:46 +01:00
0401790fed
Refactor: Don't add ScrollManag. inst. to this, since thats not needed 2023-01-28 22:46:10 +01:00
4ba00915e0
Update: Don't have the not ready app indicators at the far right
Make sure unaccounted-for indicator containers (like the not ready app
indicator indicator containers) in the right panel box are on the left
of it, since the right panel box is logically right-to-left and to avoid
unnecessary shifting should they become accounted for.
2023-01-28 09:39:04 +01:00
843c5f0e1c
Fix: Support AppIndicator/KStatusNotifierItem item addition again
Do this by not acting upon a new AppIndicator/KStatusNotifierItem item
immediately (as in trying to position it properly and maybe saving it to
settings), but rather act once the app indicators "ready" signal emits
by simply saving new items to settings and ordering the top bar boxes.
2023-01-28 08:38:27 +01:00
644656d93a
Refactor: Merge AppIndi.KStat.ItemManager code into BoxOrderManager
Let `BoxOrderManager` do the relevant work itself and get rid of
`AppIndicatorKStatusNotifierItemManager`.
This is also in preparation to make the addition of
AppIndicator/KStatusNotifierItem items work again.
2023-01-28 08:27:45 +01:00
59d5665661
Refactor: Simplify #overwritePanelAddToPanelBox heavily
Simplify `#overwritePanelAddToPanelBox` heavily by just calling the
original function in the overwrite and ordering the top bar and handling
new items afterwards.
Note that AppIndicator/KStatusNotifierItems are still not supported with
this refactor.
2023-01-28 00:20:43 +01:00
d51703eb4e
Refactor: Introduce BoxOrderManager
Introduce new `BoxOrderManager` to basically act as a heavy wrapper
around the box orders stored in settings, so that the other extension
code has easy methods to get, set and interact with box orders.
Refactor most of the code initially with it in mind.
Note that `#overwritePanelAddToPanelBox` needs more refactoring to
properly make use of that new idea (and it also needs refactoring in
general).
2023-01-27 23:48:23 +01:00
ba373e9e49
Other: Mark AppIndicator/KStatusNotifierItem addition as broken 2023-01-27 21:44:59 +01:00
7b1c030325
Fix: Don't use globalThis, since it won't get g.c. on window close
Don't use `globalThis`, since it won't get garbage collected on window
close.

See here for the relevant review:
https://extensions.gnome.org/review/38364
And for the relevant documentation:
https://gjs.guide/extensions/review-guidelines/review-guidelines.html#only-use-init-for-initialization
2023-01-26 03:42:56 +01:00
2ef16f310c
Fix: Null out this.settings on extension disable
See here for the relevant review:
https://extensions.gnome.org/review/38364
And for the relevant documentation:
https://gjs.guide/extensions/review-guidelines/review-guidelines.html#destroy-all-objects
2023-01-25 20:40:16 +01:00
93ec247cd3
Other: Bump version to 4 2023-01-25 18:56:59 +01:00
97d8f86372
Feature: Support GNOME version 43 2023-01-25 18:55:08 +01:00
c6fb9a8b8d
Docs: Add newer, cut down and commented panel.js from Gnome Shell 43.2 2023-01-24 20:40:24 +01:00
7970a7c643
Other: Add docs panel code files to .eslintignore 2023-01-24 19:39:07 +01:00
dc7ad73db4
Other: Handle ESLint complaints 2023-01-24 19:33:11 +01:00
062bc9ae26
Other: Allow underscore-prefixed unused arguments in ESLint config 2023-01-24 19:26:08 +01:00
2e5bcd4e81
Other: Move "use strict"; before ESLint config comment everywhere 2023-01-24 19:24:14 +01:00
17d7bd23b4
Other: Set ecmaVersion to 2022 for ESLint to sup. priv. class feat. 2023-01-24 19:12:38 +01:00
7c96867182
Other: Upgrade ESLint to new major version and run npm update 2023-01-24 19:00:48 +01:00
268b9b5c5e
Other: Run npm update 2023-01-24 18:42:34 +01:00
d7ec1156e2
Refactor: Setup and handle all of DND scroll. in #setupDNDScroll func.
Also handle drag events in this function so connecting to "drag-end" in
widgets with "GtkDragSource" and a `globalThis` `ScrollManager` instance
aren't needed.
2023-01-24 18:33:15 +01:00
398793d1a0
Refactor: Simplify box order save code in PrefsBOLEmptyPlaceholder
Do this by simply using the `saveBoxOrderToSettings` method of
`PrefsBoxOrderListBox`.
2023-01-22 21:18:58 +01:00
f7309e7f65
Refactor: Simplify PrefsPage UI file and get rid of InternalChildren 2023-01-22 21:12:05 +01:00
503fdcb90d
Refactor: Build Menu in UI file 2023-01-22 21:02:58 +01:00
e07411f9fd
Refactor: Use private instance methods instead of prefixing with _ 2023-01-22 20:35:13 +01:00
cc118d7796
Refactor: Remove empty constructor 2023-01-22 20:27:57 +01:00
596c8d3cdc
Refactor: Setup Drag Source and Drop Targets in UI files 2023-01-22 20:06:02 +01:00
5362629f94
Refactor: Add PrefsBoxOrderListBoxes via UI file a. let them self-init
Add the `PrefsBoxOrderListBox`es to the `PrefsPage` via the
`PrefsPage`es UI file.
Also let the `PrefsBoxOrderListBox`es initialize themselves based on the
given box order.
2023-01-22 19:28:55 +01:00
c54e24c151
Refactor: Add ScrollManager instance to globalThis
Do this so that we don't have to pass the `ScrollManager` instance to
each `PrefsBoxOrderItemRow` instance. This is useful for future
refactors.
2023-01-22 19:28:49 +01:00
34ba8a58cd
Refactor: Add settings to globalThis
Do this so that we don't have to get them in each class individually.
2023-01-22 15:48:35 +01:00
2f9f4d1a3f
Refactor: Use constructor instead of _init 2023-01-22 15:16:56 +01:00
cda099be24
Refactor: Move PrefsPage class into own file (PrefsPage.js) 2023-01-22 15:10:43 +01:00
7ed9329f2d
Refactor: Move UI files and gschema file to more standard locations 2023-01-22 14:59:40 +01:00
ddd0297868
Build: Use gnome-extensions pack for creating extension zip/bundle 2023-01-15 23:35:36 +01:00
d383908d5f
Other: Remove license notice, since its unnecessary and annoying 2023-01-04 23:57:21 +01:00
3299686041
Update: Show drag icon when reordering PrefsBoxOrderItemRows using DND
The following code from GNOME/gnome-control-center was used as a
reference to make this code work:
- ab7d752de9/panels/search/cc-search-panel-row.c (L96)
- ab7d752de9/panels/search/cc-search-panel-row.c (L84)
2022-10-24 01:07:24 +02:00
249f3554d5
Breaking: Make preferences usable again and support Gnome Shell 42
Make the preferences usable again (and look good) by using an
`AdwPreferencesPage` as well as various other libadwaita features (like
`AdwPreferencesGroup`, style classes, ...).
Also get rid of some code, which now doesn't seem to be necessary
anymore (making sure no horizontal scrollbar shows and setting a default
size for the preferences window).

This change is marked as "Breaking", since I assume `AdwPreferencesPage`
doesn't work in Gnome Shell 40 and because I changed the supported Gnome
Shell version to 42 accordingly.
2022-10-23 22:10:36 +02:00
fd66911ff5
Other: Run npm install and npm update 2022-10-22 20:52:18 +02:00
6b4c75ad83
Docs: Add newer, cut down and commented panel.js from Gnome Shell 42.5 2022-10-22 19:14:54 +02:00
91b472c4fe
Docs: Add doc. holding some setup instructions and tips for a GSE Dev VM
Add this document to provide some setup instructions and tips for making
the most of a Gnome Shell Extensions Development VM.
2022-10-22 01:25:11 +02:00
37ece04211
Rename docs/panel.js to include source branch and date
Do this in preparation for the addition of more `panel.js` files from
other branches/tags and dates.
2022-10-21 19:27:59 +02:00
d4af68313f
Other: Bump version to 3 2021-07-05 11:03:15 +02:00
10b491c252
Update: Handle the Dropbox client better
The Dropbox client appends its PID to the id, which gets used for the
application name in `handleAppIndicatorKStatusNotifierItemItem`. This
resulted in the creation of a new entry for the Dropbox client on
basically every Dropbox client start, which would fill up the extensions
settings and would also result in the user effectively not being able to
set the position for the Dropbox client permanently.
This commit fixes these issues by removing the PID (and the hypen before
it) for the Dropbox client application name.
2021-07-05 10:58:10 +02:00
03fb3355b4
Fix: Extension crashing, when no associatedRole can be found
There are situations, where no `associatedRole` for a given
`indicatorContainer` can be found (e.g. the GameMode GNOME Shell
Extension seems to cause such a situation, when you have it enabled and
lock and unlock GNOME).
Previously this would crash the extension. With this fix, those
`indicatorContainer`s just get ignored.
2021-07-05 10:06:51 +02:00
e76b24e813
Feature: Add forget action for top bar items
This forget action removes the top bar item from the top bar order
representation, which is present in the settings, and then saves the top
bar order.
This action is intended for forgetting top bar items, which aren't
present in the top bar anymore, so that the user can clean up their
settings.
So if an item isn't in the top bar at the time of action activation,
activating the action removes it from the saved top bar order. If an
item is in the top bar at the time of action activation, the item
position just gets lost (since it gets automatically re-added to the top
bar order).
2021-07-05 08:41:08 +02:00
309e2c07b9
Refactor: Let PrefsBoxOrderItemRow handle item association
Let `PrefsBoxOrderItemRow` handle the association of itself with an item
itself.
Doing so makes the code cleaner, since the association isn't done
externally anymore.
2021-07-05 07:58:01 +02:00
bd69f816eb
Refactor: Move logic for saving box order to PrefsBoxOrderListBox
Move the logic for saving the box order represented by a
`PrefsBoxOrderListBox` (and its `PrefsBoxOrderItemRows`) to the
`PrefsBoxOrderListBox` class.
This makes the code cleaner and allows for easy reuse of the logic in
the future.
2021-07-05 07:46:08 +02:00
28fb67ad4d
Refactor: Create custom class for prefs box order GtkListBoxes
Create a custom class (`PrefsBoxOrderListBox`) for GtkListBoxes, which
are used in the preferences window for holding box order settings.
Having a custom class for these GtkListBoxes is useful, since it can
hold logic related to box order settings. Future commits will bring more
of this logic into this class.
2021-07-05 07:22:19 +02:00
edfa50f254
Update: Scroll preferences window content on Drag-and-Drop
Scroll the content of the preferences window on Drag-and-Drop, when the
mouse is in the upper or lower part of the content of the preferences
window.
This helps a user, who has e.g. a lot of items, but the preferences
window with a low height, to easily move any item to any position.
2021-07-04 05:16:32 +02:00
a4ea0c630a
Update: Make the preferences window content scrollable
Previously it could happen that the preferences window got larger than
the height of the screen (e.g. when having a bunch of items and a low
vertical screen resolution).
Fix this, by making the preferences window content scrollable.

Also introduce a nice default window size.
2021-07-04 02:20:56 +02:00
52 changed files with 5829 additions and 1871 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

@ -1 +1,7 @@
/docs/panel.js
/docs/panel_master_2021-04-21.js
/docs/panel_42.5_2022-10-22.js
/docs/panel_43.2_2023-01-24.js
/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

View File

@ -2,7 +2,8 @@ env:
es2021: true
extends: 'eslint:recommended'
parserOptions:
ecmaVersion: 12
ecmaVersion: 2022
sourceType: module
rules:
indent:
- error
@ -17,5 +18,43 @@ rules:
semi:
- error
- always
no-unused-vars:
- error
- argsIgnorePattern: "^_"
curly:
- error
- all
brace-style:
- error
- 1tbs
# Rules from GJS Style Guide regarding whitespace.
# See here: https://gjs.guide/guides/gjs/style-guide.html#whitespace
func-call-spacing:
- error
- never
array-bracket-spacing:
- error
- never
space-before-function-paren:
- error
- never
space-before-blocks:
- error
- always
key-spacing:
- error
- beforeColon: false
afterColon: true
mode: strict
object-curly-spacing:
- error
- always
comma-dangle:
- error
- arrays: always-multiline
objects: always-multiline
imports: always-multiline
exports: always-multiline
functions: never
globals:
imports: readonly
log: readonly

4
.gitignore vendored
View File

@ -1,3 +1,3 @@
/node_modules/
gschemas.compiled
top-bar-organizer@julian.gse.jsts.xyz.zip
/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 (```).
-->

View File

@ -9,22 +9,22 @@ For code of this repo in `.js` files, stick to a textwidth of 80.
The commit message format is as follows:
```
Tag: Short description
tag: short description
Longer description here if necessary
```
The `Tag` should be one of the following:
The `tag` should be one of the following:
- `Fix` - for a bug fix.
- `Update` - for a backwards compatible enhancement.
- `Feature` (formerly also `New`) - for a new feature.
- `Breaking` - for a backwards-incompatible enhancement or feature.
- `Perf` - for a code change that improves performance.
- `Refactor` - for a code change that neither fixes a bug nor adds a feature (nor is `Perf`).
- `Build` - for changes that affect the build system or external dependencies.
- `Test` - for adding or correcting tests.
- `Docs` - for changes to documentation only.
- `Other` - for anything that isn't covered by the tags above.
- `fix` - for a bug fix.
- `update` - for a backwards compatible enhancement.
- `feature` (formerly also `New`) - for a new feature.
- `breaking` - for a backwards-incompatible enhancement or feature.
- `perf` - for a code change that improves performance.
- `refactor` - for a code change that neither fixes a bug nor adds a feature (nor is `perf`).
- `build` - for changes that affect the build system or external dependencies.
- `test` - for adding or correcting tests.
- `docs` - for changes to documentation only.
- `other` - for anything that isn't covered by the tags above.
Those tags are an adapted version of the tags eslint (<https://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes>) and of the tags Angular (<https://github.com/angular/angular/blob/master/CONTRIBUTING.md#type>) uses.

12
README.md Normal file
View File

@ -0,0 +1,12 @@
# Top Bar Organizer
![Screenshot of GNOME Shell 43 with Top Bar Organizer v6 running and its preferences open. The GNOME Shell top bar items aren't all in their default location.](./res/Screenshot%20of%20GNOME%20Shell%2043%20with%20Top%20Bar%20Organizer%20v6%20and%20its%20preferences%20in%20light%20theme%202023-01-30.jpg)
Top Bar Organizer allows you to organize the items of the GNOME Shell top (menu)bar.
## Installation
The extension is available on the [GNOME Extensions website](https://extensions.gnome.org/extension/4356/top-bar-organizer/).
Or you can also manually install a release from the [releases page](https://gitlab.gnome.org/june/top-bar-organizer/-/releases).
There's also a community-maintained [AUR package](https://aur.archlinux.org/packages/gnome-shell-extension-top-bar-organizer) available.

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"

9
data/css/prefs.css Normal file
View File

@ -0,0 +1,9 @@
/* Taken from: https://gitlab.gnome.org/GNOME/gnome-control-center/-/blob/43.5/shell/style.css */
.drag-handle {
color: alpha(@theme_fg_color, 0.4);
}
.drag-handle:backdrop {
color: alpha(@theme_unfocused_fg_color, 0.4);
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
<schema id="org.gnome.shell.extensions.top-bar-organizer" path="/org/gnome/shell/extensions/top-bar-organizer/">
<key name="left-box-order" type="as">
<description>Order of items in the left box of the top bar.</description>
<default>[]</default>
</key>
<key name="center-box-order" type="as">
<description>Order of items in the center box of the top bar.</description>
<default>[]</default>
</key>
<key name="right-box-order" type="as">
<description>Order of items in the right box of the top bar.</description>
<default>[]</default>
</key>
<key name="hide" type="as">
<description>Top bar items to (forcefully) hide.</description>
<default>[]</default>
</key>
<key name="show" type="as">
<description>Top bar items to (forcefully) show.</description>
<default>[]</default>
</key>
</schema>
</schemalist>

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

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="PrefsBoxOrderItemRow" parent="AdwActionRow">
<child type="prefix">
<object class="GtkImage">
<property name="icon-name">list-drag-handle-symbolic</property>
<style>
<class name="drag-handle"/>
</style>
</object>
</child>
<child type="suffix">
<object class="GtkMenuButton">
<property name="valign">center</property>
<property name="icon-name">view-more-symbolic</property>
<property name="tooltip-text">Options</property>
<property name="menu-model">optionsMenuModel</property>
<style>
<class name="flat"/>
</style>
</object>
</child>
<child>
<object class="GtkDragSource">
<property name="actions">move</property>
<signal name="prepare" handler="onDragPrepare"/>
<signal name="drag-begin" handler="onDragBegin"/>
</object>
</child>
<child>
<object class="GtkDropTarget">
<property name="actions">move</property>
<property name="formats">PrefsBoxOrderItemRow</property>
<signal name="drop" handler="onDrop"/>
</object>
</child>
</template>
<menu id="optionsMenuModel">
<section>
<item>
<attribute name="label">Move Up</attribute>
<attribute name="action">row.move-up</attribute>
</item>
<item>
<attribute name="label">Move Down</attribute>
<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>
<attribute name="action">row.forget</attribute>
</item>
</section>
</menu>
</interface>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="PrefsBoxOrderListBox" parent="GtkListBox">
<property name="selection-mode">none</property>
<property name="show-separators">True</property>
<style>
<class name="boxed-list"/>
</style>
</template>
</interface>

View File

@ -1,22 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This file is part of Top-Bar-Organizer (a Gnome Shell Extension for
organizing your Gnome Shell top bar).
Copyright (C) 2021 Julian Schacher
Top-Bar-Organizer is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<interface>
<template class="PrefsBoxOrderListEmptyPlaceholder" parent="GtkBox">
<child>
@ -37,5 +19,12 @@
</child>
</object>
</child>
<child>
<object class="GtkDropTarget">
<property name="actions">move</property>
<property name="formats">PrefsBoxOrderItemRow</property>
<signal name="drop" handler="onDrop"/>
</object>
</child>
</template>
</interface>

52
data/ui/prefs-page.ui Normal file
View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="PrefsPage" parent="AdwPreferencesPage">
<child>
<object class="AdwPreferencesGroup">
<property name="title">Item Order</property>
<property name="description">Simply use drag and drop to order the items any way you want.</property>
<child>
<object class="GtkLabel">
<property name="label">Left Top Bar Box</property>
<property name="halign">start</property>
<property name="margin-bottom">12</property>
</object>
</child>
<child>
<object class="PrefsBoxOrderListBox" id="left-box-order-list-box">
<property name="box-order">left-box-order</property>
<signal name="row-move" handler="onRowMove"/>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label">Center Top Bar Box</property>
<property name="halign">start</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
</object>
</child>
<child>
<object class="PrefsBoxOrderListBox" id="center-box-order-list-box">
<property name="box-order">center-box-order</property>
<signal name="row-move" handler="onRowMove"/>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label">Right Top Bar Box</property>
<property name="halign">start</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
</object>
</child>
<child>
<object class="PrefsBoxOrderListBox" id="right-box-order-list-box">
<property name="box-order">right-box-order</property>
<signal name="row-move" handler="onRowMove"/>
</object>
</child>
</object>
</child>
</template>
</interface>

View File

@ -0,0 +1,51 @@
# Creating a Release
## Create a Tag
To create a new tag, do the following:
1. Fill out `git_annotated_tag_template`.
2. Run the following command to tag the current commit with `vX`:
```
git tag -a -F git_annotated_tag_template -s --cleanup=verbatim vX
```
3. Restore `git_annotated_tag_template` to its original state:
```
git restore git_annotated_tag_template
```
4. Push the tag:
```
git push --tags
```
## Build a Release-ZIP
1. Build the release-ZIP:
```
./package.sh
```
2. Name the release-ZIP after the current version:
```
mv top-bar-organizer@julian.gse.jsts.xyz.shell-extension.zip top-bar-organizer@julian.gse.jsts.xyz.shell-extension_vX.zip
```
## Create a GitLab Release
1. Go to the [Releases section of the repo](https://gitlab.gnome.org/june/top-bar-organizer/-/releases) and click on the "New Release" button.
2. Select the corresponding tag created earlier.
3. Name the release "Top Bar Organizer vX".
4. Copy the [release notes template](./release_notes_template.md) and fill it out.
5. Drop the release-ZIP created in the previous step at the end of the release notes.
6. Create the release.
## Uploading to the GNOME Extensions Website
1. Go to the [upload page of the GNOME extensions website](https://extensions.gnome.org/upload/) and upload the release-ZIP.

View File

@ -1,22 +0,0 @@
# Creating a New Tag
To create a new tag, do the following:
1. Fill out `git_annotated_tag_template`.
2. Run the following command:
```
git tag -a -F git_annotated_tag_template -s --cleanup=verbatim <tagname> [<commit>]
```
3. Restore `git_annotated_tag_template` to its original state:
```
git restore git_annotated_tag_template
```
4. Push the new tag.
```
git push --tags
```

View File

@ -0,0 +1,91 @@
# Gnome Shell Extensions Development VM
This document holds some setup instructions and tips for getting the most of your Gnome Shell Extensions Development VM.
Note on commands in this document:
- `$` indicates that a command should be run as your normal user.
- `#` indicates that a command should be run as root.
## GTKInspector
Enable GTKInspector by running the following command:
```
$ gsettings set org.gtk.Settings.Debug enable-inspector-keybinding true
```
Now you can inspect GTK Apps by pressing `Ctrl + Shift + D`.
### Links and Sources
- <https://wiki.gnome.org/Projects/GTK/Inspector>
## Looking Glass
Looking Glass is Gnome Shells integrated debugger and inspector tool.
You can use it by pressing `Alt + F2`, typing `lg` and pressing Enter.
If you want to exit Looking Glass, press `Esc` in the Evaluator pane.
### Links and Sources
- <https://wiki.gnome.org/Projects/GnomeShell/LookingGlass>
## Sharing a Directory Between the Host and the Guest
To share a directory between the host and the Gnome Shell Extensions Development VM, do the following.
Note that this guide assumes you're using Virtual Machine Manager (virt-manager) and at least v4.0.0 of it.
1. Shut down the VM.
2. Go to your VMs hardware details and then to `Memory`.
Check the `Enable shared memory` checkbox there.
3. Go to your VMs hardware details and then to `Add Hardware -> Filesystem`.
Then select `virtiofs` for the driver and an appropriate source path (like `/home/user/gse_dev_vm_shared_folder`) and target path (like `shared_folder`).
Finally click on `Finish`.
4. Power on the VM.
5. Create a mountpoint by running:
```
# mkdir /mnt/shared_host_folder
```
6. Edit `/etc/fstab` and add the following line at the end:
```
TARGET_PATH_YOU_SET_IN_VIRT_MANAGER /mnt/shared_host_folder virtiofs rw,noatime,_netdev 0 0
```
7. Reboot the VM.
Now you have a shared folder between your host and your VM, which you can access on your host at the specified source path and in the VM at `/mnt/shared_host_folder`.
### Links and Sources
- <https://wiki.archlinux.org/title/Libvirt#Sharing_data_between_host_and_guest>
- <https://libvirt.org/kbase/virtiofs.html>
- <https://github.com/virt-manager/virt-manager/releases/tag/v4.0.0>
## Enabling Automatic Login
Enabling Automatic Login in the Gnome Settings under `Users -> Unlock... -> Automatic Login` saves you from inserting the VM users password after VM startups.
## Disabling Automatic Screen Lock
Disabling Automatic Screen Lock in the Gnome Settings under `Privacy -> Screen Lock -> Automatic Screen Lock` saves you from Gnome locking the VM and you having to insert the VM users password.
## Running Applications Providing Tray Icons Automatically on Startup
Especially for the development of this extension it is useful to have some applications, which provide tray icons (e.g. Element, Telegram) run automatically on startup.
To make this happen (nicely), you need to do the following:
1. Make sure the applications get started automatically on log in by using Gnome Tweaks, going to `Startup Applications` and adding them there.
2. If you're using Automatic Login, use the Password and Keys application (seahorse) to set the Login keyrings password to a blank one.
Do this to avoid the Login keyring password prompt after log in, which is triggered by some applications like Element.
Note that this leaves the keyring unencrypted, but since you're in a Dev VM, no important things should be stored in there anyway (hopefully).
3. Use the "Auto Move Windows" extension to move the windows of your applications to a different workspace, so your main one doesn't get cluttered.
4. To see tray icons, install the "AppIndicator and KStatusNotifierItem Support" extension.
### Links and Sources
- <https://wiki.archlinux.org/title/GNOME/Keyring>

View File

@ -0,0 +1,318 @@
// My annotated and cut down `js/ui/panel.js` from gnome-shell/42.5.
// 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/42.5/js/ui/panel.js
// On: 2022-10-22
// License: This code is licensed under GPLv2.
// Parts taken from: https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/42.5/js/ui/sessionMode.js
// On: 2022-10-22
// 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.
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported Panel */
const { Atk, Clutter, GLib, GObject, Meta, Shell, St } = imports.gi;
// Annotation: [...] Cut out bunch of stuff here, which isn't relevant for this
// Extension.
const CtrlAltTab = imports.ui.ctrlAltTab;
// Annotation: [...] Cut out bunch of stuff here, which isn't relevant for this
// Extension.
const PopupMenu = imports.ui.popupMenu;
const PanelMenu = imports.ui.panelMenu;
const Main = imports.ui.main;
// Annotation: [...] Cut out bunch of stuff here, which isn't relevant for this
// Extension.
// Compared to panel_master_2021-04-21.js:
// The "screenRecording" entry got added.
const PANEL_ITEM_IMPLEMENTATIONS = {
'activities': ActivitiesButton,
'aggregateMenu': AggregateMenu,
'appMenu': AppMenuButton,
'dateMenu': imports.ui.dateMenu.DateMenuButton,
'a11y': imports.ui.status.accessibility.ATIndicator,
'keyboard': imports.ui.status.keyboard.InputSourceIndicator,
'dwellClick': imports.ui.status.dwellClick.DwellClickIndicator,
'screenRecording': imports.ui.status.remoteAccess.ScreenRecordingIndicator,
};
var 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_master_2021-04-21.js:
// Didn't really change, except for two events getting connected, which we
// probably don't care about, the corners being gone and some reformatting.
_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.connect('showing', () => {
this.add_style_pseudo_class('overview');
});
Main.overview.connect('hiding', () => {
this.remove_style_pseudo_class('overview');
});
Main.layoutManager.panelBox.add(this);
Main.ctrlAltTabManager.addGroup(this, _("Top Bar"), 'focus-top-bar-symbolic',
{ sortGroup: CtrlAltTab.SortGroup.TOP });
Main.sessionMode.connect('updated', this._updatePanel.bind(this));
global.display.connect('workareas-changed', () => this.queue_relayout());
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 pnale of Mains
// (`js/ui/main.js`) instance of SessionMode (`js/ui/sessionMode.js`).
//
// And in `js/ui/sessionMode.js` (42.5, 2022-10-22) you have different
// modes with different panel configuration. For example the "user" mode
// with:
// ```
// panel: {
// left: ['activities', 'appMenu'],
// center: ['dateMenu'],
// right: ['screenRecording', 'dwellClick', 'a11y', 'keyboard', 'aggregateMenu'],
// },
// ```
//
// This way this function populates the top (menu)bar / panel with the
// default stuff you see on a fresh Gnome.
//
// Compared to panel_master_2021-04-21.js:
// Corner-related code is gone and some function name casing got 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_master_2021-04-21.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_master_2021-04-21.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_master_2021-04-21.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_master_2021-04-21.js: Nothing changed.
_addToPanelBox(role, indicator, position, box) {
let container = indicator.container;
container.show();
let parent = container.get_parent();
if (parent)
parent.remove_actor(container);
box.insert_child_at_index(container, position);
if (indicator.menu)
this.menuManager.addMenu(indicator.menu);
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_master_2021-04-21.js:
// Some syntax changes (usage of syntactic sugar).
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;
}
// Annotation:
// Compared to panel_master_2021-04-21.js:
// `_addStyleClassName(className)` and `_removeStyleClassName(className)`
// are gone, since they apparently only existed to handle stuff related to
// the corners, which are gone.
_onMenuSet(indicator) {
if (!indicator.menu || indicator.menu._openChangedId)
return;
indicator.menu._openChangedId = indicator.menu.connect('open-state-changed',
(menu, isOpen) => {
let boxAlignment;
if (this._leftBox.contains(indicator.container))
boxAlignment = Clutter.ActorAlign.START;
else if (this._centerBox.contains(indicator.container))
boxAlignment = Clutter.ActorAlign.CENTER;
else if (this._rightBox.contains(indicator.container))
boxAlignment = Clutter.ActorAlign.END;
if (boxAlignment == Main.messageTray.bannerAlignment)
Main.messageTray.bannerBlocked = isOpen;
});
}
// Annotation: [...] Cut out bunch of stuff here, which isn't relevant for
// this Extension.
});

View File

@ -0,0 +1,317 @@
// My annotated and cut down `js/ui/panel.js` from gnome-shell/43.2.
// 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/43.2/js/ui/panel.js
// On: 2023-01-24
// License: This code is licensed under GPLv2.
// Taken from: https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/43.2/js/ui/sessionMode.js
// On: 2023-01-24
// 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.
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported Panel */
const { Atk, Clutter, GLib, GObject, Meta, Shell, St } = imports.gi;
// Annotation: [...] Cut out bunch of stuff here, which isn't relevant for this
// Extension.
const CtrlAltTab = imports.ui.ctrlAltTab;
// Annotation: [...] Cut out bunch of stuff here, which isn't relevant for this
// Extension.
const PopupMenu = imports.ui.popupMenu;
const PanelMenu = imports.ui.panelMenu;
// Annotation: [...] Cut out bunch of stuff here, which isn't relevant for this
// Extension.
const Main = imports.ui.main;
// Annotation: [...] Cut out bunch of stuff here, which isn't relevant for this
// Extension.
// Compared to panel_42.5_2022-10-22.js:
// "aggregateMenu" is gone with "quickSettings" replacing it.
// "screenSharing" got added.
const PANEL_ITEM_IMPLEMENTATIONS = {
'activities': ActivitiesButton,
'appMenu': AppMenuButton,
'quickSettings': QuickSettings,
'dateMenu': imports.ui.dateMenu.DateMenuButton,
'a11y': imports.ui.status.accessibility.ATIndicator,
'keyboard': imports.ui.status.keyboard.InputSourceIndicator,
'dwellClick': imports.ui.status.dwellClick.DwellClickIndicator,
'screenRecording': imports.ui.status.remoteAccess.ScreenRecordingIndicator,
'screenSharing': imports.ui.status.remoteAccess.ScreenSharingIndicator,
};
var 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_42.5_2022-10-22.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.connect('showing', () => {
this.add_style_pseudo_class('overview');
});
Main.overview.connect('hiding', () => {
this.remove_style_pseudo_class('overview');
});
Main.layoutManager.panelBox.add(this);
Main.ctrlAltTabManager.addGroup(this, _("Top Bar"), 'focus-top-bar-symbolic',
{ sortGroup: CtrlAltTab.SortGroup.TOP });
Main.sessionMode.connect('updated', this._updatePanel.bind(this));
global.display.connect('workareas-changed', () => this.queue_relayout());
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` (43.2, 2023-01-24) you have different
// modes with different panel configuration. For example the "user" mode
// with:
// ```
// panel: {
// left: ['activities', 'appMenu'],
// 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_42.5_2022-10-22.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_42.5_2022-10-22.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_42.5_2022-10-22.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_42.5_2022-10-22.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_42.5_2022-10-22.js:
// Code for adding indicator menu to a menu manager got moved to
// `_onMenuSet` method without the if clause.
_addToPanelBox(role, indicator, position, box) {
let container = indicator.container;
container.show();
let parent = container.get_parent();
if (parent)
parent.remove_actor(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_42.5_2022-10-22.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;
}
// Compared to panel_42.5_2022-10-22.js:
// Code for adding indicator menu to a menu manager got moved to here from
// `_addToPanelBox` method without the if clause.
_onMenuSet(indicator) {
if (!indicator.menu || indicator.menu._openChangedId)
return;
this.menuManager.addMenu(indicator.menu);
indicator.menu._openChangedId = indicator.menu.connect('open-state-changed',
(menu, isOpen) => {
let boxAlignment;
if (this._leftBox.contains(indicator.container))
boxAlignment = Clutter.ActorAlign.START;
else if (this._centerBox.contains(indicator.container))
boxAlignment = Clutter.ActorAlign.CENTER;
else if (this._rightBox.contains(indicator.container))
boxAlignment = Clutter.ActorAlign.END;
if (boxAlignment == Main.messageTray.bannerAlignment)
Main.messageTray.bannerBlocked = isOpen;
});
}
// Annotation: [...] Cut out bunch of stuff here, which isn't relevant for
// this Extension.
});

View File

@ -0,0 +1,344 @@
// My annotated and cut down `js/ui/panel.js` from gnome-shell/45.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/45.0/js/ui/panel.js
// On: 2023-09-26
// License: This code is licensed under GPLv2.
// Taken from: https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/45.0/js/ui/sessionMode.js
// On: 2023-09-26
// 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.
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
// 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 AppMenuButton = [...]
// const ActivitiesButton = [...]
// const QuickSettings = [...]
const PANEL_ITEM_IMPLEMENTATIONS = {
'activities': ActivitiesButton,
'appMenu': AppMenuButton,
'quickSettings': QuickSettings,
'dateMenu': DateMenuButton,
'a11y': ATIndicator,
'keyboard': InputSourceIndicator,
'dwellClick': DwellClickIndicator,
'screenRecording': ScreenRecordingIndicator,
'screenSharing': ScreenSharingIndicator,
};
// Annotation: Uses ESM export now.
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_43.2_2023-01-24.js: Nothing changed (except for
// formatting).
_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.connect('showing', () => {
this.add_style_pseudo_class('overview');
});
Main.overview.connect('hiding', () => {
this.remove_style_pseudo_class('overview');
});
Main.layoutManager.panelBox.add(this);
Main.ctrlAltTabManager.addGroup(this,
_('Top Bar'), 'focus-top-bar-symbolic',
{sortGroup: CtrlAltTab.SortGroup.TOP});
Main.sessionMode.connect('updated', this._updatePanel.bind(this));
global.display.connect('workareas-changed', () => this.queue_relayout());
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` (45.0, 2023-09-26) 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_43.2_2023-01-24.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_43.2_2023-01-24.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_43.2_2023-01-24.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_43.2_2023-01-24.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_43.2_2023-01-24.js: Nothing changed.
_addToPanelBox(role, indicator, position, box) {
let container = indicator.container;
container.show();
let parent = container.get_parent();
if (parent)
parent.remove_actor(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_43.2_2023-01-24.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;
}
// Compared to panel_43.2_2023-01-24.js: Nothing changed, except for the
// usage of === instead of ==.
_onMenuSet(indicator) {
if (!indicator.menu || indicator.menu._openChangedId)
return;
this.menuManager.addMenu(indicator.menu);
indicator.menu._openChangedId = indicator.menu.connect('open-state-changed',
(menu, isOpen) => {
let boxAlignment;
if (this._leftBox.contains(indicator.container))
boxAlignment = Clutter.ActorAlign.START;
else if (this._centerBox.contains(indicator.container))
boxAlignment = Clutter.ActorAlign.CENTER;
else if (this._rightBox.contains(indicator.container))
boxAlignment = Clutter.ActorAlign.END;
if (boxAlignment === Main.messageTray.bannerAlignment)
Main.messageTray.bannerBlocked = isOpen;
});
}
// Annotation: [...] Cut out bunch of stuff here, which isn't relevant for
// this Extension.
});

View File

@ -0,0 +1,322 @@
// My annotated and cut down js/ui/panel.js from gnome-shell/46.4.
// 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/46.4/js/ui/panel.js
// On: 2024-09-11
// License: This code is licensed under GPLv2.
// Taken from: https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/46.4/js/ui/sessionMode.js
// On: 2023-09-11
// 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.
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
// 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 AppMenuButton = [...]
// const ActivitiesButton = [...]
// const QuickSettings = [...]
const PANEL_ITEM_IMPLEMENTATIONS = {
'activities': ActivitiesButton,
'appMenu': AppMenuButton,
'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_45.0_2023-09-26.js: Not much changed except from some
// slightly different function names/calls.
_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.connect('updated', this._updatePanel.bind(this));
global.display.connect('workareas-changed', () => this.queue_relayout());
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 (46.4, 2024-09-11) 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_45.0_2023-09-26.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_45.0_2023-09-26.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_45.0_2023-09-26.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_45.0_2023-09-26.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_45.0_2023-09-26.js: The call of
// parent.remove_actor(container) changed to parent.remove_child(container).
_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_45.0_2023-09-26.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.
});

View File

@ -0,0 +1,317 @@
// My annotated and cut down js/ui/panel.js from gnome-shell/47.rc.
// 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/47.rc/js/ui/panel.js
// On: 2024-09-12
// License: This code is licensed under GPLv2.
// Taken from: https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/47.rc/js/ui/sessionMode.js
// On: 2023-09-12
// 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 AppMenuButton = [...]
// const ActivitiesButton = [...]
// const QuickSettings = [...]
const PANEL_ITEM_IMPLEMENTATIONS = {
'activities': ActivitiesButton,
'appMenu': AppMenuButton,
'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_46.4_2024-09-11.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.connect('updated', this._updatePanel.bind(this));
global.display.connect('workareas-changed', () => this.queue_relayout());
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 (47.rc, 2024-09-12) 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_46.4_2024-09-11.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_46.4_2024-09-11.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_46.4_2024-09-11.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_46.4_2024-09-11.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_46.4_2024-09-11.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_46.4_2024-09-11.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.
});

View File

@ -0,0 +1,322 @@
// My annotated and cut down js/ui/panel.js from gnome-shell/48.2.
// 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/48.2/js/ui/panel.js
// On: 2025-06-08
// License: This code is licensed under GPLv2.
// Taken from: https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/48.2/js/ui/sessionMode.js
// On: 2025-06-08
// 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 AppMenuButton = [...]
// const ActivitiesButton = [...]
// const QuickSettings = [...]
const PANEL_ITEM_IMPLEMENTATIONS = {
'activities': ActivitiesButton,
'appMenu': AppMenuButton,
'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_47.rc_2024-09-12.js: connectObject instead of connect
// gets used, which shouldn't be relevant for this extension.
_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 (48.2, 2025-06-08) 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_47.rc_2024-09-12.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_47.rc_2024-09-12.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_47.rc_2024-09-12.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_47.rc_2024-09-12.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_47.rc_2024-09-12.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_47.rc_2024-09-12.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.
});

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.
});

View File

@ -0,0 +1,26 @@
Top Bar Organizer vX includes the following changes:
# Relevant and/or Breaking Changes
The following relevant and/or breaking changes of this version:
## Breaking Change
Description
## Relevant Change
Description
# Other Changes
- a change
- another change
# `git shortlog`
The git shortlog for this version:
```
git shortlog vX-1..vX
```

View File

@ -1,13 +1,9 @@
Top Bar Organizer v1 includes the following changes:
Top Bar Organizer vX
# Relevant and/or Breaking Changes
Relevant and/or Breaking Changes:
- breaking change: description
- relevant change: description
The following relevant and/or breaking changes of this version:
<list relevant and/or breaking changes of this version here>
# `git shortlog`
The git shortlog for this version:
<paste the git shortlog for this version here>
Other Changes:
- a change
- another change

2320
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,16 +2,23 @@
"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"
},
"repository": {
"type": "git",
"url": "git@gitlab.gnome.org:julianschacher/top-bar-organizer.git"
"url": "git@gitlab.gnome.org:june/top-bar-organizer.git"
},
"author": "Julian Schacher",
"author": "June",
"license": "GPL-3.0-or-later",
"devDependencies": {
"eslint": "^7.26.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,8 +4,14 @@ set -e
REAL_BASE_DIR=$( dirname $( readlink -f "$0" ))
glib-compile-schemas "$REAL_BASE_DIR/src/schemas"
rm "$REAL_BASE_DIR/top-bar-organizer@julian.gse.jsts.xyz.zip" || true
cd "$REAL_BASE_DIR/src"
zip -r "$REAL_BASE_DIR/top-bar-organizer@julian.gse.jsts.xyz.zip" *
zip -d "$REAL_BASE_DIR/top-bar-organizer@julian.gse.jsts.xyz.zip" "schemas/org.gnome.shell.extensions.top-bar-organizer.gschema.xml"
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 \
--extra-source ../data/ui \
--extra-source ../data/css \
--schema ../data/org.gnome.shell.extensions.top-bar-organizer.gschema.xml

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

View File

@ -1,426 +0,0 @@
/*
* This file is part of Top-Bar-Organizer (a Gnome Shell Extension for
* organizing your Gnome Shell top bar).
* Copyright (C) 2021 Julian Schacher
*
* Top-Bar-Organizer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* exported init */
"use strict";
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Main = imports.ui.main;
const Panel = imports.ui.panel;
const AppIndicatorKStatusNotifierItemManager = Me.imports.extensionModules.AppIndicatorKStatusNotifierItemManager;
const BoxOrderCreator = Me.imports.extensionModules.BoxOrderCreator;
class Extension {
constructor() {
}
enable() {
this.settings = ExtensionUtils.getSettings();
// Create an instance of AppIndicatorKStatusNotifierItemManager to
// handle AppIndicator/KStatusNotifierItem items.
this._appIndicatorKStatusNotifierItemManager = new AppIndicatorKStatusNotifierItemManager.AppIndicatorKStatusNotifierItemManager();
// Create an instance of BoxOrderCreator for the creation of special box
// orders.
this._boxOrderCreator = new BoxOrderCreator.BoxOrderCreator(this._appIndicatorKStatusNotifierItemManager);
this._addNewItemsToBoxOrders();
this._orderTopBarItemsOfAllBoxes();
this._overwritePanelAddToPanelBox();
// Handle changes of configured box orders.
this._settingsHandlerIds = [ ];
const addConfiguredBoxOrderChangeHandler = (box) => {
let handlerId = this.settings.connect(`changed::${box}-box-order`, () => {
this._orderTopBarItems(box);
/// For the case, where the currently saved box order is based
/// on a permutation of an outdated box order, get an updated
/// box order and save it, if needed.
let updatedBoxOrder;
switch (box) {
case "left":
updatedBoxOrder = this._createUpdatedBoxOrders().left;
break;
case "center":
updatedBoxOrder = this._createUpdatedBoxOrders().center;
break;
case "right":
updatedBoxOrder = this._createUpdatedBoxOrders().right;
break;
}
// Only save the updated box order to settings, if it is
// different, to avoid looping.
const currentBoxOrder = this.settings.get_strv(`${box}-box-order`);
if (JSON.stringify(currentBoxOrder) !== JSON.stringify(updatedBoxOrder)) {
this.settings.set_strv(`${box}-box-order`, updatedBoxOrder);
}
});
this._settingsHandlerIds.push(handlerId);
};
addConfiguredBoxOrderChangeHandler("left");
addConfiguredBoxOrderChangeHandler("center");
addConfiguredBoxOrderChangeHandler("right");
}
disable() {
// Revert the overwrite of `Panel._addToPanelBox`.
Panel.Panel.prototype._addToPanelBox = Panel.Panel.prototype._originalAddToPanelBox;
// Set `Panel._originalAddToPanelBox` to `undefined`.
Panel._originalAddToPanelBox = undefined;
// Disconnect signals.
for (const handlerId of this._settingsHandlerIds) {
this.settings.disconnect(handlerId);
}
}
////////////////////////////////////////////////////////////////////////////
/// Methods used on extension enable. ///
////////////////////////////////////////////////////////////////////////////
/**
* This method adds all new items currently present in the Gnome Shell top
* bar to the box orders.
*/
_addNewItemsToBoxOrders() {
const boxOrders = this._createUpdatedBoxOrders();
this.settings.set_strv("left-box-order", boxOrders.left);
this.settings.set_strv("center-box-order", boxOrders.center);
this.settings.set_strv("right-box-order", boxOrders.right);
}
/**
* This methods orders the top bar items of all boxes according to the
* configured box orders using `this._orderTopBarItems`.
*/
_orderTopBarItemsOfAllBoxes() {
this._orderTopBarItems("left");
this._orderTopBarItems("center");
this._orderTopBarItems("right");
}
/**
* An object containing a position and box overwrite.
* @typedef PositionAndBoxOverwrite
* @property {Number} position - The position overwrite.
* @property {string} box - The position box overwrite.
*/
/**
* Overwrite `Panel._addToPanelBox` with a custom method, which handles top
* bar item additions to make sure that they are added in the correct
* position and box.
*/
_overwritePanelAddToPanelBox() {
// Add the original `Panel._addToPanelBox` method as
// `Panel._originalAddToPanelBox`.
Panel.Panel.prototype._originalAddToPanelBox = Panel.Panel.prototype._addToPanelBox;
// This function gets used by the `Panel._addToPanelBox` overwrite to
// determine the position and box for a new item.
// It also adds the new item to the relevant box order, if it isn't in
// it already.
const getPositionAndBoxOverwrite = (role, box, indicator) => {
const boxOrders = {
left: this.settings.get_strv("left-box-order"),
center: this.settings.get_strv("center-box-order"),
right: this.settings.get_strv("right-box-order"),
};
let boxOrder;
// Handle the case where the new item is a
// AppIndicator/KStatusNotifierItem.
if (role.startsWith("appindicator-")) {
switch (box) {
case "left":
boxOrder = this.settings.get_strv("left-box-order");
this._appIndicatorKStatusNotifierItemManager.handleAppIndicatorKStatusNotifierItemItem(indicator.container, role, boxOrder, boxOrders);
this.settings.set_strv("left-box-order", boxOrder);
break;
case "center":
boxOrder = this.settings.get_strv("center-box-order");
this._appIndicatorKStatusNotifierItemManager.handleAppIndicatorKStatusNotifierItemItem(indicator.container, role, boxOrder, boxOrders);
this.settings.set_strv("center-box-order", boxOrder);
break;
case "right":
boxOrder = this.settings.get_strv("right-box-order");
this._appIndicatorKStatusNotifierItemManager.handleAppIndicatorKStatusNotifierItemItem(indicator.container, role, boxOrder, boxOrders, true);
this.settings.set_strv("right-box-order", boxOrder);
break;
}
}
// Get the resolved box orders for all boxes.
const resolvedBoxOrders = {
left: this._appIndicatorKStatusNotifierItemManager.createResolvedBoxOrder(this.settings.get_strv("left-box-order")),
center: this._appIndicatorKStatusNotifierItemManager.createResolvedBoxOrder(this.settings.get_strv("center-box-order")),
right: this._appIndicatorKStatusNotifierItemManager.createResolvedBoxOrder(this.settings.get_strv("right-box-order")),
};
// Also get the restricted valid box order of the target box.
const restrictedValidBoxOrderOfTargetBox = this._boxOrderCreator.createRestrictedValidBoxOrder(box);
// Get the index of the role for each box order.
const indices = {
left: resolvedBoxOrders.left.indexOf(role),
center: resolvedBoxOrders.center.indexOf(role),
right: resolvedBoxOrders.right.indexOf(role),
};
// If the role is not already configured in one of the box orders,
// just add it to the target box order at the end/beginning, save
// the updated box order and return the relevant position and box.
if (indices.left === -1
&& indices.center === -1
&& indices.right === -1) {
switch (box) {
// For the left and center box, insert the role at the end,
// since they're LTR.
case "left":
boxOrders["left"].push(role);
this.settings.set_strv("left-box-order", boxOrders["left"]);
return {
position: restrictedValidBoxOrderOfTargetBox.length - 1,
box: box
};
case "center":
boxOrders["center"].push(role);
this.settings.set_strv("center-box-order", boxOrders["center"]);
return {
position: restrictedValidBoxOrderOfTargetBox.length - 1,
box: box
};
// For the right box, insert the role at the beginning,
// since it's RTL.
case "right":
boxOrders["right"].unshift(role);
this.settings.set_strv("right-box-order", boxOrders["right"]);
return {
position: 0,
box: box
};
}
}
/// Since the role is already configured in one of the box orders,
/// determine the correct insertion index for the position.
const determineInsertionIndex = (index, restrictedValidBoxOrder, boxOrder) => {
// Set the insertion index initially to 0, so that if no closest
// item can be found, the new item just gets inserted at the
// beginning.
let insertionIndex = 0;
// Find the index of the closest item, which is also in the
// valid box order and before the new item.
// This way, we can insert the new item just after the index of
// this closest item.
for (let i = index - 1; i >= 0; i--) {
let potentialClosestItemIndex = restrictedValidBoxOrder.indexOf(boxOrder[i]);
if (potentialClosestItemIndex !== -1) {
insertionIndex = potentialClosestItemIndex + 1;
break;
}
}
return insertionIndex;
};
if (indices.left !== -1) {
return {
position: determineInsertionIndex(indices.left, this._boxOrderCreator.createRestrictedValidBoxOrder("left"), resolvedBoxOrders.left),
box: "left"
};
}
if (indices.center !== -1) {
return {
position: determineInsertionIndex(indices.center, this._boxOrderCreator.createRestrictedValidBoxOrder("center"), resolvedBoxOrders.center),
box: "center"
};
}
if (indices.right !== -1) {
return {
position: determineInsertionIndex(indices.right, this._boxOrderCreator.createRestrictedValidBoxOrder("right"), resolvedBoxOrders.right),
box: "right"
};
}
};
// Overwrite `Panel._addToPanelBox`.
Panel.Panel.prototype._addToPanelBox = function (role, indicator, position, box) {
// Get the position and box overwrite.
let positionBoxOverwrite;
switch (box) {
case this._leftBox:
positionBoxOverwrite = getPositionAndBoxOverwrite(role, "left", indicator);
break;
case this._centerBox:
positionBoxOverwrite = getPositionAndBoxOverwrite(role, "center", indicator);
break;
case this._rightBox:
positionBoxOverwrite = getPositionAndBoxOverwrite(role, "right", indicator);
break;
}
// Call the original `Panel._addToPanelBox` with the position
// overwrite as the position argument and the box determined by the
// box overwrite as the box argument.
switch (positionBoxOverwrite.box) {
case "left":
this._originalAddToPanelBox(role, indicator, positionBoxOverwrite.position, Main.panel._leftBox);
break;
case "center":
this._originalAddToPanelBox(role, indicator, positionBoxOverwrite.position, Main.panel._centerBox);
break;
case "right":
this._originalAddToPanelBox(role, indicator, positionBoxOverwrite.position, Main.panel._rightBox);
break;
}
};
}
////////////////////////////////////////////////////////////////////////////
/// Helper methods holding logic needed by other methods. ///
////////////////////////////////////////////////////////////////////////////
/**
* An object containing a box order for the left, center and right top bar
* box.
* @typedef {Object} BoxOrders
* @property {string[]} left - The box order for the left top bar box.
* @property {string[]} center - The box order for the center top bar box.
* @property {string[]} right - The box order for the right top bar box.
*/
/**
* This method adds all new items currently present in the Gnome Shell top
* bar to the correct box order and returns the new box orders.
* @returns {BoxOrders} - The updated box orders.
*/
_createUpdatedBoxOrders() {
// Load the configured box orders from settings.
const boxOrders = {
left: this.settings.get_strv("left-box-order"),
center: this.settings.get_strv("center-box-order"),
right: this.settings.get_strv("right-box-order"),
};
// Get items (or rather their roles) 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);
}
// Get the indicator containers (of the items) currently present in the
// Gnome Shell top bar boxes.
const boxOrderIndicatorContainers = {
left: Main.panel._leftBox.get_children(),
center: Main.panel._centerBox.get_children(),
// Reverse this array, since the items in the left and center box
// are logically LTR, while the items in the right box are RTL.
right: Main.panel._rightBox.get_children().reverse()
};
// This function goes through the items (or rather their indicator
// containers) of the given box and adds new items (or rather their
// roles) to the box order.
const addNewItemsToBoxOrder = (boxIndicatorContainers, boxOrder, box) => {
for (const indicatorContainer of boxIndicatorContainers) {
// First get the role associated with the current indicator
// container.
const associatedRole = indicatorContainerRoleMap.get(indicatorContainer);
// Handle an AppIndicator/KStatusNotifierItem item differently.
if (associatedRole.startsWith("appindicator-")) {
this._appIndicatorKStatusNotifierItemManager.handleAppIndicatorKStatusNotifierItemItem(indicatorContainer, associatedRole, boxOrder, boxOrders, box === "right");
continue;
}
// Add the role to the box order, if it isn't in in one already.
if (!boxOrders.left.includes(associatedRole)
&& !boxOrders.center.includes(associatedRole)
&& !boxOrders.right.includes(associatedRole)) {
if (box === "right") {
// Add the items to the beginning for this array, since
// its RTL.
boxOrder.unshift(associatedRole);
} else {
boxOrder.push(associatedRole);
}
}
}
};
addNewItemsToBoxOrder(boxOrderIndicatorContainers.left, boxOrders.left, "left");
addNewItemsToBoxOrder(boxOrderIndicatorContainers.center, boxOrders.center, "center");
addNewItemsToBoxOrder(boxOrderIndicatorContainers.right, boxOrders.right, "right");
return boxOrders;
}
/**
* This method orders the top bar items of the specified box according to
* the configured box orders.
* @param {string} box - The box to order.
*/
_orderTopBarItems(box) {
// Get the valid box order.
const validBoxOrder = this._boxOrderCreator.createValidBoxOrder(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;
}
/// Go through the items (or rather their roles) of the validBoxOrder
/// and order the panelBox accordingly.
for (let i = 0; i < validBoxOrder.length; i++) {
const role = validBoxOrder[i];
// Get the indicator container associated with the current role.
const associatedIndicatorContainer = Main.panel.statusArea[role].container;
associatedIndicatorContainer.get_parent().remove_child(associatedIndicatorContainer);
panelBox.insert_child_at_index(associatedIndicatorContainer, i);
}
// To handle the case, where the box order got set to a permutation
// of an outdated box order, it would be wise, if the caller updated the
// box order now to include the items present in the top bar.
}
}
function init() {
return new Extension();
}

200
src/extension.ts Normal file
View File

@ -0,0 +1,200 @@
"use strict";
import St from "gi://St"
import type Gio from "gi://Gio"
import * as Main from "resource:///org/gnome/shell/ui/main.js";
import * as Panel from "resource:///org/gnome/shell/ui/panel.js";
import { Extension } from "resource:///org/gnome/shell/extensions/extension.js";
import BoxOrderManager from "./extensionModules/BoxOrderManager.js";
import type { Box } from "./extensionModules/BoxOrderManager.js";
export interface CustomPanel extends Panel.Panel {
_leftBox: St.BoxLayout;
_centerBox: St.BoxLayout;
_rightBox: St.BoxLayout;
}
export default class TopBarOrganizerExtension extends Extension {
_settings!: Gio.Settings;
_boxOrderManager!: BoxOrderManager;
_settingsHandlerIds!: number[];
enable(): void {
this._settings = this.getSettings();
this._boxOrderManager = new BoxOrderManager({}, this._settings);
/// Stuff to do on startup(extension enable).
// Initially handle new top bar items and order top bar boxes.
this.#handleNewItemsAndOrderTopBar();
// Overwrite the `Panel._addToPanelBox` method with one handling new
// items.
this.#overwritePanelAddToPanelBox();
// Handle AppIndicators getting ready, to handle new AppIndicator items.
this._boxOrderManager.connect("appIndicatorReady", () => {
this.#handleNewItemsAndOrderTopBar();
});
// Handle changes of settings.
this._settingsHandlerIds = [];
const addSettingsChangeHandler = (settingsName: string) => {
const handlerId = this._settings.connect(`changed::${settingsName}`, () => {
this.#handleNewItemsAndOrderTopBar();
});
this._settingsHandlerIds.push(handlerId);
};
addSettingsChangeHandler("left-box-order");
addSettingsChangeHandler("center-box-order");
addSettingsChangeHandler("right-box-order");
addSettingsChangeHandler("hide");
addSettingsChangeHandler("show");
}
disable(): void {
// Revert the overwrite of `Panel._addToPanelBox`.
// @ts-ignore
Panel.Panel.prototype._addToPanelBox = Panel.Panel.prototype._originalAddToPanelBox;
// Set `Panel._originalAddToPanelBox` to `undefined`.
// @ts-ignore
Panel.Panel.prototype._originalAddToPanelBox = undefined;
// Disconnect signals.
for (const handlerId of this._settingsHandlerIds) {
this._settings.disconnect(handlerId);
}
this._boxOrderManager.disconnectSignals();
// @ts-ignore
this._settings = null;
// @ts-ignore
this._boxOrderManager = null;
}
////////////////////////////////////////////////////////////////////////////
/// Methods used on extension enable. ///
////////////////////////////////////////////////////////////////////////////
/**
* Overwrite `Panel._addToPanelBox` with a custom method, which simply calls
* the original one and handles new items and orders the top bar afterwards.
*/
#overwritePanelAddToPanelBox(): void {
// Add the original `Panel._addToPanelBox` method as
// `Panel._originalAddToPanelBox`.
// @ts-ignore
Panel.Panel.prototype._originalAddToPanelBox = Panel.Panel.prototype._addToPanelBox;
const handleNewItemsAndOrderTopBar = () => {
this.#handleNewItemsAndOrderTopBar();
};
// Overwrite `Panel._addToPanelBox`.
Panel.Panel.prototype._addToPanelBox = function(role, indicator, position, box) {
// Simply call the original `_addToPanelBox` and order the top bar
// and handle new items afterwards.
// @ts-ignore
this._originalAddToPanelBox(role, indicator, position, box);
handleNewItemsAndOrderTopBar();
};
}
////////////////////////////////////////////////////////////////////////////
/// Helper methods holding logic needed by other methods. ///
////////////////////////////////////////////////////////////////////////////
/**
* This method orders the top bar items of the specified box according to
* the configured box orders.
* @param {Box} box - The box to order.
*/
#orderTopBarItems(box: Box): void {
// Only run, when the session mode is "user" or the parent session mode
// is "user".
if(Main.sessionMode.currentMode !== "user" && Main.sessionMode.parentMode !== "user") {
return;
}
// Get the valid box order.
const validBoxOrder = this._boxOrderManager.getValidBoxOrder(box);
// Get the relevant box of `Main.panel`.
let panelBox = (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 as any)[item.role]?.container;
if (!(associatedIndicatorContainer instanceof St.Bin)) {
// TODO: maybe add logging
continue;
}
// Save whether or not the indicator container is visible.
const isVisible = associatedIndicatorContainer.visible;
const parent = associatedIndicatorContainer.get_parent();
if (parent !== null) {
parent.remove_child(associatedIndicatorContainer);
}
if (box === "right") {
// If the target panel box is the right panel box, insert the
// indicator container at index `-1`, which just adds it to the
// end (correct order is ensured, since `validBoxOrder` is
// sorted correctly and we're looping over it in order).
// This way unaccounted-for indicator containers will be at the
// left, which is preferred, since the box is logically
// right-to-left.
// The same applies for indicator containers, which are just
// temporarily unaccounted for (like for indicator containers of
// not yet ready app indicators), since them being at the right
// for a probably temporary stay causes all the indicator
// containers to shift.
panelBox.insert_child_at_index(associatedIndicatorContainer, -1);
} else {
panelBox.insert_child_at_index(associatedIndicatorContainer, i);
}
// Hide the indicator container...
// - ...if it wasn't visible before and the hide property of the
// item is "default".
// - if the hide property of the item is "hide".
// In all other cases have the item show.
// An e.g. screen recording indicator still wouldn't show tho, since
// this here acts on the indicator container, but a screen recording
// indicator is hidden on the indicator level.
if ((!isVisible && item.hide === "default") ||
item.hide === "hide") {
associatedIndicatorContainer.hide();
}
}
// To handle the case, where the box order got set to a permutation
// of an outdated box order, it would be wise, if the caller updated the
// box order now to include the items present in the top bar.
}
/**
* This method handles all new items currently present in the top bar and
* orders the items of all top bar boxes.
*/
#handleNewItemsAndOrderTopBar(): void {
// Only run, when the session mode is "user" or the parent session mode
// is "user".
if(Main.sessionMode.currentMode !== "user" && Main.sessionMode.parentMode !== "user") {
return;
}
this._boxOrderManager.saveNewTopBarItems();
this.#orderTopBarItems("left");
this.#orderTopBarItems("center");
this.#orderTopBarItems("right");
// In `this.#orderTopBarItems` it says to update the box orders to
// include potentially new items, since the ordering might have been
// based on an outdated box order. However, since we already handle new
// top bar items at the beginning of this method, this isn't a concern.
}
}

View File

@ -1,121 +0,0 @@
/*
* This file is part of Top-Bar-Organizer (a Gnome Shell Extension for
* organizing your Gnome Shell top bar).
* Copyright (C) 2021 Julian Schacher
*
* Top-Bar-Organizer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* exported AppIndicatorKStatusNotifierItemManager */
"use strict";
var AppIndicatorKStatusNotifierItemManager = class AppIndicatorKStatusNotifierItemManager {
constructor() {
// Create an application-role map for associating roles with
// applications.
// This is needed so that this class can handle/manage
// AppIndicator/KStatusNotifierItem items.
this._applicationRoleMap = new Map();
}
/**
* Handle an AppIndicator/KStatusNotifierItem item.
*
* This function basically does the following two things:
* - Associate the role of the given item with the application of the
* AppIndicator/KStatusNotifierItem.
* - Add a placeholder for the roles associated with the application of the
* AppIndiciator/KStatusNotifierItem to the box order, if needed.
*
* Note: The caller is responsible for saving the updated box order to
* settings.
* @param {} indicatorContainer - The container of the indicator of the
* AppIndicator/KStatusNotifierItem item.
* @param {string} role - The role of the AppIndicator/KStatusNotifierItem
* item.
* @param {string[]} - The box order the placeholder should be added to, if
* needed.
* @param {BoxOrders} boxOrders - An object containing the box orders, which
* is currently getting worked on.
* @param {boolean} - Whether to add the placeholder to the beginning of the
* box order.
*/
handleAppIndicatorKStatusNotifierItemItem(indicatorContainer, role, boxOrder, boxOrders, atToBeginning = false) {
// Get the application the AppIndicator/KStatusNotifierItem is
// associated with.
const application = indicatorContainer.get_child()._indicator.id;
// Associate the role with the application.
let roles = this._applicationRoleMap.get(application);
if (roles) {
// If the application already has an array of associated roles, just
// add the role to it, if needed.
if (!roles.includes(role)) roles.push(role);
} else {
// Otherwise create a new array.
this._applicationRoleMap.set(application, [ role ]);
}
// Store a placeholder for the roles associated with the application in
// the box order, if needed.
// (Then later the `this.createResolvedBoxOrder` method can be used to
// get a box order, where the placeholder/s get/s replaced with the
// relevant roles (by using `this._applicationRoleMap`).)
const placeholder = `appindicator-kstatusnotifieritem-${application}`;
if (!boxOrders.left.includes(placeholder)
&& !boxOrders.center.includes(placeholder)
&& !boxOrders.right.includes(placeholder)) {
if (atToBeginning) {
boxOrder.unshift(placeholder);
} else {
boxOrder.push(placeholder);
}
}
}
/**
* This function takes a box order and returns a box order, where all
* placeholders got replaced with their relevant roles.
* @param {string[]} boxOrder - The box order of which to replace the
* placeholders.
* @returns {string[]} A resolved box order, where all placeholders got
* replaced with their relevant roles.
*/
createResolvedBoxOrder(boxOrder) {
let resolvedBoxOrder = [ ];
for (const item of boxOrder) {
// If the item isn't a placeholder, just add it to the new resolved
// box order.
if (!item.startsWith("appindicator-kstatusnotifieritem-")) {
resolvedBoxOrder.push(item);
continue;
}
/// If the item is a placeholder, replace it.
// First get the application this placeholder is associated with.
const application = item.replace("appindicator-kstatusnotifieritem-", "");
// Then get the roles associated with the application.
let roles = this._applicationRoleMap.get(application);
// Continue, if there are no roles.
if (!roles) continue;
// Otherwise add the roles
for (const role of roles) {
resolvedBoxOrder.push(role);
}
}
return resolvedBoxOrder;
}
};

View File

@ -1,137 +0,0 @@
/*
* This file is part of Top-Bar-Organizer (a Gnome Shell Extension for
* organizing your Gnome Shell top bar).
* Copyright (C) 2021 Julian Schacher
*
* Top-Bar-Organizer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* exported BoxOrderCreator */
"use strict";
const ExtensionUtils = imports.misc.extensionUtils;
const Main = imports.ui.main;
/**
* A class exposing methods, which create special box orders.
*/
var BoxOrderCreator = class BoxOrderCreator {
/**
* @param {AppIndicatorKStatusNotifierItemManager}
* appIndicatorKStatusNotifierItemManager - An instance of
* AppIndicatorKStatusNotifierItemManager to be used in the methods of
* `this`.
*/
constructor(appIndicatorKStatusNotifierItemManager) {
this._appIndicatorKStatusNotifierItemManager = appIndicatorKStatusNotifierItemManager;
this._settings = ExtensionUtils.getSettings();
}
/**
* This function creates a valid box order for the given box.
* This means it returns a box order for the box, where only roles are
* included, which have their associated indicator container already in some
* box of the Gnome Shell top bar.
* @param {string} box - The box to return the valid box order for.
* Must be one of the following values:
* - "left"
* - "center"
* - "right"
* @returns {string[]} - The valid box order.
*/
createValidBoxOrder(box) {
// Get a resolved box order.
let boxOrder = this._appIndicatorKStatusNotifierItemManager.createResolvedBoxOrder(this._settings.get_strv(`${box}-box-order`));
// Get the indicator containers (of the items) currently present in the
// Gnome Shell top bar.
const boxIndicatorContainers = [ ];
const addIndicatorContainersOfBox = (panelBox) => {
for (const indicatorContainer of panelBox.get_children()) {
boxIndicatorContainers.push(indicatorContainer);
}
};
addIndicatorContainersOfBox(Main.panel._leftBox);
addIndicatorContainersOfBox(Main.panel._centerBox);
addIndicatorContainersOfBox(Main.panel._rightBox);
// Create an indicator containers set from the indicator containers for
// fast easy access.
const boxIndicatorContainersSet = new Set(boxIndicatorContainers);
// Go through the box order and only add items to the valid box order,
// where their indicator is present in the Gnome Shell top bar
// currently.
let validBoxOrder = [ ];
for (const role of boxOrder) {
// Get the indicator container associated with the current role.
const associatedIndicatorContainer = Main.panel.statusArea[role]?.container;
if (boxIndicatorContainersSet.has(associatedIndicatorContainer)) validBoxOrder.push(role);
}
return validBoxOrder;
}
/**
* This function creates a restricted valid box order for the given box.
* This means it returns a box order for the box, where only roles are
* included, which have their associated indicator container already in the
* specified box.
* @param {string} box - The box to return the valid box order for.
* Must be one of the following values:
* - "left"
* - "center"
* - "right"
* @returns {string[]} - The restricted valid box order.
*/
createRestrictedValidBoxOrder(box) {
// Get a resolved box order and get the indicator containers (of the
// items) which are currently present in the Gnome Shell top bar in the
// specified box.
let boxOrder = this._appIndicatorKStatusNotifierItemManager.createResolvedBoxOrder(this._settings.get_strv(`${box}-box-order`));
let boxIndicatorContainers;
switch (box) {
case "left":
boxIndicatorContainers = Main.panel._leftBox.get_children();
break;
case "center":
boxIndicatorContainers = Main.panel._centerBox.get_children();
break;
case "right":
boxIndicatorContainers = Main.panel._rightBox.get_children();
break;
}
// Create an indicator containers set from the indicator containers for
// fast easy access.
const boxIndicatorContainersSet = new Set(boxIndicatorContainers);
// Go through the box order and only add items to the restricted valid
// box order, where their indicator is present in the Gnome Shell top
// bar in the specified box currently.
let restrictedValidBoxOrder = [ ];
for (const role of boxOrder) {
// Get the indicator container associated with the current role.
const associatedIndicatorContainer = Main.panel.statusArea[role]?.container;
if (boxIndicatorContainersSet.has(associatedIndicatorContainer)) restrictedValidBoxOrder.push(role);
}
return restrictedValidBoxOrder;
}
};

View File

@ -0,0 +1,388 @@
"use strict";
import GObject from "gi://GObject";
import St from "gi://St";
import type Gio from "gi://Gio";
import * as Main from "resource:///org/gnome/shell/ui/main.js";
import type { CustomPanel } from "../extension.js"
export type Box = "left" | "center" | "right";
type Hide = "hide" | "show" | "default";
/**
* A resolved box order item containing the items role, settings identifier and
* additional information.
*/
interface ResolvedBoxOrderItem {
settingsId: string // The settings identifier of the item.
role: string // The role of the item.
hide: Hide // Whether the item should be (forcefully) hidden, (forcefully) shown or just be left as is.
}
/**
* This class provides an interfaces to the box orders stored in settings.
* It takes care of handling AppIndicator and Task Up UltraLite items and
* resolving from the internal item settings identifiers to roles.
* In the end this results in convenient functions, which are directly useful in
* other extension code.
*/
export default class BoxOrderManager extends GObject.Object {
static {
GObject.registerClass({
Signals: {
"appIndicatorReady": {},
},
}, this);
}
// Can't have type guarantees here, since this is working with types from
// the KStatusNotifier/AppIndicator extension.
#appIndicatorReadyHandlerIdMap: Map<any, any>;
#appIndicatorItemSettingsIdToRolesMap: Map<string, string[]>;
#taskUpUltraLiteItemRoles: string[];
#settings: Gio.Settings;
constructor(params = {}, settings: Gio.Settings) {
// @ts-ignore Params should be passed, see: https://gjs.guide/guides/gobject/subclassing.html#subclassing-gobject
super(params);
this.#appIndicatorReadyHandlerIdMap = new Map();
this.#appIndicatorItemSettingsIdToRolesMap = new Map();
this.#taskUpUltraLiteItemRoles = [];
this.#settings = settings;
}
/**
* Gets a box order for the given top bar box from settings.
* @param {Box} box - The top bar box for which to get the box order.
* @returns {string[]} - The box order consisting of an array of item
* settings identifiers.
*/
#getBoxOrder(box: Box): string[] {
return this.#settings.get_strv(`${box}-box-order`);
}
/**
* Save the given box order to settings, making sure to only save a changed
* box order, to avoid loops when listening on settings changes.
* @param {Box} box - The top bar box for which to save the box order.
* @param {string[]} boxOrder - The box order to save. Must be an array of
* item settings identifiers.
*/
#saveBoxOrder(box: Box, boxOrder: string[]): void {
const currentBoxOrder = this.#getBoxOrder(box);
// Only save the given box order to settings, if it is different, to
// avoid loops when listening on settings changes.
if (JSON.stringify(boxOrder) !== JSON.stringify(currentBoxOrder)) {
this.#settings.set_strv(`${box}-box-order`, boxOrder);
}
}
/**
* Handles an AppIndicator/KStatusNotifierItem item by deriving a settings
* identifier and then associating the role of the given item to the items
* settings identifier.
* It then returns the derived settings identifier.
* In the case, where the settings identifier can't be derived, because the
* application can't be determined, this method throws an error. However it
* then also makes sure that once the app indicators "ready" signal emits,
* this classes "appIndicatorReady" signal emits as well, such that it and
* other methods can be called again to properly handle the item.
* @param {St.Bin} indicatorContainer - The container of the indicator of the
* AppIndicator/KStatusNotifierItem item.
* @param {string} role - The role of the AppIndicator/KStatusNotifierItem
* item.
* @returns {string} The derived items settings identifier.
*/
#handleAppIndicatorItem(indicatorContainer: St.Bin, role: string): string {
// Since this is working with types from the
// AppIndicator/KStatusNotifierItem extension, we loose a bunch of type
// safety here.
// https://github.com/ubuntu/gnome-shell-extension-appindicator
const appIndicator = (indicatorContainer.get_child() as any)._indicator;
let application = appIndicator.id;
if (!application && this.#appIndicatorReadyHandlerIdMap) {
const handlerId = appIndicator.connect("ready", () => {
this.emit("appIndicatorReady");
appIndicator.disconnect(handlerId);
this.#appIndicatorReadyHandlerIdMap.delete(handlerId);
});
this.#appIndicatorReadyHandlerIdMap.set(handlerId, appIndicator);
throw new Error("Application can't be determined.");
}
// Since the Dropbox client appends its PID to the id, drop the PID and
// the hyphen before it.
if (application.startsWith("dropbox-client-")) {
application = "dropbox-client";
}
// Derive the items settings identifier from the application name.
const itemSettingsId = `appindicator-kstatusnotifieritem-${application}`;
// Associate the role with the items settings identifier.
let roles = this.#appIndicatorItemSettingsIdToRolesMap.get(itemSettingsId);
if (roles) {
// If the settings identifier already has an array of associated
// roles, just add the role to it, if needed.
if (!roles.includes(role)) {
roles.push(role);
}
} else {
// Otherwise create a new array.
this.#appIndicatorItemSettingsIdToRolesMap.set(itemSettingsId, [role]);
}
// Return the item settings identifier.
return itemSettingsId;
}
/**
* Handles a Task Up UltraLite item by storing its role and returning the
* Task Up UltraLite settings identifier.
* This is needed since the Task Up UltraLite extension creates a bunch of
* top bar items as part of its functionality, so we want to group them
* under one identifier in the settings.
* https://extensions.gnome.org/extension/7700/task-up-ultralite/
* @param {string} role - The role of the Task Up UltraLite item.
* @returns {string} The settings identifier to use.
*/
#handleTaskUpUltraLiteItem(role: string): string {
const roles = this.#taskUpUltraLiteItemRoles;
if (!roles.includes(role)) {
roles.push(role);
}
return "item-role-group-task-up-ultralite";
}
/**
* Gets a resolved box order for the given top bar box, where all
* AppIndicator and Task Up UltraLite items got resolved using their roles,
* meaning they might be present multiple times or not at all depending on
* the roles stored.
* The items of the box order also have additional information stored.
* @param {Box} box - The top bar box for which to get the resolved box order.
* @returns {ResolvedBoxOrderItem[]} - The resolved box order.
*/
#getResolvedBoxOrder(box: Box): ResolvedBoxOrderItem[] {
let boxOrder = this.#getBoxOrder(box);
const itemsToHide = this.#settings.get_strv("hide");
const itemsToShow = this.#settings.get_strv("show");
let resolvedBoxOrder = [];
for (const itemSettingsId of boxOrder) {
const resolvedBoxOrderItem = {
settingsId: itemSettingsId,
role: "",
hide: "",
};
// Set the hide state of the item.
if (itemsToHide.includes(resolvedBoxOrderItem.settingsId)) {
resolvedBoxOrderItem.hide = "hide";
} else if (itemsToShow.includes(resolvedBoxOrderItem.settingsId)) {
resolvedBoxOrderItem.hide = "show";
} else {
resolvedBoxOrderItem.hide = "default";
}
// If the items settings identifier doesn't indicate that the item
// is an AppIndicator/KStatusNotifierItem item or the Task Up
// UltraLite item role group, then its identifier is the role and it
// can just be added to the resolved box order.
if (!itemSettingsId.startsWith("appindicator-kstatusnotifieritem-") &&
itemSettingsId !== "item-role-group-task-up-ultralite") {
resolvedBoxOrderItem.role = resolvedBoxOrderItem.settingsId;
resolvedBoxOrder.push(resolvedBoxOrderItem);
continue;
}
// If the items settings identifier indicates otherwise, then handle
// the item specially.
// Get the roles associated with the items settings id.
let roles: string[] = [];
if (itemSettingsId.startsWith("appindicator-kstatusnotifieritem-")) {
roles = this.#appIndicatorItemSettingsIdToRolesMap.get(resolvedBoxOrderItem.settingsId) ?? [];
} else if (itemSettingsId === "item-role-group-task-up-ultralite") {
roles = this.#taskUpUltraLiteItemRoles;
}
// Create a new resolved box order item for each role and add it to
// the resolved box order.
for (const role of roles) {
const newResolvedBoxOrderItem = JSON.parse(JSON.stringify(resolvedBoxOrderItem));
newResolvedBoxOrderItem.role = role;
resolvedBoxOrder.push(newResolvedBoxOrderItem);
}
}
return resolvedBoxOrder;
}
/**
* Disconnects all signals (and disables future signal connection).
* This is typically used before nulling an instance of this class to make
* sure all signals are disconnected.
*/
disconnectSignals(): void {
for (const [handlerId, appIndicator] of this.#appIndicatorReadyHandlerIdMap) {
if (handlerId && appIndicator?.signalHandlerIsConnected(handlerId)) {
appIndicator.disconnect(handlerId);
}
}
// @ts-ignore
this.#appIndicatorReadyHandlerIdMap = null;
}
/**
* Gets a valid box order for the given top bar box, where all AppIndicator
* and Task Up UltraLite items got resolved and where only items are
* included, which are in some GNOME Shell top bar box.
* The items of the box order also have additional information stored.
* @param {Box} box - The top bar box to return the valid box order for.
* @returns {ResolvedBoxOrderItem[]} - The valid box order.
*/
getValidBoxOrder(box: Box): ResolvedBoxOrderItem[] {
// Get a resolved box order.
let resolvedBoxOrder = this.#getResolvedBoxOrder(box);
// Get the indicator containers (of the items) currently present in the
// GNOME Shell top bar.
// They should be St.Bins (see link), so ensure that using a filter.
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/48.2/js/ui/panelMenu.js?ref_type=tags#L21
const indicatorContainers = 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: ResolvedBoxOrderItem[] = [];
for (const item of resolvedBoxOrder) {
const associatedIndicatorContainer = (Main.panel.statusArea as any)[item.role]?.container;
if (!(associatedIndicatorContainer instanceof St.Bin)) {
// TODO: maybe add logging
continue;
}
if (indicatorContainers.has(associatedIndicatorContainer)) {
validBoxOrder.push(item);
}
}
return validBoxOrder;
}
/**
* This method saves all new items currently present in the GNOME Shell top
* bar to the settings.
*/
saveNewTopBarItems(): void {
// Only run, when the session mode is "user" or the parent session mode
// is "user".
if (Main.sessionMode.currentMode !== "user" && Main.sessionMode.parentMode !== "user") {
return;
}
// Get the box orders.
const boxOrders = {
left: this.#getBoxOrder("left"),
center: this.#getBoxOrder("center"),
right: this.#getBoxOrder("right"),
};
// Get roles (of items) currently present in the GNOME Shell top bar and
// index them using their associated indicator container.
let indicatorContainerRoleMap = new Map<St.Bin, string>();
for (const role in (Main.panel.statusArea as any)) {
const associatedIndicatorContainer = (Main.panel.statusArea as any)[role]?.container;
if (!(associatedIndicatorContainer instanceof St.Bin)) {
// TODO: maybe add logging
continue;
}
indicatorContainerRoleMap.set(associatedIndicatorContainer, role);
}
// Get the indicator containers (of the items) currently present in the
// GNOME Shell top bar boxes.
// They should be St.Bins (see link), so ensure that using a filter.
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/48.2/js/ui/panelMenu.js?ref_type=tags#L21
const boxIndicatorContainers = {
left: (Main.panel as CustomPanel)._leftBox.get_children().filter(ic => ic instanceof St.Bin),
center: (Main.panel as CustomPanel)._centerBox.get_children().filter(ic => ic instanceof St.Bin),
// Reverse this array, since the items in the left and center box
// are logically LTR, while the items in the right box are RTL.
right: (Main.panel as CustomPanel)._rightBox.get_children().filter(ic => ic instanceof St.Bin).reverse(),
};
// This function goes through the indicator containers of the given box
// and adds new item settings identifiers to the given box order.
const addNewItemSettingsIdsToBoxOrder = (indicatorContainers: St.Bin[], boxOrder: string[], box: Box) => {
for (const indicatorContainer of indicatorContainers) {
// First get the role associated with the current indicator
// container.
let role = indicatorContainerRoleMap.get(indicatorContainer);
if (!role) {
continue;
}
// Then get a settings identifier for the item.
let itemSettingsId;
if (role.startsWith("appindicator-")) {
// If the role indicates that the item is an
// AppIndicator/KStatusNotifierItem item, then handle it
// differently.
try {
itemSettingsId = this.#handleAppIndicatorItem(indicatorContainer, role);
} catch (e) {
if (!(e instanceof Error)) {
throw(e);
}
if (e.message !== "Application can't be determined.") {
throw(e);
}
continue;
}
} else if (role.startsWith("task-button-")) {
// If the role indicates that the item is a Task Up
// UltraLite item, then handle it differently.
itemSettingsId = this.#handleTaskUpUltraLiteItem(role);
} else { // Otherwise just use the role as the settings identifier.
itemSettingsId = role;
}
// Add the items settings identifier to the box order, if it
// isn't in in one already.
if (!boxOrders.left.includes(itemSettingsId)
&& !boxOrders.center.includes(itemSettingsId)
&& !boxOrders.right.includes(itemSettingsId)) {
if (box === "right") {
// Add the items to the beginning for this array, since
// its RTL.
boxOrder.unshift(itemSettingsId);
} else {
boxOrder.push(itemSettingsId);
}
}
}
};
addNewItemSettingsIdsToBoxOrder(boxIndicatorContainers.left, boxOrders.left, "left");
addNewItemSettingsIdsToBoxOrder(boxIndicatorContainers.center, boxOrders.center, "center");
addNewItemSettingsIdsToBoxOrder(boxIndicatorContainers.right, boxOrders.right, "right");
this.#saveBoxOrder("left", boxOrders.left);
this.#saveBoxOrder("center", boxOrders.center);
this.#saveBoxOrder("right", boxOrders.right);
}
}

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": 2,
"shell-version": [ "40" ],
"version": 15,
"shell-version": [ "45", "46", "47", "48", "49" ],
"settings-schema": "org.gnome.shell.extensions.top-bar-organizer",
"url": "https://gitlab.gnome.org/julianschacher/top-bar-organizer"
"url": "https://gitlab.gnome.org/june/top-bar-organizer"
}

View File

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This file is part of Top-Bar-Organizer (a Gnome Shell Extension for
organizing your Gnome Shell top bar).
Copyright (C) 2021 Julian Schacher
Top-Bar-Organizer is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<interface>
<template class="PrefsBoxOrderItemRow" parent="GtkListBoxRow">
<child>
<object class="GtkBox">
<property name="spacing">10</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<child>
<object class="GtkImage">
<property name="icon-name">list-drag-handle-symbolic</property>
</object>
</child>
<child>
<object class="GtkLabel" id="item-name-display-label"/>
</child>
</object>
</child>
</template>
</interface>

View File

@ -1,101 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This file is part of Top-Bar-Organizer (a Gnome Shell Extension for
organizing your Gnome Shell top bar).
Copyright (C) 2021 Julian Schacher
Top-Bar-Organizer is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<interface>
<template class="PrefsWidget" parent="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">24</property>
<property name="margin-top">36</property>
<property name="margin-bottom">36</property>
<property name="margin-start">36</property>
<property name="margin-end">36</property>
<child>
<object class="GtkLabel">
<property name="label">Simply use drag and drop to order the items any way you want.</property>
<property name="halign">start</property>
<style>
<class name="dim-label"/>
</style>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="label">Left Top Bar Box</property>
<property name="halign">start</property>
<property name="margin-bottom">12</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkListBox" id="left-box-order">
<property name="selection-mode">none</property>
<property name="show-separators">True</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="label">Center Top Bar Box</property>
<property name="halign">start</property>
<property name="margin-bottom">12</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkListBox" id="center-box-order">
<property name="selection-mode">none</property>
<property name="show-separators">True</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="label">Right Top Bar Box</property>
<property name="halign">start</property>
<property name="margin-bottom">12</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkListBox" id="right-box-order">
<property name="selection-mode">none</property>
<property name="show-separators">True</property>
</object>
</child>
</object>
</child>
</template>
</interface>

View File

@ -1,89 +0,0 @@
/*
* This file is part of Top-Bar-Organizer (a Gnome Shell Extension for
* organizing your Gnome Shell top bar).
* Copyright (C) 2021 Julian Schacher
*
* Top-Bar-Organizer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* exported buildPrefsWidget, init */
"use strict";
const Gtk = imports.gi.Gtk;
const GObject = imports.gi.GObject;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const PrefsBoxOrderListEmptyPlaceholder = Me.imports.prefsModules.PrefsBoxOrderListEmptyPlaceholder;
const PrefsBoxOrderItemRow = Me.imports.prefsModules.PrefsBoxOrderItemRow;
var PrefsWidget = GObject.registerClass({
GTypeName: "PrefsWidget",
Template: Me.dir.get_child("prefs-widget.ui").get_uri(),
InternalChildren: [
"left-box-order",
"center-box-order",
"right-box-order"
]
}, class PrefsWidget extends Gtk.Box {
_init(params = {}) {
super._init(params);
this._settings = ExtensionUtils.getSettings();
// Initialize the given `gtkListBox`.
const initializeGtkListBox = (boxOrder, gtkListBox) => {
// Add the items of the given configured box order as
// GtkListBoxRows.
for (const item of boxOrder) {
const listBoxRow = new PrefsBoxOrderItemRow.PrefsBoxOrderItemRow();
listBoxRow.item = item;
if (item.startsWith("appindicator-kstatusnotifieritem-")) {
// Set `item_name_display_label` of the `listBoxRow` to
// something nicer, if the associated item is an
// AppIndicator/KStatusNotifierItem item.
listBoxRow.item_name_display_label.set_label(item.replace("appindicator-kstatusnotifieritem-", ""));
} else {
// Otherwise just set the `item_name_display_label` of the
// `listBoxRow` to `item`.
listBoxRow.item_name_display_label.set_label(item);
}
gtkListBox.append(listBoxRow);
}
// Add a placeholder widget for the case, where `gtkListBox` doesn't
// have any GtkListBoxRows.
gtkListBox.set_placeholder(new PrefsBoxOrderListEmptyPlaceholder.PrefsBoxOrderListEmptyPlaceholder());
};
initializeGtkListBox(this._settings.get_strv("left-box-order"), this._left_box_order);
initializeGtkListBox(this._settings.get_strv("center-box-order"), this._center_box_order);
initializeGtkListBox(this._settings.get_strv("right-box-order"), this._right_box_order);
// Set the box order each GtkListBox is associated with.
// This is needed by the reordering of the GtkListBoxRows, so that the
// updated box orders can be saved.
this._left_box_order.boxOrder = "left-box-order";
this._center_box_order.boxOrder = "center-box-order";
this._right_box_order.boxOrder = "right-box-order";
}
});
function buildPrefsWidget() {
return new PrefsWidget();
}
function init() {
}

32
src/prefs.ts Normal file
View File

@ -0,0 +1,32 @@
"use strict";
import Gtk from "gi://Gtk";
import Gdk from "gi://Gdk";
import { ExtensionPreferences } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";
import PrefsPage from "./prefsModules/PrefsPage.js";
export default class TopBarOrganizerPreferences extends ExtensionPreferences {
getPreferencesWidget() {
const provider = new Gtk.CssProvider();
provider.load_from_path(this.metadata.dir.get_path() + "/css/prefs.css");
const defaultGdkDisplay = Gdk.Display.get_default();
Gtk.StyleContext.add_provider_for_display(
(defaultGdkDisplay as Gdk.Display),
provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
);
const prefsPage = new PrefsPage();
prefsPage.connect("destroy", () => {
Gtk.StyleContext.remove_provider_for_display(
(defaultGdkDisplay as Gdk.Display),
provider
);
});
return prefsPage;
}
}

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

@ -1,132 +0,0 @@
/*
* This file is part of Top-Bar-Organizer (a Gnome Shell Extension for
* organizing your Gnome Shell top bar).
* Copyright (C) 2021 Julian Schacher
*
* Top-Bar-Organizer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* exported PrefsBoxOrderItemRow */
"use strict";
const Gtk = imports.gi.Gtk;
const Gdk = imports.gi.Gdk;
const GObject = imports.gi.GObject;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
var PrefsBoxOrderItemRow = GObject.registerClass({
GTypeName: "PrefsBoxOrderItemRow",
Template: Me.dir.get_child("prefs-box-order-item-row.ui").get_uri(),
Children: ["item-name-display-label"]
}, class PrefsBoxOrderItemRow extends Gtk.ListBoxRow {
_init(params = {}) {
super._init(params);
// Make `this` draggable by creating a drag source and adding it to
// `this`.
let dragSource = new Gtk.DragSource();
dragSource.set_actions(Gdk.DragAction.MOVE);
dragSource.connect("prepare", () => {
return Gdk.ContentProvider.new_for_value(this);
});
this.add_controller(dragSource);
/// Make `this` accept drops by creating a drop target and adding it to
/// `this`.
let dropTarget = new Gtk.DropTarget();
dropTarget.set_gtypes([this.constructor.$gtype]);
dropTarget.set_actions(Gdk.DragAction.MOVE);
// Handle a new drop on `this` properly.
// `value` is the thing getting dropped.
dropTarget.connect("drop", (target, value) => {
// If `this` got dropped onto itself, do nothing.
if (value === this) {
return;
}
// Get the GtkListBoxes of `this` and the drop value.
const ownListBox = this.get_parent();
const valueListBox = value.get_parent();
// Get the position of `this` and the drop value.
const ownPosition = this.get_index();
const valuePosition = value.get_index();
// Remove the drop value from its list box.
valueListBox.remove(value);
// Since an element got potentially removed from the list of `this`,
// get the position of `this` again.
const updatedOwnPosition = this.get_index();
if (ownListBox !== valueListBox) {
// First handle the case where `this` and the drop value are in
// different list boxes.
if ((ownListBox.boxOrder === "right-box-order" && valueListBox.boxOrder === "left-box-order")
|| (ownListBox.boxOrder === "right-box-order" && valueListBox.boxOrder === "center-box-order")
|| (ownListBox.boxOrder === "center-box-order" && valueListBox.boxOrder === "left-box-order")) {
// If the list box of the drop value comes before the list
// box of `this`, add the drop value after `this`.
ownListBox.insert(value, updatedOwnPosition + 1);
} else {
// Otherwise, add the drop value where `this` currently is.
ownListBox.insert(value, updatedOwnPosition);
}
} else {
if (valuePosition < ownPosition) {
// If the drop value was before `this`, add the drop value
// after `this`.
ownListBox.insert(value, updatedOwnPosition + 1);
} else {
// Otherwise, add the drop value where `this` currently is.
ownListBox.insert(value, updatedOwnPosition);
}
}
/// Finally save the box orders to settings.
const settings = ExtensionUtils.getSettings();
let updatedBoxOrder = [ ];
for (let potentialListBoxRow of ownListBox) {
// Only process PrefsBoxOrderItemRows.
if (potentialListBoxRow.constructor.$gtype.name !== "PrefsBoxOrderItemRow") {
continue;
}
const item = potentialListBoxRow.item;
updatedBoxOrder.push(item);
}
settings.set_strv(ownListBox.boxOrder, updatedBoxOrder);
// If the list boxes of `this` and the drop value were different,
// save an updated box order for the list were the drop value was in
// as well.
if (ownListBox !== valueListBox) {
let updatedBoxOrder = [ ];
for (let potentialListBoxRow of valueListBox) {
// Only process PrefsBoxOrderItemRows.
if (potentialListBoxRow.constructor.$gtype.name !== "PrefsBoxOrderItemRow") {
continue;
}
const item = potentialListBoxRow.item;
updatedBoxOrder.push(item);
}
settings.set_strv(valueListBox.boxOrder, updatedBoxOrder);
}
});
this.add_controller(dropTarget);
}
});

View File

@ -0,0 +1,161 @@
"use strict";
import Gtk from "gi://Gtk";
import Gdk from "gi://Gdk";
import GObject from "gi://GObject";
import Adw from "gi://Adw";
import GLib from "gi://GLib";
import 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),
Signals: {
"move": {
param_types: [GObject.TYPE_STRING],
},
},
}, this);
this.install_action("row.forget", null, (self, _actionName, _param) => {
const parentListBox = self.get_parent() as PrefsBoxOrderListBox;
parentListBox.removeRow(self as PrefsBoxOrderItemRow);
parentListBox.saveBoxOrderToSettings();
parentListBox.determineRowMoveActionEnable();
});
this.install_action("row.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"));
}
item: string;
#drag_starting_point_x?: number;
#drag_starting_point_y?: number;
constructor(params = {}, item: string) {
super(params);
// Associate `this` with an item.
this.item = item;
if (this.item.startsWith("appindicator-kstatusnotifieritem-")) {
// Set the title to something nicer, if the associated item is an
// AppIndicator/KStatusNotifierItem item.
this.set_title(this.item.replace("appindicator-kstatusnotifieritem-", ""));
} else if (this.item === "item-role-group-task-up-ultralite") {
// Set the title to something nicer, if the item in question is the
// Task Up UltraLite item role group.
this.set_title("Task Up UltraLite Items");
} else {
// Otherwise just set it to `item`.
this.set_title(this.item);
}
}
onDragPrepare(_source: Gtk.DragSource, x: number, y: number): Gdk.ContentProvider {
const value = new GObject.Value();
value.init(PrefsBoxOrderItemRow.$gtype);
value.set_object(this);
this.#drag_starting_point_x = x;
this.#drag_starting_point_y = y;
return Gdk.ContentProvider.new_for_value(value);
}
onDragBegin(_source: Gtk.DragSource, drag: Gdk.Drag): void {
let dragWidget = new Gtk.ListBox();
let allocation = this.get_allocation();
dragWidget.set_size_request(allocation.width, allocation.height);
let dragPrefsBoxOrderItemRow = new PrefsBoxOrderItemRow({}, this.item);
dragWidget.append(dragPrefsBoxOrderItemRow);
dragWidget.drag_highlight_row(dragPrefsBoxOrderItemRow);
let currentDragIcon = Gtk.DragIcon.get_for_drag(drag);
currentDragIcon.set_child(dragWidget);
// Even tho this should always be the case, ensure the values for the hotspot aren't undefined.
if (typeof this.#drag_starting_point_x !== "undefined" &&
typeof this.#drag_starting_point_y !== "undefined") {
drag.set_hotspot(this.#drag_starting_point_x, this.#drag_starting_point_y);
}
}
// Handle a new drop on `this` properly.
// `value` is the thing getting dropped.
onDrop(_target: Gtk.DropTarget, value: any, _x: number, _y: number): boolean {
// According to the type annotations of Gtk.DropTarget, value is of type
// GObject.Value, so ensure the one we work with is of type
// PrefsBoxOrderItemRow.
if (!(value instanceof PrefsBoxOrderItemRow)) {
// TODO: maybe add logging
return false;
}
// If `this` got dropped onto itself, do nothing.
if (value === this) {
return false;
}
// Get the GtkListBoxes of `this` and the drop value.
const ownListBox = this.get_parent() as PrefsBoxOrderListBox;
const valueListBox = value.get_parent() as PrefsBoxOrderListBox;
// Get the position of `this` and the drop value.
const ownPosition = this.get_index();
const valuePosition = value.get_index();
// Remove the drop value from its list box.
valueListBox.removeRow(value);
// Since an element got potentially removed from the list of `this`,
// get the position of `this` again.
const updatedOwnPosition = this.get_index();
if (ownListBox !== valueListBox) {
// First handle the case where `this` and the drop value are in
// different list boxes.
if ((ownListBox.boxOrder === "right-box-order" && valueListBox.boxOrder === "left-box-order")
|| (ownListBox.boxOrder === "right-box-order" && valueListBox.boxOrder === "center-box-order")
|| (ownListBox.boxOrder === "center-box-order" && valueListBox.boxOrder === "left-box-order")) {
// If the list box of the drop value comes before the list
// box of `this`, add the drop value after `this`.
ownListBox.insertRow(value, updatedOwnPosition + 1);
} else {
// Otherwise, add the drop value where `this` currently is.
ownListBox.insertRow(value, updatedOwnPosition);
}
} else {
if (valuePosition < ownPosition) {
// If the drop value was before `this`, add the drop value
// after `this`.
ownListBox.insertRow(value, updatedOwnPosition + 1);
} else {
// Otherwise, add the drop value where `this` currently is.
ownListBox.insertRow(value, updatedOwnPosition);
}
}
/// Finally save the box order(/s) to settings and make sure move
/// actions are correctly enabled/disabled.
ownListBox.saveBoxOrderToSettings();
ownListBox.determineRowMoveActionEnable();
// If the list boxes of `this` and the drop value were different, handle
// the former list box of the drop value as well.
if (ownListBox !== valueListBox) {
valueListBox.saveBoxOrderToSettings();
valueListBox.determineRowMoveActionEnable();
}
return true;
}
}

View File

@ -0,0 +1,152 @@
"use strict";
import Gtk from "gi://Gtk";
import GObject from "gi://GObject";
import GLib from "gi://GLib";
import type Gio from "gi://Gio";
import { ExtensionPreferences } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";
import PrefsBoxOrderItemRow from "./PrefsBoxOrderItemRow.js";
import PrefsBoxOrderListEmptyPlaceholder from "./PrefsBoxOrderListEmptyPlaceholder.js";
export default class PrefsBoxOrderListBox extends Gtk.ListBox {
static {
GObject.registerClass({
GTypeName: "PrefsBoxOrderListBox",
Template: GLib.uri_resolve_relative(import.meta.url, "../ui/prefs-box-order-list-box.ui", GLib.UriFlags.NONE),
Properties: {
BoxOrder: GObject.ParamSpec.string(
"box-order",
"Box Order",
"The box order this PrefsBoxOrderListBox is associated with.",
GObject.ParamFlags.READWRITE,
""
),
},
Signals: {
"row-move": {
param_types: [PrefsBoxOrderItemRow.$gtype, GObject.TYPE_STRING],
},
},
}, this);
}
_boxOrder!: string;
#settings: Gio.Settings;
#rowSignalHandlerIds = new Map<PrefsBoxOrderItemRow, number[]>();
/**
* @param {Object} params
*/
constructor(params = {}) {
super(params);
// Load the settings.
this.#settings = ExtensionPreferences.lookupByURL(import.meta.url)!.getSettings();
// Add a placeholder widget for the case, where no GtkListBoxRows are
// present.
this.set_placeholder(new PrefsBoxOrderListEmptyPlaceholder());
}
get boxOrder(): string {
return this._boxOrder;
}
set boxOrder(value: string) {
this._boxOrder = value;
// Get the actual box order for the given box order name from settings.
const boxOrder = this.#settings.get_strv(this._boxOrder);
// Populate this GtkListBox with GtkListBoxRows for the items of the
// given configured box order.
for (const item of boxOrder) {
const row = new PrefsBoxOrderItemRow({}, item);
this.insertRow(row, -1);
}
this.determineRowMoveActionEnable();
this.notify("box-order");
}
/**
* Inserts the given PrefsBoxOrderItemRow to this list box at the given
* position.
* Also handles stuff like connecting signals.
*/
insertRow(row: PrefsBoxOrderItemRow, position: number): void {
this.insert(row, position);
const signalHandlerIds: number[] = [];
signalHandlerIds.push(row.connect("move", (row, direction) => {
this.emit("row-move", row, direction);
}));
this.#rowSignalHandlerIds.set(row, signalHandlerIds);
}
/**
* Removes the given PrefsBoxOrderItemRow from this list box.
* Also handles stuff like disconnecting signals.
*/
removeRow(row: PrefsBoxOrderItemRow): void {
const signalHandlerIds = this.#rowSignalHandlerIds.get(row) ?? [];
for (const id of signalHandlerIds) {
row.disconnect(id);
}
this.remove(row);
}
/**
* Saves the box order represented by `this` (and its
* `PrefsBoxOrderItemRows`) to settings.
*/
saveBoxOrderToSettings(): void {
let currentBoxOrder: string[] = [];
for (let potentialPrefsBoxOrderItemRow of this) {
// Only process PrefsBoxOrderItemRows.
if (!(potentialPrefsBoxOrderItemRow instanceof PrefsBoxOrderItemRow)) {
continue;
}
const item = potentialPrefsBoxOrderItemRow.item;
currentBoxOrder.push(item);
}
this.#settings.set_strv(this.boxOrder, currentBoxOrder);
}
/**
* Determines whether or not each move action of each PrefsBoxOrderItemRow
* should be enabled or disabled.
*/
determineRowMoveActionEnable(): void {
for (let potentialPrefsBoxOrderItemRow of this) {
// Only process PrefsBoxOrderItemRows.
if (!(potentialPrefsBoxOrderItemRow instanceof PrefsBoxOrderItemRow)) {
continue;
}
const row = potentialPrefsBoxOrderItemRow;
// If the current row is the topmost row in the topmost list box,
// then disable the move-up action.
if (row.get_index() === 0 && this.boxOrder === "left-box-order") {
row.action_set_enabled("row.move-up", false);
} else { // Else enable it.
row.action_set_enabled("row.move-up", true);
}
// If the current row is the bottommost row in the bottommost list
// box, then disable the move-down action.
const rowNextSibling = row.get_next_sibling();
if ((rowNextSibling instanceof PrefsBoxOrderListEmptyPlaceholder || rowNextSibling === null) && this.boxOrder === "right-box-order") {
row.action_set_enabled("row.move-down", false);
} else { // Else enable it.
row.action_set_enabled("row.move-down", true);
}
}
}
}

View File

@ -1,73 +0,0 @@
/*
* This file is part of Top-Bar-Organizer (a Gnome Shell Extension for
* organizing your Gnome Shell top bar).
* Copyright (C) 2021 Julian Schacher
*
* Top-Bar-Organizer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* exported PrefsBoxOrderListEmptyPlaceholder */
"use strict";
const Gtk = imports.gi.Gtk;
const Gdk = imports.gi.Gdk;
const GObject = imports.gi.GObject;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
var PrefsBoxOrderListEmptyPlaceholder = GObject.registerClass({
GTypeName: "PrefsBoxOrderListEmptyPlaceholder",
Template: Me.dir.get_child("prefs-box-order-list-empty-placeholder.ui").get_uri()
}, class PrefsBoxOrderListEmptyPlaceholder extends Gtk.Box {
_init(params = {}) {
super._init(params);
/// Make `this` accept drops by creating a drop target and adding it to
/// `this`.
let dropTarget = new Gtk.DropTarget();
dropTarget.set_gtypes([GObject.type_from_name("PrefsBoxOrderItemRow")]);
dropTarget.set_actions(Gdk.DragAction.MOVE);
// Handle a new drop on `this` properly.
// `value` is the thing getting dropped.
dropTarget.connect("drop", (target, value) => {
// Get the GtkListBoxes of `this` and the drop value.
const ownListBox = this.get_parent();
const valueListBox = value.get_parent();
// Remove the drop value from its list box.
valueListBox.remove(value);
// Insert the drop value into the list box of `this`.
ownListBox.insert(value, 0);
/// Finally save the box orders to settings.
const settings = ExtensionUtils.getSettings();
settings.set_strv(ownListBox.boxOrder, [value.item]);
let updatedBoxOrder = [ ];
for (let potentialListBoxRow of valueListBox) {
// Only process PrefsBoxOrderItemRows.
if (potentialListBoxRow.constructor.$gtype.name !== "PrefsBoxOrderItemRow") {
continue;
}
const item = potentialListBoxRow.item;
updatedBoxOrder.push(item);
}
settings.set_strv(valueListBox.boxOrder, updatedBoxOrder);
});
this.add_controller(dropTarget);
}
});

View File

@ -0,0 +1,48 @@
"use strict";
import Gtk from "gi://Gtk";
import GObject from "gi://GObject";
import GLib from "gi://GLib";
import PrefsBoxOrderItemRow from "./PrefsBoxOrderItemRow.js";
import type PrefsBoxOrderListBox from "./PrefsBoxOrderListBox.js";
export default class PrefsBoxOrderListEmptyPlaceholder extends Gtk.Box {
static {
GObject.registerClass({
GTypeName: "PrefsBoxOrderListEmptyPlaceholder",
Template: GLib.uri_resolve_relative(import.meta.url, "../ui/prefs-box-order-list-empty-placeholder.ui", GLib.UriFlags.NONE),
}, this);
}
// Handle a new drop on `this` properly.
// `value` is the thing getting dropped.
onDrop(_target: Gtk.DropTarget, value: any, _x: number, _y: number): boolean {
// According to the type annotations of Gtk.DropTarget, value is of type
// GObject.Value, so ensure the one we work with is of type
// PrefsBoxOrderItemRow.
if (!(value instanceof PrefsBoxOrderItemRow)) {
// TODO: maybe add logging
return false;
}
// Get the GtkListBoxes of `this` and the drop value.
const ownListBox = this.get_parent() as PrefsBoxOrderListBox;
const valueListBox = value.get_parent() as PrefsBoxOrderListBox;
// Remove the drop value from its list box.
valueListBox.removeRow(value);
// Insert the drop value into the list box of `this`.
ownListBox.insertRow(value, 0);
/// Finally save the box orders to settings and make sure move actions
/// are correctly enabled/disabled.
ownListBox.saveBoxOrderToSettings();
ownListBox.determineRowMoveActionEnable();
valueListBox.saveBoxOrderToSettings();
valueListBox.determineRowMoveActionEnable();
return true;
}
}

View File

@ -0,0 +1,196 @@
"use strict";
import Gdk from "gi://Gdk";
import Gtk from "gi://Gtk";
import GObject from "gi://GObject";
import Adw from "gi://Adw";
import GLib from "gi://GLib";
import ScrollManager from "./ScrollManager.js";
import PrefsBoxOrderListEmptyPlaceholder from "./PrefsBoxOrderListEmptyPlaceholder.js";
import type PrefsBoxOrderItemRow from "./PrefsBoxOrderItemRow.js";
// Imports to make UI file work.
// eslint-disable-next-line
import PrefsBoxOrderListBox from "./PrefsBoxOrderListBox.js";
export default class PrefsPage extends Adw.PreferencesPage {
static {
GObject.registerClass({
GTypeName: "PrefsPage",
Template: GLib.uri_resolve_relative(import.meta.url, "../ui/prefs-page.ui", GLib.UriFlags.NONE),
InternalChildren: [
"left-box-order-list-box",
"center-box-order-list-box",
"right-box-order-list-box",
],
}, this);
}
_dndEnded?: boolean;
declare _left_box_order_list_box: PrefsBoxOrderListBox;
declare _center_box_order_list_box: PrefsBoxOrderListBox;
declare _right_box_order_list_box: PrefsBoxOrderListBox;
constructor(params = {}) {
super(params);
this.#setupDNDScroll();
}
/**
* This function sets up Drag-and-Drop scrolling.
* This means that scroll up or down is happening when a Drag-and-Drop
* operation is in progress and the user has their cursor either in the
* upper or lower 10% of this widget respectively.
*/
#setupDNDScroll(): void {
// Pass `this.get_first_child()` to the ScrollManager, since this
// `PrefsPage` extends an `Adw.PreferencesPage` and the first child of
// an `Adw.PreferencesPage` is the built-in `Gtk.ScrolledWindow`.
const scrollManager = new ScrollManager(this.get_first_child() as Gtk.ScrolledWindow);
/// Setup GtkDropControllerMotion event controller and make use of its
/// events.
let controller = new Gtk.DropControllerMotion();
// Make sure scrolling stops, when DND operation ends.
this._dndEnded = true;
// Scroll, when the pointer is in the right places and a DND operation
// is properly set up (this._dndEnded is false).
controller.connect("motion", (_, _x, y) => {
if ((y <= this.get_allocated_height() * 0.1) && !this._dndEnded) {
// If the pointer is currently in the upper ten percent of this
// widget, then scroll up.
scrollManager.startScrollUp();
} else if ((y >= this.get_allocated_height() * 0.9) && !this._dndEnded) {
// If the pointer is currently in the lower ten percent of this
// widget, then scroll down.
scrollManager.startScrollDown();
} else {
// Otherwise stop scrolling.
scrollManager.stopScrollAll();
}
});
const stopScrollAllAtDNDEnd = () => {
this._dndEnded = true;
scrollManager.stopScrollAll();
};
controller.connect("leave", () => {
stopScrollAllAtDNDEnd();
});
controller.connect("enter", () => {
// Make use of `this._dndEnded` to setup stopScrollAtDNDEnd only
// once per DND operation.
if (this._dndEnded) {
const drag = controller.get_drop()?.get_drag() ?? null;
// Ensure we have a Gdk.Drag.
// If this is not the case for whatever reason, then don't start
// DND scrolling and just return.
if (!(drag instanceof Gdk.Drag)) {
// TODO: maybe add logging
return;
}
drag.connect("drop-performed", () => {
stopScrollAllAtDNDEnd();
});
drag.connect("dnd-finished", () => {
stopScrollAllAtDNDEnd();
});
drag.connect("cancel", () => {
stopScrollAllAtDNDEnd();
});
this._dndEnded = false;
}
});
this.add_controller(controller);
}
onRowMove(listBox: PrefsBoxOrderListBox, row: PrefsBoxOrderItemRow, direction: string): void {
const rowPosition = row.get_index();
if (direction === "up") { // If the direction of the move is up.
// Handle the case, where the row is the topmost row in the list box.
if (rowPosition === 0) {
switch (listBox.boxOrder) {
// If the row is also in the topmost list box, then do
// nothing and return.
case "left-box-order":
log("The row is already the topmost row in the topmost box order.");
return;
// If the row is in the center list box, then move it up to
// the left one.
case "center-box-order":
listBox.removeRow(row);
this._left_box_order_list_box.insertRow(row, -1);
// First save the box order of the destination, then do
// "a save for clean up".
this._left_box_order_list_box.saveBoxOrderToSettings();
this._left_box_order_list_box.determineRowMoveActionEnable();
listBox.saveBoxOrderToSettings();
listBox.determineRowMoveActionEnable();
return;
// If the row is in the right list box, then move it up to
// the center one.
case "right-box-order":
listBox.removeRow(row);
this._center_box_order_list_box.insertRow(row, -1);
this._center_box_order_list_box.saveBoxOrderToSettings();
this._center_box_order_list_box.determineRowMoveActionEnable();
listBox.saveBoxOrderToSettings();
listBox.determineRowMoveActionEnable();
return;
}
}
// Else just move the row up in the box.
listBox.removeRow(row);
listBox.insertRow(row, rowPosition - 1);
listBox.saveBoxOrderToSettings();
listBox.determineRowMoveActionEnable();
return;
} else { // Else the direction of the move must be down.
// Handle the case, where the row is the bottommost row in the list box.
const rowNextSibling = row.get_next_sibling();
if (rowNextSibling instanceof PrefsBoxOrderListEmptyPlaceholder || rowNextSibling === null) {
switch (listBox.boxOrder) {
// If the row is also in the bottommost list box, then do
// nothing and return.
case "right-box-order":
log("The row is already the bottommost row in the bottommost box order.");
return;
// If the row is in the center list box, then move it down
// to the right one.
case "center-box-order":
listBox.removeRow(row);
this._right_box_order_list_box.insertRow(row, 0);
this._right_box_order_list_box.saveBoxOrderToSettings();
this._right_box_order_list_box.determineRowMoveActionEnable();
listBox.saveBoxOrderToSettings();
listBox.determineRowMoveActionEnable();
return;
// If the row is in the left list box, then move it down to
// the center one.
case "left-box-order":
listBox.removeRow(row);
this._center_box_order_list_box.insertRow(row, 0);
this._center_box_order_list_box.saveBoxOrderToSettings();
this._center_box_order_list_box.determineRowMoveActionEnable();
listBox.saveBoxOrderToSettings();
listBox.determineRowMoveActionEnable();
return;
}
}
// Else just move the row down in the box.
listBox.removeRow(row);
listBox.insertRow(row, rowPosition + 1);
listBox.saveBoxOrderToSettings();
listBox.determineRowMoveActionEnable();
return;
}
}
}

View File

@ -0,0 +1,87 @@
"use strict";
import GLib from "gi://GLib";
import type Gtk from "gi://Gtk";
export default class ScrollManager {
#gtkScrolledWindow: Gtk.ScrolledWindow;
#scrollUp: boolean;
#scrollDown: boolean;
constructor(gtkScrolledWindow: Gtk.ScrolledWindow) {
this.#gtkScrolledWindow = gtkScrolledWindow;
this.#scrollUp = false;
this.#scrollDown = false;
}
startScrollUp(): void {
// If the scroll up is already started, don't do anything.
if (this.#scrollUp) {
return;
}
// Make sure scroll down is stopped.
this.stopScrollDown();
this.#scrollUp = true;
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 200, () => {
// Set the new vadjustment value to either the current value minus a
// step increment or to 0.
const newVAdjustementValue = Math.max(this.#gtkScrolledWindow.vadjustment.get_value() - this.#gtkScrolledWindow.vadjustment.get_step_increment(), 0);
// If the new value is the old one, return and stop this interval.
if (newVAdjustementValue === this.#gtkScrolledWindow.vadjustment.get_value()) {
this.#scrollUp = false;
return this.#scrollUp;
}
// Otherwise, update the value.
this.#gtkScrolledWindow.vadjustment.set_value(newVAdjustementValue);
return this.#scrollUp;
});
}
startScrollDown(): void {
// If the scroll down is already started, don't do anything.
if (this.#scrollDown) {
return;
}
// Make sure scroll up is stopped.
this.stopScrollUp();
this.#scrollDown = true;
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 200, () => {
// Set the new vadjusment value either to the curent value plus a
// step increment or to the upper value minus the page size.
const newVAdjustementValue = Math.min(
this.#gtkScrolledWindow.vadjustment.get_value() + this.#gtkScrolledWindow.vadjustment.get_step_increment(),
this.#gtkScrolledWindow.vadjustment.get_upper() - this.#gtkScrolledWindow.vadjustment.get_page_size()
);
// If the new value is the old one, return and stop this interval.
if (newVAdjustementValue === this.#gtkScrolledWindow.vadjustment.get_value()) {
this.#scrollDown = false;
return this.#scrollDown;
}
// Otherwise, update the value.
this.#gtkScrolledWindow.vadjustment.set_value(newVAdjustementValue);
return this.#scrollDown;
});
}
stopScrollUp(): void {
this.#scrollUp = false;
}
stopScrollDown(): void {
this.#scrollDown = false;
}
stopScrollAll(): void {
this.stopScrollUp();
this.stopScrollDown();
}
}

View File

@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This file is part of Top-Bar-Organizer (a Gnome Shell Extension for
organizing your Gnome Shell top bar).
Copyright (C) 2021 Julian Schacher
Top-Bar-Organizer is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<schemalist>
<schema id="org.gnome.shell.extensions.top-bar-organizer" path="/org/gnome/shell/extensions/top-bar-organizer/">
<key name="left-box-order" type="as">
<description>Order of items in the left box of the top bar.</description>
<default>[]</default>
</key>
<key name="center-box-order" type="as">
<description>Order of items in the center box of the top bar.</description>
<default>[]</default>
</key>
<key name="right-box-order" type="as">
<description>Order of items in the right box of the top bar.</description>
<default>[]</default>
</key>
</schema>
</schemalist>

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