├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── .vscode
└── tasks.json
├── LICENSE
├── README.md
├── build
├── create-translation
├── extension
├── assets
│ └── icons
│ │ ├── notification-symbolic.svg
│ │ └── window-symbolic.svg
├── extension.js
├── locale
│ ├── fr.po
│ ├── nl.po
│ └── rocketbar.pot
├── metadata.json
├── prefs.js
├── schemas
│ └── org.gnome.shell.extensions.rocketbar.gschema.xml
├── services
│ ├── notificationService.js
│ └── soundVolumeService.js
├── settings
│ ├── aboutPage.js
│ ├── behaviorPage.js
│ ├── customizePage.js
│ ├── generalPage.js
│ └── pageTemplate.js
├── shell
│ └── tweaks.js
├── stylesheet.css
├── ui
│ ├── appButton.js
│ ├── appButtonIndicator.js
│ ├── appButtonMenu.js
│ ├── appButtonNotificationBadge.js
│ ├── appButtonTooltip.js
│ ├── notificationCounter.js
│ └── taskbar.js
└── utils
│ ├── config.js
│ ├── connections.js
│ ├── dominantColorExtractor.js
│ ├── favorites.js
│ ├── iconProvider.js
│ ├── launcherAPI.js
│ ├── positionProvider.js
│ ├── scrollHandler.js
│ └── timeout.js
├── install
├── media
├── customize.png
├── get-it-logo.png
├── taskbar.jpg
├── taskbar_bottom.png
└── taskbar_top.png
├── uninstall
└── update-pot
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help me improve Rocketbar
4 | title: ''
5 | labels: bug
6 | assignees: ChepKun
7 |
8 | ---
9 |
10 | **Description**
11 | A clear and concise description of what the bug is and how to reproduce it.
12 |
13 | **Screenshots**
14 | If applicable, add screenshots to help explain the problem.
15 |
16 |
17 | Debug logs
18 |
19 | ```
20 | # Run in terminal and paste the output here
21 | journalctl -o cat /usr/bin/gnome-shell | grep Rocketbar
22 | ```
23 |
24 |
25 | **System information**
26 | - GNOME version:
27 | - Extension verion:
28 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this extension
4 | title: ''
5 | labels: feature
6 | assignees: ChepKun
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.compiled
2 | *.zip
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "build",
6 | "type": "shell",
7 | "command": "./build",
8 | "group": "build",
9 | "presentation": {
10 | "reveal": "always",
11 | "panel": "shared"
12 | },
13 | "problemMatcher": []
14 | },
15 | {
16 | "label": "install",
17 | "type": "shell",
18 | "command": "./install",
19 | "group": "build",
20 | "presentation": {
21 | "reveal": "always",
22 | "panel": "shared"
23 | },
24 | "problemMatcher": []
25 | },
26 | {
27 | "label": "uninstall",
28 | "type": "shell",
29 | "command": "./uninstall",
30 | "group": "build",
31 | "presentation": {
32 | "reveal": "always",
33 | "panel": "shared"
34 | },
35 | "problemMatcher": []
36 | },
37 | {
38 | "label": "debug",
39 | "type": "shell",
40 | "command": "journalctl -f -o cat /usr/bin/gnome-shell",
41 | "group": "build",
42 | "presentation": {
43 | "reveal": "always",
44 | "panel": "shared"
45 | },
46 | "problemMatcher": []
47 | },
48 | {
49 | "label": "nested-shell",
50 | "type": "shell",
51 | "command": "env MUTTER_DEBUG_DUMMY_MODE_SPECS='1920x1000@60.0' dbus-run-session -- gnome-shell --nested --wayland",
52 | "group": "build",
53 | "presentation": {
54 | "reveal": "always",
55 | "panel": "shared"
56 | },
57 | "problemMatcher": []
58 | },
59 | {
60 | "label": "update-pot",
61 | "type": "shell",
62 | "command": "./update-pot",
63 | "group": "build",
64 | "presentation": {
65 | "reveal": "always",
66 | "panel": "shared"
67 | },
68 | "problemMatcher": []
69 | }
70 | ]
71 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Rocketbar
2 | [](https://github.com/linux-is-awesome/gnome_extension_rocketbar/blob/master/LICENSE)
3 |
4 | 
5 |
6 | -----
7 |
8 | ### Key Features
9 |
10 | - Taskbar
11 | - Optimized for best performance
12 | - Doesn't hurt CPU and Shell on every mouse click
13 | - Highly customizable
14 | - Dominant color support for app buttons and indicators
15 | - Optimized to work with a fully transparent panel
16 | - Supports both top and bottom positions of the Main panel
17 | - Per app customization feature
18 | - Drag and Drop support to reorder existing and pin new apps in the taskbar
19 | - Displaying of notification badges on top of app buttons
20 | - Tooltips with additional information such as windows count and notification count
21 | - Set focus on urgent windows of an active application automatically (Fixes 'Open Folder' dialog in VS Code and so on)
22 |
23 | - Shell Tweaks
24 | - Dash killing feature to hide the Dash and prevent it from rendering behind the scene
25 | - Scroll the Main panel to change sound volume and middle click to toggle mute
26 | - Activities button click behavior override
27 | - Overview empty space clicks support
28 | - Fullscreen Hot Corner
29 |
30 | Note: to get additional customization options for the GNOME Shell I would suggest to use [Just Perfection](https://extensions.gnome.org/extension/3843/just-perfection) extension.
31 |
32 | -----
33 |
34 | ### Demonstration
35 |
36 | - Taskbar on top
37 |
38 | 
39 |
40 | - Taskbar on bottom
41 |
42 | 
43 |
44 | ### Per app customization options
45 |
46 | - Right click on an app button to open a context menu
47 | - Find Customize section
48 | - Change options or reset current customizations
49 | - Activitation Behavior
50 | - New Window - if an app is displaying as not running for the current workspace, create a new window of the app on the workspace
51 | - Move Windows - if app is running on another workspace, move windows of the app to the current one. Can be useful for apps that don't support creating of new windows, such as GitHub Desktop and etc.
52 | - Icon Size
53 | - Allows to change the icon size a bit when app icon looks smaller/bigger than others
54 |
55 | 
56 |
57 | -----
58 |
59 | ### Get the latest official release
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | ### Manual installation steps to get the latest and greatest version
68 |
69 | - open Terminal and run the following commands:
70 | ```
71 | git clone https://github.com/linux-is-awesome/gnome_extension_rocketbar
72 | cd ./gnome_extension_rocketbar
73 | OPTIONAL: to get the latest UNSTABLE version type: git checkout develop
74 | ./install
75 | ```
76 |
77 | - push Alt + F2 and type 'r'
78 | - go to Extensions and enable Rocketbar there
79 |
80 | -----
81 |
82 | ### Special thanks to
83 |
84 | - Gnome 45 support by [@laurentlbm](https://github.com/laurentlbm)
85 |
86 | -----
87 |
88 | ### Translations
89 |
90 | - French by [@celestomm](https://github.com/celestomm)
91 | - Dutch by [@Vistaus](https://github.com/Vistaus)
92 |
93 | To create a new translation:
94 |
95 | - open Terminal and run the following commands:
96 | ```
97 | git clone https://github.com/linux-is-awesome/gnome_extension_rocketbar
98 | cd ./gnome_extension_rocketbar
99 | git branch translation_ (translation_en, translation_es, translation_ru and so on...)
100 | git checkout
101 | ./create-translation
102 | ```
103 | - A new .po file will be created under the extension/locales folder
104 | - Go to the file, translate text strings
105 | - Commit your changes and publish the branch
106 | - Create a pull request to the 'develop' branch
107 |
108 | -----
109 |
110 | ### Credits
111 |
112 | [App Icons Taskbar](https://gitlab.com/AndrewZaech/aztaskbar) |
113 | [Dash to Dock](https://github.com/micheleg/dash-to-dock) |
114 | [Overview Clicking](https://github.com/mechtifs/overview-clicking) |
115 | [Volume Scroller](https://github.com/trflynn89/gnome-shell-volume-scroller) |
116 | [Fullscreen Hot Corner](https://github.com/soal/gnome-shell-fullscreen-hot-corner) |
117 | [Just Perfection](https://gitlab.gnome.org/jrahmatzadeh/just-perfection)
118 |
--------------------------------------------------------------------------------
/build:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | cd ./extension
4 |
5 | gnome-extensions pack --force\
6 | --podir=locale ./\
7 | --extra-source ../LICENSE\
8 | --extra-source assets/\
9 | --extra-source ui/\
10 | --extra-source services/\
11 | --extra-source utils/\
12 | --extra-source shell/\
13 | --extra-source settings/\
14 | --out-dir ../
--------------------------------------------------------------------------------
/create-translation:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ExtensionName=`cat ./extension/metadata.json | grep -oP '(?<="name": ")[^"]*'`
4 |
5 | cd ./extension/locale
6 |
7 | msginit --locale $1
--------------------------------------------------------------------------------
/extension/assets/icons/notification-symbolic.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/extension/assets/icons/window-symbolic.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/extension/extension.js:
--------------------------------------------------------------------------------
1 | //#region imports
2 |
3 | import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
4 | import { Connections } from './utils/connections.js';
5 | import { ShellTweaks } from './shell/tweaks.js';
6 | import { Taskbar } from './ui/taskbar.js';
7 | import { NotificationCounter } from './ui/notificationCounter.js';
8 | import { IconProvider } from './utils/iconProvider.js';
9 | import { LauncherAPI } from './utils/launcherAPI.js';
10 |
11 | //#endregion imports
12 |
13 | //#region main
14 |
15 | export default class RocketBarExtension extends Extension {
16 | enable() {
17 | // call instance() to initialize dbus interface
18 | // this should be done as soon as possible
19 | // to make apps use the interface correctly
20 | LauncherAPI.instance();
21 |
22 | this._settings = this.getSettings();
23 |
24 | this._iconProvider = new IconProvider(this.path);
25 | this._shellTweaks = new ShellTweaks(this._settings);
26 |
27 | this._handleSettings();
28 |
29 | this._connections = new Connections();
30 | this._connections.addScope(this._settings, [
31 | 'changed::taskbar-enabled',
32 | 'changed::notification-counter-enabled'
33 | ], () => this._handleSettings());
34 | }
35 |
36 | disable() {
37 | // destroy all
38 | this._connections.destroy();
39 | this._taskbar?.destroy();
40 | this._notificationCounter?.destroy();
41 | this._shellTweaks?.destroy();
42 | LauncherAPI.destroy();
43 |
44 | // and nullify all
45 | this._taskbar = null;
46 | this._notificationCounter = null;
47 | this._shellTweaks = null;
48 | this._settings = null;
49 | this._connections = null;
50 | this._iconProvider = null;
51 | }
52 |
53 | _handleSettings() {
54 |
55 | const taskbarEnabled = this._settings.get_boolean('taskbar-enabled');
56 | const notificationCounterEnabled = this._settings.get_boolean('notification-counter-enabled');
57 |
58 | if (taskbarEnabled && !this._taskbar) {
59 | this._taskbar = new Taskbar(this._settings, this._iconProvider);
60 | } else if (!taskbarEnabled && this._taskbar) {
61 | this._taskbar.destroy();
62 | this._taskbar = null;
63 | }
64 |
65 | if (notificationCounterEnabled && !this._notificationCounter) {
66 | this._notificationCounter = new NotificationCounter(this._settings);
67 | } else if (!notificationCounterEnabled && this._notificationCounter) {
68 | this._notificationCounter.destroy();
69 | this._notificationCounter = null;
70 | }
71 |
72 | }
73 | }
74 |
75 |
76 | //#endregion main
--------------------------------------------------------------------------------
/extension/locale/fr.po:
--------------------------------------------------------------------------------
1 | # French translations for PACKAGE package.
2 | # Copyright (C) 2022 THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the PACKAGE package.
4 | # Céleri , 2022.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: PACKAGE VERSION\n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2022-07-21 01:39+0300\n"
11 | "PO-Revision-Date: 2022-07-21 02:42+0200\n"
12 | "Last-Translator: Celeri \n"
13 | "Language-Team: French\n"
14 | "Language: fr\n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=UTF-8\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n"
19 |
20 | #: settings/aboutPage.js:13
21 | msgid "About"
22 | msgstr "À propos"
23 |
24 | #: settings/aboutPage.js:26
25 | msgid " Version"
26 | msgstr " Version"
27 |
28 | #: settings/aboutPage.js:29
29 | msgid "Useful Links"
30 | msgstr "Liens Utiles"
31 |
32 | #: settings/aboutPage.js:30
33 | msgid "Report an issue"
34 | msgstr "Signaler un problème"
35 |
36 | #: settings/aboutPage.js:31
37 | msgid "Share your ideas"
38 | msgstr "Proposer une idée"
39 |
40 | #: settings/aboutPage.js:34
41 | msgid "Credits"
42 | msgstr "Crédits"
43 |
44 | #: settings/behaviorPage.js:14
45 | msgid "Behavior"
46 | msgstr "Comportement"
47 |
48 | #: settings/behaviorPage.js:38 settings/generalPage.js:49
49 | msgid "Overview"
50 | msgstr "Aperçu"
51 |
52 | #: settings/behaviorPage.js:39
53 | msgid "Enable empty space clicks in Overview"
54 | msgstr "Activer les clics sur le vide dans l'Aperçu"
55 |
56 | #: settings/behaviorPage.js:40
57 | msgid ""
58 | "Left button click to close the Overview, Right button click to show the App "
59 | "Grid"
60 | msgstr ""
61 | "Clic Gauche pour fermer l'Aperçu, clic Droit pour ouvrir la grille "
62 | "d'Applications"
63 |
64 | #: settings/behaviorPage.js:44
65 | msgid "Hot Corner"
66 | msgstr "Coin actif"
67 |
68 | #: settings/behaviorPage.js:45
69 | msgid "Enable Fullscreen Hot Corner"
70 | msgstr "Activer le Coin Actif en Plein Écran"
71 |
72 | #: settings/behaviorPage.js:52 ui/appButtonMenu.js:398
73 | msgid "New window"
74 | msgstr "Nouvelle fenêtre"
75 |
76 | #: settings/behaviorPage.js:53 ui/appButtonMenu.js:402
77 | msgid "Move windows"
78 | msgstr "Déplacer la fenêtre"
79 |
80 | #: settings/behaviorPage.js:56 settings/customizePage.js:84
81 | #: settings/generalPage.js:34
82 | msgid "Taskbar"
83 | msgstr "Barre des Tâches"
84 |
85 | #: settings/behaviorPage.js:57
86 | msgid "Enable Drag and Drop"
87 | msgstr "Activer le Glisser-Déposer"
88 |
89 | #: settings/behaviorPage.js:58
90 | msgid "Reorder apps in the taskbar using Drag and Drop"
91 | msgstr "Réorganiser les applications dans la barre des tâches "
92 | "avec le Glisser-Déposer"
93 |
94 | #: settings/behaviorPage.js:59
95 | msgid "Enable Minimize action"
96 | msgstr "Activer l'action de Réduction"
97 |
98 | #: settings/behaviorPage.js:60
99 | msgid "Allow to minimize single app windows by clicking apps in the taskbar"
100 | msgstr "Permet de réduire individuellement les fenêtres des applications "
101 | "en cliquant sur leurs icônes dans la barre des tâches"
102 |
103 | #: settings/behaviorPage.js:61
104 | msgid "Require click to open context menus"
105 | msgstr "Nécessiter le clic pour ouvrir le menu contextuel"
106 |
107 | #: settings/behaviorPage.js:63
108 | msgid "Middle click to toggle app sound mute"
109 | msgstr "Clic Molette pour activer/désactiver le son de l'application"
110 |
111 | #: settings/behaviorPage.js:64
112 | msgid ""
113 | "By default Middle click is used to open new app windows and to close the "
114 | "first app window when Ctrl is pressed"
115 | msgstr "Par défaut, le clic Molette est utilisé pour ouvrir une nouvelle "
116 | "fenêtre de l'application et pour fermer la première fenêtre quand Ctrl est maintenu"
117 |
118 | #: settings/behaviorPage.js:65
119 | msgid "Scroll to change app sound volume"
120 | msgstr "Faire rouler la molette pour changer le volume de l'application"
121 |
122 | #: settings/behaviorPage.js:68
123 | msgid "Scroll to cycle app windows"
124 | msgstr "Utiliser la molette pour défiler entre les fenêtres d'applications"
125 |
126 | #: settings/behaviorPage.js:76
127 | msgid "Running apps activation behavior"
128 | msgstr "Comportement d'activation pour les applications en fonctionnement"
129 |
130 | #: settings/behaviorPage.js:78
131 | msgid ""
132 | "Controls the behavior when an app is running but has no windows on the "
133 | "active workspace, supports isolated workspaces only, can be configured "
134 | "separately for each app via an app menu"
135 | msgstr "Contrôle le comportement quand une application est active mais n'a pas "
136 | "de fenêtre sur l'espace de travail actuel, ne fonctionne qu'avec les espaces "
137 | "de travail isolés, peut être configuré séparément pour chaque application"
138 |
139 | #: settings/behaviorPage.js:85
140 | msgid "Panel"
141 | msgstr "Panneau"
142 |
143 | #: settings/behaviorPage.js:86
144 | msgid "Require click to activate the panel menu buttons"
145 | msgstr "Nécessiter un clic pour activer les boutons de menu du panneau"
146 |
147 | #: settings/behaviorPage.js:87
148 | msgid "Middle click to toggle sound mute"
149 | msgstr "Clic Molette pour activer/désactiver le son"
150 |
151 | #: settings/behaviorPage.js:88
152 | msgid "Press middle button on an empty space of the panel"
153 | msgstr "Cliquer sur la molette à un endroit vide du panneau"
154 |
155 | #: settings/behaviorPage.js:89
156 | msgid "Scroll to change sound volume"
157 | msgstr "Faire rouler la molette pour changer le volume"
158 |
159 | #: settings/behaviorPage.js:96
160 | msgid "Slowest"
161 | msgstr "Très Lent"
162 |
163 | #: settings/behaviorPage.js:97
164 | msgid "Slow"
165 | msgstr "Lent"
166 |
167 | #: settings/behaviorPage.js:98
168 | msgid "Normal"
169 | msgstr "Normal"
170 |
171 | #: settings/behaviorPage.js:99
172 | msgid "Fast"
173 | msgstr "Rapide"
174 |
175 | #: settings/behaviorPage.js:100
176 | msgid "Faster"
177 | msgstr "Très Rapide"
178 |
179 | #: settings/behaviorPage.js:101
180 | msgid "Turbo"
181 | msgstr "Turbo"
182 |
183 | #: settings/behaviorPage.js:104 ui/appButtonMenu.js:279
184 | msgid "Sound Volume Control"
185 | msgstr "Contrôle du volume sonore"
186 |
187 | #: settings/behaviorPage.js:106
188 | msgid "Volume change speed"
189 | msgstr "Vitesse de changement du volume"
190 |
191 | #: settings/behaviorPage.js:110
192 | msgid "Volume change speed when Ctrl pressed"
193 | msgstr "Vitesse de changement du volume quand Ctrl est maintenu"
194 |
195 | #: settings/behaviorPage.js:128
196 | msgid "None"
197 | msgstr "Aucun"
198 |
199 | #: settings/behaviorPage.js:129
200 | msgid "Left Button"
201 | msgstr "Clic Gauche"
202 |
203 | #: settings/behaviorPage.js:130
204 | msgid "Right Button"
205 | msgstr "Clic Droit"
206 |
207 | #: settings/behaviorPage.js:131
208 | msgid "Middle Button"
209 | msgstr "Clic Molette"
210 |
211 | #: settings/behaviorPage.js:134
212 | msgid "Activities"
213 | msgstr "Activités"
214 |
215 | #: settings/behaviorPage.js:136
216 | msgid "Click Activities to show the App Grid"
217 | msgstr "Cliquer sur Activités pour ouvrir la grille des Applications"
218 |
219 | #: settings/customizePage.js:14 ui/appButtonMenu.js:299
220 | msgid "Customize"
221 | msgstr "Personnaliser"
222 |
223 | #: settings/customizePage.js:23
224 | msgid "No customizations available"
225 | msgstr "Pas de personnalisation disponible"
226 |
227 | #: settings/customizePage.js:79
228 | msgid "Left"
229 | msgstr "Gauche"
230 |
231 | #: settings/customizePage.js:80
232 | msgid "Center"
233 | msgstr "Centre"
234 |
235 | #: settings/customizePage.js:81
236 | msgid "Right"
237 | msgstr "Droite"
238 |
239 | #: settings/customizePage.js:86 settings/customizePage.js:148
240 | #: settings/customizePage.js:176
241 | msgid "Position"
242 | msgstr "Position"
243 |
244 | #: settings/customizePage.js:90
245 | msgid "Position Offset"
246 | msgstr "Décalage de la Position"
247 |
248 | #: settings/customizePage.js:93
249 | msgid "Preserve Position"
250 | msgstr "Conserver la Position"
251 |
252 | #: settings/customizePage.js:94
253 | msgid "Prevent position changes caused by other extensions in the panel"
254 | msgstr "Empecher les autres extensions de changer la position dans le panneau"
255 |
256 | #: settings/customizePage.js:99
257 | msgid "App Buttons"
258 | msgstr "Boutons des Applications"
259 |
260 | #: settings/customizePage.js:101 ui/appButtonMenu.js:434
261 | msgid "Icon Size"
262 | msgstr "Taille des Icônes"
263 |
264 | #: settings/customizePage.js:103
265 | msgid "Can be configured separately for each app via an app menu"
266 | msgstr "Peut être configuré individuellement pour chaque "
267 | "application via un menu d'application"
268 |
269 | #: settings/customizePage.js:106
270 | msgid "Icon Padding"
271 | msgstr "Marge Intérieure des Icônes"
272 |
273 | #: settings/customizePage.js:110
274 | msgid "Vertical Margin"
275 | msgstr "Marge Extérieure Verticale"
276 |
277 | #: settings/customizePage.js:114
278 | msgid "Roundness"
279 | msgstr "Rondeur"
280 |
281 | #: settings/customizePage.js:118
282 | msgid "Spacing"
283 | msgstr "Espacement"
284 |
285 | #: settings/customizePage.js:121
286 | msgid "Dominant Color Backlight"
287 | msgstr "Rétroéclairage avec la Couleur Dominante"
288 |
289 | #: settings/customizePage.js:124
290 | msgid "Backlight Intensity"
291 | msgstr "Intensité du Rétroéclairage"
292 |
293 | #: settings/customizePage.js:134
294 | msgid "Top"
295 | msgstr "En Haut"
296 |
297 | #: settings/customizePage.js:135
298 | msgid "Bottom"
299 | msgstr "En Bas"
300 |
301 | #: settings/customizePage.js:138
302 | msgid "Indicators"
303 | msgstr "Indicateurs"
304 |
305 | #: settings/customizePage.js:139
306 | msgid "Active Dominant Color"
307 | msgstr "Couleur Dominante Active"
308 |
309 | #: settings/customizePage.js:141
310 | msgid "Active Color"
311 | msgstr "Couleur Active"
312 |
313 | #: settings/customizePage.js:143
314 | msgid "Inactive Dominant Color"
315 | msgstr "Couleur Dominante Inactive"
316 |
317 | #: settings/customizePage.js:145
318 | msgid "Inactive Color"
319 | msgstr "Couleur Inactive"
320 |
321 | #: settings/customizePage.js:152 settings/customizePage.js:180
322 | msgid "Size"
323 | msgstr "Taille"
324 |
325 | #: settings/customizePage.js:156
326 | msgid "Limit"
327 | msgstr "Limite"
328 |
329 | #: settings/customizePage.js:158
330 | msgid "The maximum number of indicators to display on top of app buttons"
331 | msgstr "Nombre maximum d'indicateurs à afficher sur un bouton d'application"
332 |
333 | #: settings/customizePage.js:166
334 | msgid "Top Left"
335 | msgstr "En Haut à Gauche"
336 |
337 | #: settings/customizePage.js:167
338 | msgid "Top Right"
339 | msgstr "En Haut à Droite"
340 |
341 | #: settings/customizePage.js:168
342 | msgid "Bottom Left"
343 | msgstr "En Bas à Gauche"
344 |
345 | #: settings/customizePage.js:169
346 | msgid "Bottom Right"
347 | msgstr "En Bas à Droite"
348 |
349 | #: settings/customizePage.js:172
350 | msgid "Notification Badges"
351 | msgstr "Badges de Notification"
352 |
353 | #: settings/customizePage.js:173
354 | msgid "Color"
355 | msgstr "Couleur"
356 |
357 | #: settings/customizePage.js:174
358 | msgid "Border Color"
359 | msgstr "Couleur de la Bordure"
360 |
361 | #: settings/customizePage.js:184
362 | msgid "Margin"
363 | msgstr "Marge"
364 |
365 | #: settings/customizePage.js:191
366 | msgid "Tooltips"
367 | msgstr "Info-bulles"
368 |
369 | #: settings/customizePage.js:193
370 | msgid "Show Delay"
371 | msgstr "Délai d'Affichage"
372 |
373 | #: settings/generalPage.js:14
374 | msgid "General"
375 | msgstr "Général"
376 |
377 | #: settings/generalPage.js:35
378 | msgid "Enabled"
379 | msgstr "Activé"
380 |
381 | #: settings/generalPage.js:37
382 | msgid "Show Favorites"
383 | msgstr "Afficher les Favoris"
384 |
385 | #: settings/generalPage.js:38
386 | msgid "Isolate Workspaces"
387 | msgstr "Isoler les Espaces de Travail"
388 |
389 | #: settings/generalPage.js:39
390 | msgid "Enable Indicators"
391 | msgstr "Activer les Indicateurs"
392 |
393 | #: settings/generalPage.js:40
394 | msgid "Enable Notification Badges"
395 | msgstr "Activer les Badges de Notification"
396 |
397 | #: settings/generalPage.js:41
398 | msgid "Enable Tooltips"
399 | msgstr "Activer les Info-bulles"
400 |
401 | #: settings/generalPage.js:42
402 | msgid "Enable Sound Volume Control"
403 | msgstr "Activer le Contrôle du Volume Sonore"
404 |
405 | #: settings/generalPage.js:43
406 | msgid "Experimental feature"
407 | msgstr "Fonctionnalités expérimentales"
408 |
409 | #: settings/generalPage.js:50
410 | msgid "Kill the Dash"
411 | msgstr "Tuer le Dash"
412 |
413 | #: settings/generalPage.js:51
414 | msgid ""
415 | "Hide the Dash from Overview and prevent it from rerendering behind the scene"
416 | msgstr "Cache le Dash de l'Aperçu et l'empêche de faire son rendu en arrière-plan"
417 |
418 | #: ui/appButtonMenu.js:179
419 | msgid "Pin"
420 | msgstr "Épingler au Dash"
421 |
422 | #: ui/appButtonMenu.js:395
423 | msgid "Activation Behavior"
424 | msgstr "Comportement d'Activation"
425 |
426 | #: ui/appButtonMenu.js:412
427 | msgid "Icon"
428 | msgstr "Icône"
429 |
430 | #: ui/appButtonMenu.js:415
431 | msgid "Import"
432 | msgstr "Importer"
433 |
434 | #: ui/appButtonMenu.js:426
435 | msgid "Reset to default"
436 | msgstr "Réinitialiser"
437 |
438 | #: ui/appButtonMenu.js:440
439 | msgid "Reset all to default"
440 | msgstr "Tout Réinitialiser"
441 |
--------------------------------------------------------------------------------
/extension/locale/nl.po:
--------------------------------------------------------------------------------
1 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
2 | # This file is distributed under the same license as the PACKAGE package.
3 | #
4 | # Heimen Stoffels , 2022.
5 | msgid ""
6 | msgstr ""
7 | "Project-Id-Version: \n"
8 | "Report-Msgid-Bugs-To: \n"
9 | "POT-Creation-Date: 2023-03-25 23:42+0300\n"
10 | "PO-Revision-Date: 2023-03-31 11:45+0200\n"
11 | "Last-Translator: Heimen Stoffels \n"
12 | "Language-Team: Dutch\n"
13 | "Language: nl\n"
14 | "MIME-Version: 1.0\n"
15 | "Content-Type: text/plain; charset=UTF-8\n"
16 | "Content-Transfer-Encoding: 8bit\n"
17 | "Plural-Forms: nplurals=2; plural=(n != 1);\n"
18 | "X-Generator: Poedit 3.2.2\n"
19 |
20 | #: settings/aboutPage.js:13
21 | msgid "About"
22 | msgstr "Over"
23 |
24 | #: settings/aboutPage.js:26
25 | msgid " Version"
26 | msgstr " Versie"
27 |
28 | #: settings/aboutPage.js:27
29 | msgid "Release Notes"
30 | msgstr "Wijzigingslog"
31 |
32 | #: settings/aboutPage.js:30
33 | msgid "Useful Links"
34 | msgstr "Handige links"
35 |
36 | #: settings/aboutPage.js:31
37 | msgid "Report an issue"
38 | msgstr "Probleem melden"
39 |
40 | #: settings/aboutPage.js:32
41 | msgid "Share your ideas"
42 | msgstr "Ideeën delen"
43 |
44 | #: settings/aboutPage.js:35
45 | msgid "Credits"
46 | msgstr "Met dank aan"
47 |
48 | #: settings/behaviorPage.js:14
49 | msgid "Behavior"
50 | msgstr "Gedrag"
51 |
52 | #: settings/behaviorPage.js:29
53 | msgid "Notification Service"
54 | msgstr "Meldingsdienst"
55 |
56 | #: settings/behaviorPage.js:30
57 | msgid "Enable Unity Launcher API support"
58 | msgstr "Unity-starterondersteuning inschakelen"
59 |
60 | #: settings/behaviorPage.js:31
61 | msgid "Use Unity Launcher API DBus interface to count notifications for apps"
62 | msgstr ""
63 | "Gebruik de Unity dbus-api om het aantal meldingen van toepassingen bij te "
64 | "houden"
65 |
66 | #: settings/behaviorPage.js:32
67 | msgid "Count Window Demands Attention notifications for apps"
68 | msgstr "Voor venstertelling dienen meldingen te worden getoond"
69 |
70 | #: settings/behaviorPage.js:45 settings/generalPage.js:54
71 | msgid "Overview"
72 | msgstr "Overzicht"
73 |
74 | #: settings/behaviorPage.js:46
75 | msgid "Enable empty space clicks in Overview"
76 | msgstr "Klikken op lege ruimtes toestaan op overzicht"
77 |
78 | #: settings/behaviorPage.js:47
79 | msgid ""
80 | "Left button click to close the Overview, Right button click to show the App "
81 | "Grid"
82 | msgstr ""
83 | "Klik met de linkermuiskop om het overzicht te sluiten en met de "
84 | "rechtermuisknop om alle toepassingen te tonen"
85 |
86 | #: settings/behaviorPage.js:51
87 | msgid "Hot Corner"
88 | msgstr "Snelhoek"
89 |
90 | #: settings/behaviorPage.js:52
91 | msgid "Enable Fullscreen Hot Corner"
92 | msgstr "Snelhoek toestaan in schermvullende weergave"
93 |
94 | #: settings/behaviorPage.js:56
95 | msgid "Lock Screen"
96 | msgstr "Vergrendelscherm"
97 |
98 | #: settings/behaviorPage.js:57
99 | msgid "Force primary input source on Lock Screen"
100 | msgstr "Gebruik van primaire invoerbron afdwingen op vergrendelscherm"
101 |
102 | #: settings/behaviorPage.js:58 settings/generalPage.js:48
103 | msgid "Experimental feature"
104 | msgstr "Experimentele functie"
105 |
106 | #: settings/behaviorPage.js:68 ui/appButtonMenu.js:418
107 | msgid "New window"
108 | msgstr "Nieuw venster"
109 |
110 | #: settings/behaviorPage.js:69 ui/appButtonMenu.js:422
111 | msgid "Move windows"
112 | msgstr "Vensters verplaatsen"
113 |
114 | #: settings/behaviorPage.js:72 settings/customizePage.js:93
115 | #: settings/generalPage.js:39
116 | msgid "Taskbar"
117 | msgstr "Taakbalk"
118 |
119 | #: settings/behaviorPage.js:73
120 | msgid "Enable Drag and Drop"
121 | msgstr "Slepen-en-neerzetten inschakelen"
122 |
123 | #: settings/behaviorPage.js:74
124 | msgid "Reorder apps in the taskbar using Drag and Drop"
125 | msgstr "Orden toepassingen op de taakbalk middels slepen-en-neerzetten"
126 |
127 | #: settings/behaviorPage.js:75
128 | msgid "Enable Minimize action"
129 | msgstr "Minimaliseeractie inschakelen"
130 |
131 | #: settings/behaviorPage.js:76
132 | msgid "Allow to minimize single app windows by clicking apps in the taskbar"
133 | msgstr ""
134 | "Minimaliseer toepassingsvensters door de toepassing in kwestie aan te "
135 | "klikken op de taakbalk"
136 |
137 | #: settings/behaviorPage.js:77
138 | msgid "Require click to open context menus"
139 | msgstr "Vereist klik-om-te-openenmenu's"
140 |
141 | #: settings/behaviorPage.js:79
142 | msgid "Middle click to toggle app sound mute"
143 | msgstr "Middelklikken geluid te dempen/ontdempen"
144 |
145 | #: settings/behaviorPage.js:80
146 | msgid ""
147 | "By default Middle click is used to open new app windows and to close the "
148 | "first app window when Ctrl is pressed"
149 | msgstr ""
150 | "Standaard wordt de middelste muisknop gebruikt om nieuwe toepassingsvensters "
151 | "te openen en, met behulp van Ctrl, het eerste venster te sluiten"
152 |
153 | #: settings/behaviorPage.js:81
154 | msgid "Scroll to change app sound volume"
155 | msgstr "Scrollen om volume te wijzigen"
156 |
157 | #: settings/behaviorPage.js:84
158 | msgid "Scroll to cycle app windows"
159 | msgstr "Scrollen om van venster te wisselen"
160 |
161 | #: settings/behaviorPage.js:92
162 | msgid "Running apps activation behavior"
163 | msgstr "Opengedrag van actieve toepassingen"
164 |
165 | #: settings/behaviorPage.js:94
166 | msgid ""
167 | "Controls the behavior when an app is running but has no windows on the "
168 | "active workspace, supports isolated workspaces only, can be configured "
169 | "separately for each app via an app menu"
170 | msgstr ""
171 | "Bepaal het gedrag van een toepassing die actief is, maar geen openstaande "
172 | "vensters op het actieve werkblad heeft. Let op: er is alléén ondersteuning "
173 | "voor geïsoleerde werkbladen."
174 |
175 | #: settings/behaviorPage.js:103 settings/behaviorPage.js:151
176 | msgid "None"
177 | msgstr "Geen"
178 |
179 | #: settings/behaviorPage.js:104
180 | msgid "Change Sound Volume"
181 | msgstr "Volume wijzigen"
182 |
183 | #: settings/behaviorPage.js:105
184 | msgid "Switch Workspace"
185 | msgstr "Naar ander werkblad gaan"
186 |
187 | #: settings/behaviorPage.js:108
188 | msgid "Panel"
189 | msgstr "Bovenbalk"
190 |
191 | #: settings/behaviorPage.js:109
192 | msgid "Require click to activate menu buttons"
193 | msgstr "Klikken om menuknoppen te activeren"
194 |
195 | #: settings/behaviorPage.js:110
196 | msgid "Middle click to toggle sound mute"
197 | msgstr "Middelklikken geluid te dempen/ontdempen"
198 |
199 | #: settings/behaviorPage.js:111
200 | msgid "Press middle button on an empty space of the panel"
201 | msgstr "Druk met de middelste muisknop op een lege ruimte op de bovenbalk"
202 |
203 | #: settings/behaviorPage.js:112
204 | msgid "Scroll action"
205 | msgstr "Scrolactie"
206 |
207 | #: settings/behaviorPage.js:119
208 | msgid "Slowest"
209 | msgstr "Traagst"
210 |
211 | #: settings/behaviorPage.js:120
212 | msgid "Slow"
213 | msgstr "Traag"
214 |
215 | #: settings/behaviorPage.js:121
216 | msgid "Normal"
217 | msgstr "Normaal"
218 |
219 | #: settings/behaviorPage.js:122
220 | msgid "Fast"
221 | msgstr "Snel"
222 |
223 | #: settings/behaviorPage.js:123
224 | msgid "Faster"
225 | msgstr "Sneller"
226 |
227 | #: settings/behaviorPage.js:124
228 | msgid "Turbo"
229 | msgstr "Standje turbo"
230 |
231 | #: settings/behaviorPage.js:127 ui/appButtonMenu.js:299
232 | msgid "Sound Volume Control"
233 | msgstr "Volumeregeling"
234 |
235 | #: settings/behaviorPage.js:129
236 | msgid "Volume change speed"
237 | msgstr "Wijzigingssnelheid"
238 |
239 | #: settings/behaviorPage.js:133
240 | msgid "Volume change speed when Ctrl pressed"
241 | msgstr "De te wijzigen snelheid als Ctrl ingedrukt wordt gehouden"
242 |
243 | #: settings/behaviorPage.js:152
244 | msgid "Left Button"
245 | msgstr "Linkermuisknop"
246 |
247 | #: settings/behaviorPage.js:153
248 | msgid "Right Button"
249 | msgstr "Rechtermuisknop"
250 |
251 | #: settings/behaviorPage.js:154
252 | msgid "Middle Button"
253 | msgstr "Middelste muisknop"
254 |
255 | #: settings/behaviorPage.js:157
256 | msgid "Activities"
257 | msgstr "Activiteiten"
258 |
259 | #: settings/behaviorPage.js:159
260 | msgid "Click Activities to show the App Grid"
261 | msgstr "Klikken op activiteitenknop om alle toepassingen te tonen"
262 |
263 | #: settings/behaviorPage.js:166
264 | msgid "Switcher Popups"
265 | msgstr "Taakschakelaarpop-ups"
266 |
267 | #: settings/behaviorPage.js:167
268 | msgid "Override show delay"
269 | msgstr "Pas de wachttijd alvorens te tonen aan"
270 |
271 | #: settings/behaviorPage.js:170 settings/customizePage.js:238
272 | msgid "Show Delay"
273 | msgstr "Wachttijd alvorens te tonen"
274 |
275 | #: settings/behaviorPage.js:174
276 | msgid "Do not grab focus"
277 | msgstr "Focus niet verleggen"
278 |
279 | #: settings/customizePage.js:14 ui/appButtonMenu.js:319
280 | msgid "Customize"
281 | msgstr "Aanpassen"
282 |
283 | #: settings/customizePage.js:23
284 | msgid "No customizations available"
285 | msgstr "Er zijn geen aanpassingen beschikbaar"
286 |
287 | #: settings/customizePage.js:88
288 | msgid "Left"
289 | msgstr "Links"
290 |
291 | #: settings/customizePage.js:89
292 | msgid "Center"
293 | msgstr "Midden"
294 |
295 | #: settings/customizePage.js:90
296 | msgid "Right"
297 | msgstr "Rechts"
298 |
299 | #: settings/customizePage.js:95 settings/customizePage.js:154
300 | #: settings/customizePage.js:221
301 | msgid "Position"
302 | msgstr "Locatie"
303 |
304 | #: settings/customizePage.js:99
305 | msgid "Position Offset"
306 | msgstr "Locatieverschuiving"
307 |
308 | #: settings/customizePage.js:102
309 | msgid "Preserve Position"
310 | msgstr "Locatie vergrendelen"
311 |
312 | #: settings/customizePage.js:103
313 | msgid "Prevent position changes caused by other extensions in the panel"
314 | msgstr ""
315 | "Voorkomt dat de locatie gewijzigd wordt door andere uitbreidingen op de "
316 | "bovenbalk"
317 |
318 | #: settings/customizePage.js:109
319 | msgid "Backlight Color"
320 | msgstr "Achtergrondkleur"
321 |
322 | #: settings/customizePage.js:112
323 | msgid "App Buttons"
324 | msgstr "Toepassingsknoppen"
325 |
326 | #: settings/customizePage.js:114 ui/appButtonMenu.js:454
327 | msgid "Icon Size"
328 | msgstr "Pictogramgrootte"
329 |
330 | #: settings/customizePage.js:116
331 | msgid "Can be configured separately for each app via an app menu"
332 | msgstr "Dit kan per toepassing via het toepassingsmenu worden ingesteld"
333 |
334 | #: settings/customizePage.js:119
335 | msgid "Icon Horizontal Padding"
336 | msgstr "Horizontale pictogramopvulling"
337 |
338 | #: settings/customizePage.js:123
339 | msgid "Icon Vertical Padding"
340 | msgstr "Verticale pictogramopvulling"
341 |
342 | #: settings/customizePage.js:127 settings/customizePage.js:261
343 | msgid "Roundness"
344 | msgstr "Afronding"
345 |
346 | #: settings/customizePage.js:131
347 | msgid "Spacing"
348 | msgstr "Uitlijning"
349 |
350 | #: settings/customizePage.js:134
351 | msgid "Dominant Color Backlight"
352 | msgstr "Dominante achtergrondkleur"
353 |
354 | #: settings/customizePage.js:139
355 | msgid "Backlight Intensity"
356 | msgstr "Achtergrondsterkte"
357 |
358 | #: settings/customizePage.js:148
359 | msgid "Top"
360 | msgstr "Bovenaan"
361 |
362 | #: settings/customizePage.js:149
363 | msgid "Bottom"
364 | msgstr "Onderaan"
365 |
366 | #: settings/customizePage.js:152
367 | msgid "Indicators"
368 | msgstr "Indicators"
369 |
370 | #: settings/customizePage.js:158
371 | msgid "Limit"
372 | msgstr "Limiet"
373 |
374 | #: settings/customizePage.js:160
375 | msgid "The maximum number of indicators to display on top of app buttons"
376 | msgstr ""
377 | "Het maximumaantal indicators dat op toepassingsknoppen mag worden getoond"
378 |
379 | #: settings/customizePage.js:162
380 | msgid "Active Dominant Color"
381 | msgstr "Actieve dominante kleur"
382 |
383 | #: settings/customizePage.js:164
384 | msgid "Active Color"
385 | msgstr "Actieve kleur"
386 |
387 | #: settings/customizePage.js:166
388 | msgid "Inactive Dominant Color"
389 | msgstr "Inactieve dominante kleur"
390 |
391 | #: settings/customizePage.js:168
392 | msgid "Inactive Color"
393 | msgstr "Inactieve kleur"
394 |
395 | #: settings/customizePage.js:172
396 | msgid "Inactive Width"
397 | msgstr "Inactieve breedte"
398 |
399 | #: settings/customizePage.js:176
400 | msgid "Active Width"
401 | msgstr "Actieve breedte"
402 |
403 | #: settings/customizePage.js:180
404 | msgid "Inactive Height"
405 | msgstr "Inactieve hoogte"
406 |
407 | #: settings/customizePage.js:184
408 | msgid "Active Height"
409 | msgstr "Actieve hoogte"
410 |
411 | #: settings/customizePage.js:188
412 | msgid "Inactive Roundness"
413 | msgstr "Inactieve afronding"
414 |
415 | #: settings/customizePage.js:192
416 | msgid "Active Roundness"
417 | msgstr "Actieve afronding"
418 |
419 | #: settings/customizePage.js:197
420 | msgid "Inactive Spacing"
421 | msgstr "Inactieve uitlijning"
422 |
423 | #: settings/customizePage.js:201
424 | msgid "Active Spacing"
425 | msgstr "Actieve uitlijning"
426 |
427 | #: settings/customizePage.js:211
428 | msgid "Top Left"
429 | msgstr "Linksboven"
430 |
431 | #: settings/customizePage.js:212
432 | msgid "Top Right"
433 | msgstr "Rechtsboven"
434 |
435 | #: settings/customizePage.js:213
436 | msgid "Bottom Left"
437 | msgstr "Linksonder"
438 |
439 | #: settings/customizePage.js:214
440 | msgid "Bottom Right"
441 | msgstr "Rechtsonder"
442 |
443 | #: settings/customizePage.js:217
444 | msgid "Notification Badges"
445 | msgstr "Meldingsemblemen"
446 |
447 | #: settings/customizePage.js:218
448 | msgid "Color"
449 | msgstr "Kleur"
450 |
451 | #: settings/customizePage.js:219
452 | msgid "Border Color"
453 | msgstr "Randkleur"
454 |
455 | #: settings/customizePage.js:225
456 | msgid "Size"
457 | msgstr "Grootte"
458 |
459 | #: settings/customizePage.js:229
460 | msgid "Margin"
461 | msgstr "Marge"
462 |
463 | #: settings/customizePage.js:236
464 | msgid "Tooltips"
465 | msgstr "Tekstballonnen"
466 |
467 | #: settings/customizePage.js:242
468 | msgid "Max Width"
469 | msgstr "Max. breedte"
470 |
471 | #: settings/customizePage.js:249 settings/generalPage.js:29
472 | msgid "Notification Counter"
473 | msgstr "Meldingsteller"
474 |
475 | #: settings/customizePage.js:250
476 | msgid "Hide when empty"
477 | msgstr "Verbergen indien geen meldingen"
478 |
479 | #: settings/customizePage.js:251
480 | msgid "Center clock"
481 | msgstr "Klok centreren"
482 |
483 | #: settings/customizePage.js:253
484 | msgid "Max Count"
485 | msgstr "Maximumaantal"
486 |
487 | #: settings/customizePage.js:257
488 | msgid "Font Size"
489 | msgstr "Tekstgrootte"
490 |
491 | #: settings/customizePage.js:265
492 | msgid "Top Margin"
493 | msgstr "Bovenste marge"
494 |
495 | #: settings/customizePage.js:269
496 | msgid "Empty Color"
497 | msgstr "Ongekleurd"
498 |
499 | #: settings/customizePage.js:271
500 | msgid "Not Empty Color"
501 | msgstr "Meldingskleur"
502 |
503 | #: settings/customizePage.js:272
504 | msgid "Text Color"
505 | msgstr "Tekstkleur"
506 |
507 | #: settings/customizePage.js:274
508 | msgid "Do Not Disturb - Empty Color"
509 | msgstr "Niet storen - Ongekleurd"
510 |
511 | #: settings/customizePage.js:276
512 | msgid "Do Not Disturb - Not Empty Color"
513 | msgstr "Niet storen - Meldingskleur"
514 |
515 | #: settings/customizePage.js:277
516 | msgid "Do Not Disturb - Text Color"
517 | msgstr "Niet storen - Tekstkleur"
518 |
519 | #: settings/generalPage.js:14
520 | msgid "General"
521 | msgstr "Algemeen"
522 |
523 | #: settings/generalPage.js:30 settings/generalPage.js:40
524 | msgid "Enabled"
525 | msgstr "Ingeschakeld"
526 |
527 | #: settings/generalPage.js:42
528 | msgid "Show Favorites"
529 | msgstr "Favorieten tonen"
530 |
531 | #: settings/generalPage.js:43
532 | msgid "Isolate Workspaces"
533 | msgstr "Werkbladen isoleren"
534 |
535 | #: settings/generalPage.js:44
536 | msgid "Enable Indicators"
537 | msgstr "Indicators tonen"
538 |
539 | #: settings/generalPage.js:45
540 | msgid "Enable Notification Badges"
541 | msgstr " Meldingsemblemen tonen"
542 |
543 | #: settings/generalPage.js:46
544 | msgid "Enable Tooltips"
545 | msgstr "Tekstballonnen tonen"
546 |
547 | #: settings/generalPage.js:47
548 | msgid "Enable Sound Volume Control"
549 | msgstr "Volumeregeling gebruiken"
550 |
551 | #: settings/generalPage.js:55
552 | msgid "Kill the Dash"
553 | msgstr "Geen paneel gebruiken"
554 |
555 | #: settings/generalPage.js:56
556 | msgid ""
557 | "Hide the Dash from Overview and prevent it from rerendering behind the scene"
558 | msgstr "Verbergt het toepassingenpaneel op het overzicht"
559 |
560 | #: ui/appButtonMenu.js:190
561 | msgid "Pin"
562 | msgstr "Vastmaken"
563 |
564 | #: ui/appButtonMenu.js:415
565 | msgid "Activation Behavior"
566 | msgstr "Opengedrag"
567 |
568 | #: ui/appButtonMenu.js:432
569 | msgid "Icon"
570 | msgstr "Pictogram"
571 |
572 | #: ui/appButtonMenu.js:435
573 | msgid "Import"
574 | msgstr "Importeren"
575 |
576 | #: ui/appButtonMenu.js:446
577 | msgid "Reset to default"
578 | msgstr "Standaardwaarden herstellen"
579 |
580 | #: ui/appButtonMenu.js:460
581 | msgid "Reset all to default"
582 | msgstr "Herstel alle standaardwaarden"
583 |
--------------------------------------------------------------------------------
/extension/locale/rocketbar.pot:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the PACKAGE package.
4 | # FIRST AUTHOR , YEAR.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: PACKAGE VERSION\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2023-11-12 22:57+0300\n"
12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13 | "Last-Translator: FULL NAME \n"
14 | "Language-Team: LANGUAGE \n"
15 | "Language: \n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=CHARSET\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 |
20 | #: settings/aboutPage.js:11
21 | msgid "About"
22 | msgstr ""
23 |
24 | #: settings/aboutPage.js:24
25 | msgid " Version"
26 | msgstr ""
27 |
28 | #: settings/aboutPage.js:25
29 | msgid "Release Notes"
30 | msgstr ""
31 |
32 | #: settings/aboutPage.js:28
33 | msgid "Useful Links"
34 | msgstr ""
35 |
36 | #: settings/aboutPage.js:29
37 | msgid "Report an issue"
38 | msgstr ""
39 |
40 | #: settings/aboutPage.js:30
41 | msgid "Share your ideas"
42 | msgstr ""
43 |
44 | #: settings/aboutPage.js:33
45 | msgid "Credits"
46 | msgstr ""
47 |
48 | #: settings/behaviorPage.js:12
49 | msgid "Behavior"
50 | msgstr ""
51 |
52 | #: settings/behaviorPage.js:27
53 | msgid "Notification Service"
54 | msgstr ""
55 |
56 | #: settings/behaviorPage.js:28
57 | msgid "Enable Unity Launcher API support"
58 | msgstr ""
59 |
60 | #: settings/behaviorPage.js:29
61 | msgid "Use Unity Launcher API DBus interface to count notifications for apps"
62 | msgstr ""
63 |
64 | #: settings/behaviorPage.js:30
65 | msgid "Count Window Demands Attention notifications for apps"
66 | msgstr ""
67 |
68 | #: settings/behaviorPage.js:43 settings/generalPage.js:52
69 | msgid "Overview"
70 | msgstr ""
71 |
72 | #: settings/behaviorPage.js:44
73 | msgid "Enable empty space clicks in Overview"
74 | msgstr ""
75 |
76 | #: settings/behaviorPage.js:45
77 | msgid ""
78 | "Left button click to close the Overview, Right button click to show the App "
79 | "Grid"
80 | msgstr ""
81 |
82 | #: settings/behaviorPage.js:49
83 | msgid "Hot Corner"
84 | msgstr ""
85 |
86 | #: settings/behaviorPage.js:50
87 | msgid "Enable Fullscreen Hot Corner"
88 | msgstr ""
89 |
90 | #: settings/behaviorPage.js:54
91 | msgid "Lock Screen"
92 | msgstr ""
93 |
94 | #: settings/behaviorPage.js:55
95 | msgid "Force primary input source on Lock Screen"
96 | msgstr ""
97 |
98 | #: settings/behaviorPage.js:56 settings/generalPage.js:46
99 | msgid "Experimental feature"
100 | msgstr ""
101 |
102 | #: settings/behaviorPage.js:66 ui/appButtonMenu.js:417
103 | msgid "New window"
104 | msgstr ""
105 |
106 | #: settings/behaviorPage.js:67 ui/appButtonMenu.js:421
107 | msgid "Move windows"
108 | msgstr ""
109 |
110 | #: settings/behaviorPage.js:70 settings/customizePage.js:91
111 | #: settings/generalPage.js:37
112 | msgid "Taskbar"
113 | msgstr ""
114 |
115 | #: settings/behaviorPage.js:71
116 | msgid "Enable Drag and Drop"
117 | msgstr ""
118 |
119 | #: settings/behaviorPage.js:72
120 | msgid "Reorder apps in the taskbar using Drag and Drop"
121 | msgstr ""
122 |
123 | #: settings/behaviorPage.js:73
124 | msgid "Enable Minimize action"
125 | msgstr ""
126 |
127 | #: settings/behaviorPage.js:74
128 | msgid "Allow to minimize single app windows by clicking apps in the taskbar"
129 | msgstr ""
130 |
131 | #: settings/behaviorPage.js:75
132 | msgid "Require click to open context menus"
133 | msgstr ""
134 |
135 | #: settings/behaviorPage.js:77
136 | msgid "Middle click to toggle app sound mute"
137 | msgstr ""
138 |
139 | #: settings/behaviorPage.js:78
140 | msgid ""
141 | "By default Middle click is used to open new app windows and to close the "
142 | "first app window when Ctrl is pressed"
143 | msgstr ""
144 |
145 | #: settings/behaviorPage.js:79
146 | msgid "Scroll to change app sound volume"
147 | msgstr ""
148 |
149 | #: settings/behaviorPage.js:82
150 | msgid "Scroll to cycle app windows"
151 | msgstr ""
152 |
153 | #: settings/behaviorPage.js:90
154 | msgid "Running apps activation behavior"
155 | msgstr ""
156 |
157 | #: settings/behaviorPage.js:92
158 | msgid ""
159 | "Controls the behavior when an app is running but has no windows on the "
160 | "active workspace, supports isolated workspaces only, can be configured "
161 | "separately for each app via an app menu"
162 | msgstr ""
163 |
164 | #: settings/behaviorPage.js:101 settings/behaviorPage.js:149
165 | msgid "None"
166 | msgstr ""
167 |
168 | #: settings/behaviorPage.js:102
169 | msgid "Change Sound Volume"
170 | msgstr ""
171 |
172 | #: settings/behaviorPage.js:103
173 | msgid "Switch Workspace"
174 | msgstr ""
175 |
176 | #: settings/behaviorPage.js:106
177 | msgid "Panel"
178 | msgstr ""
179 |
180 | #: settings/behaviorPage.js:107
181 | msgid "Require click to activate menu buttons"
182 | msgstr ""
183 |
184 | #: settings/behaviorPage.js:108
185 | msgid "Middle click to toggle sound mute"
186 | msgstr ""
187 |
188 | #: settings/behaviorPage.js:109
189 | msgid "Press middle button on an empty space of the panel"
190 | msgstr ""
191 |
192 | #: settings/behaviorPage.js:110
193 | msgid "Scroll action"
194 | msgstr ""
195 |
196 | #: settings/behaviorPage.js:117
197 | msgid "Slowest"
198 | msgstr ""
199 |
200 | #: settings/behaviorPage.js:118
201 | msgid "Slow"
202 | msgstr ""
203 |
204 | #: settings/behaviorPage.js:119
205 | msgid "Normal"
206 | msgstr ""
207 |
208 | #: settings/behaviorPage.js:120
209 | msgid "Fast"
210 | msgstr ""
211 |
212 | #: settings/behaviorPage.js:121
213 | msgid "Faster"
214 | msgstr ""
215 |
216 | #: settings/behaviorPage.js:122
217 | msgid "Turbo"
218 | msgstr ""
219 |
220 | #: settings/behaviorPage.js:125 ui/appButtonMenu.js:298
221 | msgid "Sound Volume Control"
222 | msgstr ""
223 |
224 | #: settings/behaviorPage.js:127
225 | msgid "Volume change speed"
226 | msgstr ""
227 |
228 | #: settings/behaviorPage.js:131
229 | msgid "Volume change speed when Ctrl pressed"
230 | msgstr ""
231 |
232 | #: settings/behaviorPage.js:150
233 | msgid "Left Button"
234 | msgstr ""
235 |
236 | #: settings/behaviorPage.js:151
237 | msgid "Right Button"
238 | msgstr ""
239 |
240 | #: settings/behaviorPage.js:152
241 | msgid "Middle Button"
242 | msgstr ""
243 |
244 | #: settings/behaviorPage.js:155
245 | msgid "Activities"
246 | msgstr ""
247 |
248 | #: settings/behaviorPage.js:157
249 | msgid "Click Activities to show the App Grid"
250 | msgstr ""
251 |
252 | #: settings/behaviorPage.js:164
253 | msgid "Switcher Popups"
254 | msgstr ""
255 |
256 | #: settings/behaviorPage.js:165
257 | msgid "Override show delay"
258 | msgstr ""
259 |
260 | #: settings/behaviorPage.js:168 settings/customizePage.js:236
261 | msgid "Show Delay"
262 | msgstr ""
263 |
264 | #: settings/behaviorPage.js:172
265 | msgid "Do not grab focus"
266 | msgstr ""
267 |
268 | #: settings/customizePage.js:12 ui/appButtonMenu.js:318
269 | msgid "Customize"
270 | msgstr ""
271 |
272 | #: settings/customizePage.js:21
273 | msgid "No customizations available"
274 | msgstr ""
275 |
276 | #: settings/customizePage.js:86
277 | msgid "Left"
278 | msgstr ""
279 |
280 | #: settings/customizePage.js:87
281 | msgid "Center"
282 | msgstr ""
283 |
284 | #: settings/customizePage.js:88
285 | msgid "Right"
286 | msgstr ""
287 |
288 | #: settings/customizePage.js:93 settings/customizePage.js:152
289 | #: settings/customizePage.js:219
290 | msgid "Position"
291 | msgstr ""
292 |
293 | #: settings/customizePage.js:97
294 | msgid "Position Offset"
295 | msgstr ""
296 |
297 | #: settings/customizePage.js:100
298 | msgid "Preserve Position"
299 | msgstr ""
300 |
301 | #: settings/customizePage.js:101
302 | msgid "Prevent position changes caused by other extensions in the panel"
303 | msgstr ""
304 |
305 | #: settings/customizePage.js:107
306 | msgid "Backlight Color"
307 | msgstr ""
308 |
309 | #: settings/customizePage.js:110
310 | msgid "App Buttons"
311 | msgstr ""
312 |
313 | #: settings/customizePage.js:112 ui/appButtonMenu.js:453
314 | msgid "Icon Size"
315 | msgstr ""
316 |
317 | #: settings/customizePage.js:114
318 | msgid "Can be configured separately for each app via an app menu"
319 | msgstr ""
320 |
321 | #: settings/customizePage.js:117
322 | msgid "Icon Horizontal Padding"
323 | msgstr ""
324 |
325 | #: settings/customizePage.js:121
326 | msgid "Icon Vertical Padding"
327 | msgstr ""
328 |
329 | #: settings/customizePage.js:125 settings/customizePage.js:259
330 | msgid "Roundness"
331 | msgstr ""
332 |
333 | #: settings/customizePage.js:129
334 | msgid "Spacing"
335 | msgstr ""
336 |
337 | #: settings/customizePage.js:132
338 | msgid "Dominant Color Backlight"
339 | msgstr ""
340 |
341 | #: settings/customizePage.js:137
342 | msgid "Backlight Intensity"
343 | msgstr ""
344 |
345 | #: settings/customizePage.js:146
346 | msgid "Top"
347 | msgstr ""
348 |
349 | #: settings/customizePage.js:147
350 | msgid "Bottom"
351 | msgstr ""
352 |
353 | #: settings/customizePage.js:150
354 | msgid "Indicators"
355 | msgstr ""
356 |
357 | #: settings/customizePage.js:156
358 | msgid "Limit"
359 | msgstr ""
360 |
361 | #: settings/customizePage.js:158
362 | msgid "The maximum number of indicators to display on top of app buttons"
363 | msgstr ""
364 |
365 | #: settings/customizePage.js:160
366 | msgid "Active Dominant Color"
367 | msgstr ""
368 |
369 | #: settings/customizePage.js:162
370 | msgid "Active Color"
371 | msgstr ""
372 |
373 | #: settings/customizePage.js:164
374 | msgid "Inactive Dominant Color"
375 | msgstr ""
376 |
377 | #: settings/customizePage.js:166
378 | msgid "Inactive Color"
379 | msgstr ""
380 |
381 | #: settings/customizePage.js:170
382 | msgid "Inactive Width"
383 | msgstr ""
384 |
385 | #: settings/customizePage.js:174
386 | msgid "Active Width"
387 | msgstr ""
388 |
389 | #: settings/customizePage.js:178
390 | msgid "Inactive Height"
391 | msgstr ""
392 |
393 | #: settings/customizePage.js:182
394 | msgid "Active Height"
395 | msgstr ""
396 |
397 | #: settings/customizePage.js:186
398 | msgid "Inactive Roundness"
399 | msgstr ""
400 |
401 | #: settings/customizePage.js:190
402 | msgid "Active Roundness"
403 | msgstr ""
404 |
405 | #: settings/customizePage.js:195
406 | msgid "Inactive Spacing"
407 | msgstr ""
408 |
409 | #: settings/customizePage.js:199
410 | msgid "Active Spacing"
411 | msgstr ""
412 |
413 | #: settings/customizePage.js:209
414 | msgid "Top Left"
415 | msgstr ""
416 |
417 | #: settings/customizePage.js:210
418 | msgid "Top Right"
419 | msgstr ""
420 |
421 | #: settings/customizePage.js:211
422 | msgid "Bottom Left"
423 | msgstr ""
424 |
425 | #: settings/customizePage.js:212
426 | msgid "Bottom Right"
427 | msgstr ""
428 |
429 | #: settings/customizePage.js:215
430 | msgid "Notification Badges"
431 | msgstr ""
432 |
433 | #: settings/customizePage.js:216
434 | msgid "Color"
435 | msgstr ""
436 |
437 | #: settings/customizePage.js:217
438 | msgid "Border Color"
439 | msgstr ""
440 |
441 | #: settings/customizePage.js:223
442 | msgid "Size"
443 | msgstr ""
444 |
445 | #: settings/customizePage.js:227
446 | msgid "Margin"
447 | msgstr ""
448 |
449 | #: settings/customizePage.js:234
450 | msgid "Tooltips"
451 | msgstr ""
452 |
453 | #: settings/customizePage.js:240
454 | msgid "Max Width"
455 | msgstr ""
456 |
457 | #: settings/customizePage.js:247 settings/generalPage.js:27
458 | msgid "Notification Counter"
459 | msgstr ""
460 |
461 | #: settings/customizePage.js:248
462 | msgid "Hide when empty"
463 | msgstr ""
464 |
465 | #: settings/customizePage.js:249
466 | msgid "Center clock"
467 | msgstr ""
468 |
469 | #: settings/customizePage.js:251
470 | msgid "Max Count"
471 | msgstr ""
472 |
473 | #: settings/customizePage.js:255
474 | msgid "Font Size"
475 | msgstr ""
476 |
477 | #: settings/customizePage.js:263
478 | msgid "Top Margin"
479 | msgstr ""
480 |
481 | #: settings/customizePage.js:267
482 | msgid "Empty Color"
483 | msgstr ""
484 |
485 | #: settings/customizePage.js:269
486 | msgid "Not Empty Color"
487 | msgstr ""
488 |
489 | #: settings/customizePage.js:270
490 | msgid "Text Color"
491 | msgstr ""
492 |
493 | #: settings/customizePage.js:272
494 | msgid "Do Not Disturb - Empty Color"
495 | msgstr ""
496 |
497 | #: settings/customizePage.js:274
498 | msgid "Do Not Disturb - Not Empty Color"
499 | msgstr ""
500 |
501 | #: settings/customizePage.js:275
502 | msgid "Do Not Disturb - Text Color"
503 | msgstr ""
504 |
505 | #: settings/generalPage.js:12
506 | msgid "General"
507 | msgstr ""
508 |
509 | #: settings/generalPage.js:28 settings/generalPage.js:38
510 | msgid "Enabled"
511 | msgstr ""
512 |
513 | #: settings/generalPage.js:40
514 | msgid "Show Favorites"
515 | msgstr ""
516 |
517 | #: settings/generalPage.js:41
518 | msgid "Isolate Workspaces"
519 | msgstr ""
520 |
521 | #: settings/generalPage.js:42
522 | msgid "Enable Indicators"
523 | msgstr ""
524 |
525 | #: settings/generalPage.js:43
526 | msgid "Enable Notification Badges"
527 | msgstr ""
528 |
529 | #: settings/generalPage.js:44
530 | msgid "Enable Tooltips"
531 | msgstr ""
532 |
533 | #: settings/generalPage.js:45
534 | msgid "Enable Sound Volume Control"
535 | msgstr ""
536 |
537 | #: settings/generalPage.js:53
538 | msgid "Kill the Dash"
539 | msgstr ""
540 |
541 | #: settings/generalPage.js:54
542 | msgid ""
543 | "Hide the Dash from Overview and prevent it from rerendering behind the scene"
544 | msgstr ""
545 |
546 | #: ui/appButtonMenu.js:187
547 | msgid "Pin"
548 | msgstr ""
549 |
550 | #: ui/appButtonMenu.js:414
551 | msgid "Activation Behavior"
552 | msgstr ""
553 |
554 | #: ui/appButtonMenu.js:431
555 | msgid "Icon"
556 | msgstr ""
557 |
558 | #: ui/appButtonMenu.js:434
559 | msgid "Import"
560 | msgstr ""
561 |
562 | #: ui/appButtonMenu.js:445
563 | msgid "Reset to default"
564 | msgstr ""
565 |
566 | #: ui/appButtonMenu.js:459
567 | msgid "Reset all to default"
568 | msgstr ""
569 |
--------------------------------------------------------------------------------
/extension/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Rocketbar",
3 | "description": "Taskbar and misc additions for the GNOME Shell.",
4 | "uuid": "rocketbar@chepkun.github.com",
5 | "settings-schema": "org.gnome.shell.extensions.rocketbar",
6 | "gettext-domain": "rocketbar",
7 | "url": "https://github.com/linux-is-awesome/gnome_extension_rocketbar",
8 | "version": 9,
9 | "shell-version": [
10 | "45"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/extension/prefs.js:
--------------------------------------------------------------------------------
1 | import { ExtensionPreferences } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
2 |
3 | import { GeneralPage } from './settings/generalPage.js';
4 | import { CustomizePage } from './settings/customizePage.js';
5 | import { BehaviorPage } from './settings/behaviorPage.js';
6 | import { AboutPage } from './settings/aboutPage.js';
7 |
8 | export default class RocketBarExtensionPreferences extends ExtensionPreferences {
9 | fillPreferencesWindow(window) {
10 | const settings = this.getSettings();
11 |
12 | // enable search
13 | window.set_search_enabled(true);
14 |
15 | // resize the window
16 | window.set_size_request(
17 | window.default_width + 50,
18 | window.default_height + 150
19 | );
20 |
21 | // create pages
22 | window.add(new GeneralPage(settings));
23 | window.add(new CustomizePage(settings));
24 | window.add(new BehaviorPage(settings));
25 | window.add(new AboutPage(this.metadata));
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/extension/schemas/org.gnome.shell.extensions.rocketbar.gschema.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | true
8 | Enable Taskbar
9 |
10 |
11 | true
12 | Show Favorites
13 |
14 |
15 | true
16 | Isolate Workspaces
17 |
18 |
19 | true
20 | Enable Tooltips
21 |
22 |
23 | true
24 | Enable Indicators
25 |
26 |
27 | true
28 | Enable Notification Badges
29 |
30 |
31 | false
32 | Enable Sound Volume Control
33 |
34 |
35 | false
36 | Enable Notification Counter
37 |
38 |
39 | false
40 | Hide the Dash from Overview and prevent it from rerendering behind the scene
41 |
42 |
43 |
44 |
45 |
46 | 'left'
47 | Taskbar position in the panel
48 |
49 |
50 | 1
51 | Taskbar position offset
52 |
53 |
54 | false
55 | Preserve the selected Taskbar position
56 |
57 |
58 |
59 | 20
60 | App Buttons icon size
61 |
62 |
63 | 8
64 | App Buttons icon horizontal padding
65 |
66 |
67 | 3
68 | App Buttons icon vertical padding
69 |
70 |
71 | 1
72 | App Buttons spacing
73 |
74 |
75 | 50
76 | App Buttons roundness
77 |
78 |
79 | 'rgb(255, 255, 255)'
80 | App Buttons backlight color
81 |
82 |
83 | false
84 | App Buttons Dominant color backlight
85 |
86 |
87 | 1
88 | App Buttons backlight intensity
89 |
90 |
91 |
92 | false
93 | Use dominant color for active indicators
94 |
95 |
96 | 'rgb(53, 132, 228)'
97 | Active indicators color when dominant color is not enabled
98 |
99 |
100 | false
101 | Use dominant color for inactive indicators
102 |
103 |
104 | 'rgb(255, 255, 255)'
105 | Inactive indicators color when dominant color is not enabled
106 |
107 |
108 | 'top'
109 | Indicators position
110 |
111 |
112 | 4
113 | Inactive Indicators width
114 |
115 |
116 | 4
117 | Active Indicators width
118 |
119 |
120 | 4
121 | Inactive Indicators width
122 |
123 |
124 | 4
125 | Active Indicators height
126 |
127 |
128 | 4
129 | Inactive Indicators roundness
130 |
131 |
132 | 4
133 | Active Indicators roundness
134 |
135 |
136 | 2
137 | Inactive Indicators spacing
138 |
139 |
140 | 2
141 | Active Indicators spacing
142 |
143 |
144 | 3
145 | Max number of indicators to display for running apps
146 |
147 |
148 |
149 | 'rgb(255, 0, 0)'
150 | Notification Badge color
151 |
152 |
153 | 'rgb(70, 70, 70)'
154 | Notification Badge border color
155 |
156 |
157 | 'bottom_right'
158 | Notification Badge position
159 |
160 |
161 | 5
162 | Notification Badge size
163 |
164 |
165 | 7
166 | Notification Badge margin
167 |
168 |
169 |
170 | 500
171 | Tooltips show delay
172 |
173 |
174 | 500
175 | Tooltips max width
176 |
177 |
178 |
179 | false
180 | Hide Notification Counter when it's empty
181 |
182 |
183 | false
184 | Center the clock when Notification Counter is shown
185 |
186 |
187 | 999
188 | Notification Counter max count to display
189 |
190 |
191 | 10
192 | Notification Counter font size
193 |
194 |
195 | 10
196 | Notification Counter roundness
197 |
198 |
199 | 0
200 | Notification Counter top margin
201 |
202 |
203 | 'rgb(255, 255, 255)'
204 | Notification Counter color when empty
205 |
206 |
207 | 'rgb(255, 255, 255)'
208 | Notification Counter color when not empty
209 |
210 |
211 | 'rgb(0, 0, 0)'
212 | Notification Counter text color
213 |
214 |
215 | 'rgba(255, 255, 255, 0.8)'
216 | Notification Counter - Do Not Disturb color when empty
217 |
218 |
219 | 'rgba(255, 255, 255, 0.8)'
220 | Notification Counter - Do Not Disturb color when not empty
221 |
222 |
223 | 'rgb(0, 0, 0)'
224 | Notification Counter - Do Not Disturb text color
225 |
226 |
227 |
228 |
229 |
230 | true
231 | Allow Drag and Drop
232 |
233 |
234 | true
235 | Require click to open context menus for apps in the taskbar
236 |
237 |
238 | 'new_window'
239 | Running apps activation behavior
240 |
241 |
242 | true
243 | Enable Minimize action
244 |
245 |
246 | false
247 | Scroll to change application sound volume
248 |
249 |
250 | true
251 | Scroll to cycle app windows
252 |
253 |
254 | false
255 | Middle click to toggle application sound mute
256 |
257 |
258 | false
259 | Use Unity Launcher API DBus interface to count notifications for apps
260 |
261 |
262 | false
263 | Count Window Demands Attention notifications for apps
264 |
265 |
266 | false
267 | Require click to activate the panel menu buttons
268 |
269 |
270 | true
271 | Middle click on panel empty space to toggle sound mute
272 |
273 |
274 | 'none'
275 | Panel scroll action
276 |
277 |
278 | 2
279 | Sound volume change speed
280 |
281 |
282 | 10
283 | Sound volume change speed when Ctrl pressed
284 |
285 |
286 | true
287 | Enable Hot Corner in fullscreen mode
288 |
289 |
290 | 'right_button'
291 | Click Activities to show the App Grid
292 |
293 |
294 | true
295 | Enable empty space clicks in Overview
296 |
297 |
298 | false
299 | Force primary input source on Lock Screen
300 |
301 |
302 | false
303 | Allow to override Switcher Popup windows show delay
304 |
305 |
306 | 150
307 | Switcher Popup windows show delay
308 |
309 |
310 | false
311 | Handle Switcher Popup windows to change focus behavior
312 |
313 |
314 |
315 |
316 |
317 | ''
318 | AppButton config override
319 |
320 |
321 |
322 |
323 |
324 |
--------------------------------------------------------------------------------
/extension/services/notificationService.js:
--------------------------------------------------------------------------------
1 | /* exported NotificationHandler */
2 |
3 | //#region imports
4 |
5 | import * as Main from 'resource:///org/gnome/shell/ui/main.js';
6 |
7 | // custom modules import
8 | import { Connections } from '../utils/connections.js';
9 | import { LauncherAPI } from '../utils/launcherAPI.js';
10 | import { Timeout } from '../utils/timeout.js';
11 |
12 | //#endregion imports
13 |
14 | class UnityDBusConnector {
15 |
16 | // store the counters in a static variable
17 | // to restore them after unlocking user's session
18 | static _countByAppId = null; // Map
19 |
20 | constructor(callback) {
21 | this._callback = callback;
22 |
23 | if (!UnityDBusConnector._countByAppId) {
24 | UnityDBusConnector._countByAppId = new Map();
25 | } else {
26 | this._callback();
27 | }
28 |
29 | this._dbusHandler = LauncherAPI.instance().subscribe(params => this._update(params));
30 | }
31 |
32 | destroy() {
33 | LauncherAPI.instance().unsubscribe(this._dbusHandler);
34 | }
35 |
36 | getCount(callback) {
37 |
38 | if (!UnityDBusConnector._countByAppId?.size || !callback) {
39 | return;
40 | }
41 |
42 | UnityDBusConnector._countByAppId.forEach(callback);
43 | }
44 |
45 | _update(params) {
46 |
47 | if (!params || !UnityDBusConnector._countByAppId) {
48 | return;
49 | }
50 |
51 | const [ appUri, props ] = params.deepUnpack();
52 |
53 | const appId = appUri?.replace(/(^\w+:|^)\/\//, '');
54 |
55 | if (!appId || !props) {
56 | return;
57 | }
58 |
59 | const count = props['count']?.get_int64() ?? 0;
60 | const countVisible = props['count-visible']?.get_boolean() ?? false;
61 |
62 | if (!count || !countVisible) {
63 |
64 | // no need to trigger the callback as nothing has changed
65 | if (!UnityDBusConnector._countByAppId.has(appId)) {
66 | return;
67 | }
68 |
69 | UnityDBusConnector._countByAppId.delete(appId);
70 | } else {
71 | UnityDBusConnector._countByAppId.set(appId, count);
72 | }
73 |
74 | this._callback();
75 | }
76 |
77 | }
78 |
79 |
80 | const APPID_REGEXP_STRING = /.desktop/g;
81 |
82 | function cleanAppId(appId) {
83 | return appId?.replace(APPID_REGEXP_STRING, '');
84 | }
85 |
86 | const NotificationSource = {
87 | FdoNotification: 'FdoNotificationDaemonSource',
88 | GtkNotification: 'GtkNotificationDaemonAppSource',
89 | WindowAttention: 'WindowAttentionSource'
90 | };
91 |
92 | class NotificationService {
93 |
94 | constructor(settings) {
95 |
96 | this._settings = settings;
97 | this._handlers = []; // [NotificationHandler...]
98 |
99 | this._resetCounts();
100 |
101 | this._createSources();
102 |
103 | this._handleSettings();
104 |
105 | this._createConnections();
106 | }
107 |
108 | addHandler(handler) {
109 |
110 | if (!handler) {
111 | return;
112 | }
113 |
114 | this._handlers.push(handler);
115 |
116 | this._triggerHandler(handler);
117 | }
118 |
119 | removeHandler(handler) {
120 |
121 | if (!handler) {
122 | return;
123 | }
124 |
125 | const handlerIndex = this._handlers.indexOf(handler);
126 |
127 | if (handlerIndex < 0) {
128 | return;
129 | }
130 |
131 | this._handlers.splice(handlerIndex, 1);
132 | }
133 |
134 | isEmpty() {
135 | return !this._handlers.length;
136 | }
137 |
138 | destroy() {
139 |
140 | this._stopUpdateCountQueue();
141 |
142 | this._unityDBusConnector?.destroy();
143 |
144 | this._connections.destroy();
145 | }
146 |
147 | _createConnections() {
148 | this._connections = new Connections();
149 | this._connections.add(Main.messageTray, 'source-added', (tray, source) => this._addSource(source));
150 | this._connections.add(Main.messageTray, 'source-removed', (tray, source) => this._removeSource(source));
151 | this._connections.add(Main.messageTray, 'queue-changed', () => this._queueUpdateCount());
152 | // handle settings
153 | this._connections.addScope(this._settings, [
154 | 'changed::notification-service-enable-unity-dbus',
155 | 'changed::notification-service-count-attention-sources'
156 | ], () => {
157 | this._handleSettings();
158 | this._queueUpdateCount();
159 | });
160 | }
161 |
162 | _handleSettings() {
163 |
164 | this._setConfig();
165 |
166 | if (this._config.enableUnityDBus && !this._unityDBusConnector) {
167 | this._unityDBusConnector = new UnityDBusConnector(() => this._queueUpdateCount());
168 | } else if (!this._config.enableUnityDBus && this._unityDBusConnector) {
169 | this._unityDBusConnector.destroy();
170 | this._unityDBusConnector = null;
171 | }
172 | }
173 |
174 | _setConfig() {
175 | this._config = {
176 | enableUnityDBus: this._settings.get_boolean('notification-service-enable-unity-dbus'),
177 | countAttentionSources: this._settings.get_boolean('notification-service-count-attention-sources')
178 | };
179 | }
180 |
181 | _createSources() {
182 |
183 | this._sources = new Map(); // source => connection id
184 |
185 | const messageTraySources = Main.messageTray.getSources();
186 |
187 | if (!messageTraySources.length) {
188 | return;
189 | }
190 |
191 | for (let i = 0, l = messageTraySources.length; i < l; ++i) {
192 | this._addSource(messageTraySources[i]);
193 | }
194 | }
195 |
196 | _addSource(source) {
197 |
198 | if (!this._sources.has(source)) {
199 | this._sources.set(source, source.connect('notify::count', () => this._queueUpdateCount()));
200 | }
201 |
202 | this._queueUpdateCount();
203 | }
204 |
205 | _removeSource(source) {
206 |
207 | if (this._sources.has(source)) {
208 | source.disconnect(this._sources.get(source));
209 | this._sources.delete(source);
210 | }
211 |
212 | this._queueUpdateCount();
213 | }
214 |
215 | _queueUpdateCount() {
216 |
217 | this._stopUpdateCountQueue();
218 |
219 | // slow down it a bit
220 | // I believe we can wait for 500 ms to get notifications
221 | this._updateCountTimeout = Timeout.idle(500).run(() => {
222 | this._updateCountTimeout = null;
223 | this._updateCount();
224 | });
225 | }
226 |
227 | _stopUpdateCountQueue() {
228 | this._updateCountTimeout?.destroy();
229 | this._updateCountTimeout = null;
230 | }
231 |
232 | _updateCount() {
233 |
234 | this._resetCounts();
235 |
236 | let unityAppIds = new Set();
237 |
238 | // let's use the Unity dbus connection
239 | // as the source of truth to count notifications for apps
240 | this._unityDBusConnector?.getCount((count, appId) => {
241 | unityAppIds.add(appId);
242 | this._countByAppId.set(appId, count);
243 | });
244 |
245 | const sources = [...this._sources.keys()];
246 |
247 | for (let i = 0, l = sources.length; i < l; ++i) {
248 |
249 | const source = sources[i];
250 | const sourceCount = source.count || 0;
251 |
252 | this._totalCount += sourceCount;
253 |
254 | // count notifications for apps
255 |
256 | let appId = this._getNotificationAppId(source);
257 |
258 | if (!appId || unityAppIds.has(appId)) {
259 | continue;
260 | }
261 |
262 | let countForApp = this._countByAppId.get(appId) || 0;
263 |
264 | countForApp += sourceCount;
265 |
266 | if (countForApp === 0) {
267 | continue;
268 | }
269 |
270 | this._countByAppId.set(appId, countForApp);
271 | }
272 |
273 | this._triggerHandlers();
274 | }
275 |
276 | _getNotificationAppId(source) {
277 | if (source.constructor?.name === NotificationSource.FdoNotification) {
278 | return cleanAppId(source.app?.id);
279 | } else if (source.constructor?.name === NotificationSource.GtkNotification) {
280 | return cleanAppId(source._appId);
281 | } else if (source.constructor?.name === NotificationSource.WindowAttention) {
282 | return cleanAppId(source._app?.id);
283 | } else {
284 | return null;
285 | }
286 | }
287 |
288 | _resetCounts() {
289 | this._totalCount = 0;
290 | this._countByAppId = new Map();
291 | }
292 |
293 | _triggerHandlers() {
294 | for (let i = 0, l = this._handlers.length; i < l; ++i) {
295 | this._triggerHandler(this._handlers[i]);
296 | }
297 | }
298 |
299 | _triggerHandler(handler) {
300 |
301 | if (!handler.appId) {
302 | handler.setCount(this._totalCount);
303 | return;
304 | }
305 |
306 | handler.setCount(this._countByAppId.get(handler.appId) || 0);
307 | }
308 | }
309 |
310 | export class NotificationHandler {
311 |
312 | // static instance of NotificationService
313 | static _service = null;
314 |
315 | /*
316 | * callback: (count) => {} to notify handler target about notifications
317 | * appId: optional string value to filter notifications by app Id, null to get total count
318 | */
319 | constructor(callback, settings, appId) {
320 |
321 | this.appId = cleanAppId(appId);
322 |
323 | this._callback = callback;
324 |
325 | if (!NotificationHandler._service) {
326 | NotificationHandler._service = new NotificationService(settings);
327 | }
328 |
329 | NotificationHandler._service.addHandler(this);
330 | }
331 |
332 | destroy() {
333 |
334 | this.setCount(0);
335 |
336 | this._callback = null;
337 |
338 | if (!NotificationHandler._service) {
339 | return;
340 | }
341 |
342 | NotificationHandler._service.removeHandler(this);
343 |
344 | if (NotificationHandler._service.isEmpty()) {
345 | NotificationHandler._service.destroy();
346 | NotificationHandler._service = null;
347 | }
348 | }
349 |
350 | setCount(count) {
351 |
352 | if (!this._callback) {
353 | return;
354 | }
355 |
356 | this._callback(count);
357 | }
358 |
359 | }
360 |
--------------------------------------------------------------------------------
/extension/services/soundVolumeService.js:
--------------------------------------------------------------------------------
1 | /* exported SoundVolumeControl, AppSoundVolumeControl */
2 |
3 | //#region imports
4 |
5 | import Gio from 'gi://Gio';
6 | import Gvc from 'gi://Gvc';
7 | import Shell from 'gi://Shell';
8 | import * as Main from 'resource:///org/gnome/shell/ui/main.js';
9 | import * as Volume from 'resource:///org/gnome/shell/ui/status/volume.js';
10 |
11 | // custom modules import
12 | import { Connections } from '../utils/connections.js';
13 | import { Timeout } from '../utils/timeout.js';
14 |
15 | //#endregion imports
16 |
17 | //#region base classes
18 |
19 | class SoundStream {
20 |
21 | constructor(stream) {
22 |
23 | this._stream = stream;
24 |
25 | this.id = this._stream?.id;
26 |
27 | this._volumeMax = (
28 | this._stream?.get_base_volume() ||
29 | // for app streams get_base_volume returns 0
30 | // so we need this fallback
31 | Volume.getMixerControl().get_vol_max_norm()
32 | );
33 | }
34 |
35 | /*
36 | * volume: positive double 0..0.1...0.8..0.9..1
37 | */
38 | setVolume(volume) {
39 |
40 | if (!this._stream) {
41 | return false;
42 | }
43 |
44 | volume = this._volumeMax * volume;
45 |
46 | volume = Math.min(volume, this._volumeMax);
47 | volume = Math.max(volume, 0);
48 |
49 | this._stream.volume = volume;
50 | this._stream.push_volume();
51 |
52 | return true;
53 | }
54 |
55 | toggleMute() {
56 |
57 | if (!this._stream) {
58 | return false;
59 | }
60 |
61 | this._stream.change_is_muted(!this.isMuted());
62 |
63 | return true;
64 | }
65 |
66 | // change_is_muted changes state of the stream with a delay
67 | // so we may need to pass the actual state manually
68 | getVolume(isMuted = this.isMuted()) {
69 | return (
70 | !this._stream || isMuted ? 0 :
71 | this._stream.volume / this._volumeMax
72 | );
73 | }
74 |
75 | isPlaying() {
76 | return this._stream?.state === Gvc.MixerStreamState.RUNNING;
77 | }
78 |
79 | isMuted() {
80 | return this._stream?.is_muted;
81 | }
82 |
83 | getName() {
84 | return this._stream ? (
85 | this._stream.get_port() ?
86 | this._stream.get_port().human_port :
87 | this._stream.get_name()
88 | ) : null;
89 | }
90 |
91 | }
92 |
93 | class SoundVolumeControlBase {
94 |
95 | constructor() {
96 | this._notifyVolumeChangeTimeout = null;
97 | }
98 |
99 | destroy() {
100 | this._notifyVolumeChangeTimeout?.destroy();
101 | }
102 |
103 | _showOSD(stream, isMuted, name) {
104 |
105 | if (!stream) {
106 | return;
107 | }
108 |
109 | const volumeLevel = stream.getVolume(isMuted);
110 | const volumeIcon = this._getVolumeIcon(volumeLevel);
111 | const monitorIndex = global.display.get_current_monitor();
112 |
113 | Main.osdWindowManager.show(monitorIndex, volumeIcon, name || stream.getName(), volumeLevel);
114 | }
115 |
116 | _getVolumeIcon(volumeLevel = 0) {
117 |
118 | const volumeIcons = [
119 | 'audio-volume-muted-symbolic',
120 | 'audio-volume-low-symbolic',
121 | 'audio-volume-medium-symbolic',
122 | 'audio-volume-high-symbolic'
123 | ];
124 |
125 | let iconIndex = 0;
126 |
127 | if (volumeLevel > 0) {
128 |
129 | const iconIndexMax = volumeIcons.length - 1;
130 |
131 | iconIndex = parseInt(iconIndexMax * volumeLevel + 1);
132 | iconIndex = Math.max(1, iconIndex);
133 | iconIndex = Math.min(iconIndexMax, iconIndex);
134 | }
135 |
136 | return Gio.Icon.new_for_string(volumeIcons[iconIndex]);
137 | }
138 |
139 | _notifyVolumeChange(stream) {
140 |
141 | if (this._notifyVolumeChangeTimeout) {
142 | return;
143 | }
144 |
145 | // slow down notifications a bit
146 | this._notifyVolumeChangeTimeout = Timeout.default(50).run(() => {
147 | this._notifyVolumeChangeTimeout = null;
148 | });
149 |
150 | if (this._volumeCancellable) {
151 | this._volumeCancellable.cancel();
152 | }
153 |
154 | this._volumeCancellable = null;
155 |
156 | // feedback not necessary while playing
157 | if (!stream || stream.isPlaying())
158 | return;
159 |
160 | this._volumeCancellable = new Gio.Cancellable();
161 |
162 | const player = global.display.get_sound_player();
163 |
164 | player.play_from_theme('audio-volume-change', 'Volume changed', this._volumeCancellable);
165 | }
166 |
167 | }
168 |
169 | //#endregion base classes
170 |
171 | //#region system wide control
172 |
173 | export class SoundVolumeControl extends SoundVolumeControlBase {
174 |
175 | constructor() {
176 | super();
177 |
178 | this._stream = null;
179 |
180 | this._connections = new Connections();
181 |
182 | const mixerControl = Volume.getMixerControl();
183 |
184 | this._connections.add(
185 | mixerControl, 'default-sink-changed',
186 | (mixerControl, streamId) => this._handleActiveStream(mixerControl, streamId)
187 | );
188 |
189 | this._handleActiveStream(mixerControl);
190 | }
191 |
192 | destroy() {
193 |
194 | super.destroy();
195 |
196 | this._connections.destroy();
197 | }
198 |
199 | /*
200 | * volume: negative or positive integer -100..-1, 1..100
201 | */
202 | addVolume(volume) {
203 |
204 | if (!volume || !this._stream) {
205 | return;
206 | }
207 |
208 | if (this._stream.isMuted()) {
209 | this.toggleMute(this._stream);
210 | return;
211 | }
212 |
213 | if (!this._stream.setVolume(this._stream.getVolume() + (volume / 100))) {
214 | return;
215 | }
216 |
217 | this._showOSD(this._stream);
218 |
219 | this._notifyVolumeChange(this._stream);
220 | }
221 |
222 | toggleMute() {
223 |
224 | if (!this._stream) {
225 | return;
226 | }
227 |
228 | const isMuted = this._stream.isMuted();
229 |
230 | if (!this._stream.toggleMute()) {
231 | return;
232 | }
233 |
234 | this._showOSD(this._stream, !isMuted);
235 |
236 | // play sound when stream gets unmuted
237 | if (isMuted) {
238 | this._notifyVolumeChange(this._stream);
239 | }
240 | }
241 |
242 | _handleActiveStream(mixerControl, streamId) {
243 | this._stream = new SoundStream(
244 | streamId ?
245 | mixerControl.lookup_stream_id(streamId) :
246 | mixerControl.get_default_sink()
247 | );
248 | }
249 |
250 | }
251 |
252 | //#endregion system wide control
253 |
254 | //#region app sound volume control
255 |
256 | class AppSoundStream extends SoundStream {
257 |
258 | static isValidStream(stream) {
259 | return stream && stream.id && !stream.is_event_stream && (
260 | stream instanceof Gvc.MixerSinkInput ||
261 | stream instanceof Gvc.MixerSourceOutput
262 | );
263 | }
264 |
265 | constructor(stream) {
266 | super(stream);
267 |
268 | this.name = this._stream?.name;
269 |
270 | this.isInput = (
271 | this._stream &&
272 | this._stream instanceof Gvc.MixerSourceOutput
273 | );
274 |
275 | this._listeners = []; // [AppSoundVolumeControl...]
276 | }
277 |
278 | addListener(listener) {
279 |
280 | if (!this._listeners || (
281 | this._listeners.length &&
282 | this._listeners.indexOf(listener) >= 0
283 | )) {
284 | return;
285 | }
286 |
287 | this._listeners.push(listener);
288 | }
289 |
290 | removeListener(listener) {
291 |
292 | if (!this._listeners || !this._listeners.length) {
293 | return;
294 | }
295 |
296 | const index = this._listeners.indexOf(listener);
297 |
298 | if (index < 0) {
299 | return;
300 | }
301 |
302 | this._listeners.splice(index, 1);
303 |
304 | }
305 |
306 | destroy() {
307 |
308 | this._stream = null;
309 |
310 | if (this._listeners.length) {
311 | for (let listener of this._listeners) {
312 | listener.removeStream(this);
313 | }
314 | }
315 |
316 | this._listeners = null;
317 | }
318 | }
319 |
320 | class AppSoundVolumeService {
321 |
322 | constructor() {
323 |
324 | this._controls = [];
325 |
326 | const mixerControl = Volume.getMixerControl();
327 |
328 | this._setStreams(mixerControl);
329 |
330 | this._connections = new Connections();
331 |
332 | this._connections.add(
333 | mixerControl, 'stream-added',
334 | (mixerControl, streamId) => this._addStream(mixerControl, streamId)
335 | );
336 |
337 | this._connections.add(
338 | mixerControl, 'stream-removed',
339 | (mixerControl, streamId) => this._removeStream(streamId)
340 | );
341 | }
342 |
343 | destroy() {
344 |
345 | this._controls = null;
346 | this._streams = null;
347 |
348 | this._stopUpdateControls();
349 |
350 | this._connections.destroy();
351 | }
352 |
353 | isEmpty() {
354 | return !this._controls?.length;
355 | }
356 |
357 | addControl(control) {
358 |
359 | if (!control || this._controls.indexOf(control) >= 0) {
360 | return;
361 | }
362 |
363 | this._controls.push(control);
364 |
365 | this._queueUpdateControls();
366 | }
367 |
368 | removeControl(control) {
369 |
370 | if (!control) {
371 | return;
372 | }
373 |
374 | const controlIndex = this._controls.indexOf(control);
375 |
376 | if (controlIndex >= 0) {
377 | this._controls.splice(controlIndex, 1);
378 | }
379 |
380 | }
381 |
382 | forceUpdate() {
383 | this._queueUpdateControls();
384 | }
385 |
386 | _setStreams(mixerControl) {
387 |
388 | this._streams = new Map(); // id => AppSoundStream
389 |
390 | for (let stream of mixerControl.get_streams()) {
391 |
392 | if (!AppSoundStream.isValidStream(stream)) {
393 | continue;
394 | }
395 |
396 | const appStream = new AppSoundStream(stream);
397 |
398 | if (!appStream.id) {
399 | continue;
400 | }
401 |
402 | this._streams.set(appStream.id, appStream);
403 | }
404 | }
405 |
406 | _addStream(mixerControl, streamId) {
407 |
408 | if (!mixerControl || !streamId || !this._streams) {
409 | return;
410 | }
411 |
412 | if (this._streams.has(streamId)) {
413 | return;
414 | }
415 |
416 | const stream = mixerControl.lookup_stream_id(streamId);
417 |
418 | if (AppSoundStream.isValidStream(stream)) {
419 | this._streams.set(streamId, new AppSoundStream(stream));
420 | }
421 |
422 | this._queueUpdateControls();
423 | }
424 |
425 | _removeStream(streamId) {
426 |
427 | if (!streamId || !this._streams) {
428 | return;
429 | }
430 |
431 | const appStream = this._streams.get(streamId);
432 |
433 | if (appStream) {
434 | appStream.destroy();
435 | this._streams.delete(streamId);
436 | }
437 |
438 | }
439 |
440 | _queueUpdateControls() {
441 | this._stopUpdateControls();
442 |
443 | if (this._skipUpdateControls()) {
444 | return;
445 | }
446 |
447 | this._updateControlsTimeout = Timeout.idle(500).run(() => {
448 | this._updateControlsTimeout = null;
449 | this._updateControls();
450 | });
451 | }
452 |
453 | _stopUpdateControls() {
454 | this._updateControlsTimeout?.destroy();
455 | this._updateControlsTimeout = null;
456 | }
457 |
458 | _updateControls() {
459 |
460 | if (this._skipUpdateControls()) {
461 | return;
462 | }
463 |
464 | const streams = [...this._streams.values()];
465 |
466 | for (let i = 0, l = this._controls.length; i < l; ++i) {
467 |
468 | const appControl = this._controls[i];
469 |
470 | for (let i = 0, l = streams.length; i < l; ++i) {
471 | // let the app control to validate the stream
472 | appControl.addStream(streams[i]);
473 | }
474 | }
475 | }
476 |
477 | _skipUpdateControls() {
478 | return !this._controls?.length || !this._streams?.size;
479 | }
480 |
481 | }
482 |
483 | export class AppSoundVolumeControl extends SoundVolumeControlBase {
484 |
485 | static _service = null;
486 |
487 | constructor(app) {
488 | super();
489 |
490 | this._app = app;
491 |
492 | this._originalAppName = this._app.get_name();
493 | this._appName = null; // will be set later
494 |
495 | this._inputStreams = [];
496 |
497 | this._outputStreams = [];
498 |
499 | if (!AppSoundVolumeControl._service) {
500 | AppSoundVolumeControl._service = new AppSoundVolumeService();
501 | }
502 |
503 | AppSoundVolumeControl._service.addControl(this);
504 | }
505 |
506 | destroy() {
507 |
508 | super.destroy();
509 |
510 | if (this._inputStreams) {
511 | for (let appStream of this._inputStreams) {
512 | appStream.removeListener(this);
513 | }
514 | this._inputStreams = null;
515 | }
516 |
517 | if (this._outputStreams) {
518 | for (let appStream of this._outputStreams) {
519 | appStream.removeListener(this);
520 | }
521 | this._outputStreams = null;
522 | }
523 |
524 | if (!AppSoundVolumeControl._service) {
525 | return;
526 | }
527 |
528 | AppSoundVolumeControl._service.removeControl(this);
529 |
530 | if (AppSoundVolumeControl._service.isEmpty()) {
531 | AppSoundVolumeControl._service.destroy();
532 | AppSoundVolumeControl._service = null;
533 | }
534 | }
535 |
536 | addStream(appStream) {
537 |
538 | if (!this._inputStreams || !this._outputStreams ||
539 | !this._canAcceptStream(appStream)) {
540 | return;
541 | }
542 |
543 | const streams = (
544 | appStream.isInput ?
545 | this._inputStreams :
546 | this._outputStreams
547 | );
548 |
549 | if (streams.length && streams.indexOf(appStream) >= 0) {
550 | return;
551 | }
552 |
553 | streams.push(appStream);
554 |
555 | appStream.addListener(this);
556 | }
557 |
558 | removeStream(appStream) {
559 |
560 | if (!appStream || !this._inputStreams || !this._outputStreams) {
561 | return;
562 | }
563 |
564 | const streams = (
565 | appStream.isInput ?
566 | this._inputStreams :
567 | this._outputStreams
568 | );
569 |
570 | const streamIndex = (
571 | streams.length ?
572 | streams.indexOf(appStream) :
573 | -1
574 | );
575 |
576 | if (streamIndex < 0) {
577 | return;
578 | }
579 |
580 | streams.splice(streamIndex, 1);
581 |
582 | // no need to call appStream.removeListener because this method
583 | // will be called by the appStream itself when it gets removed
584 | }
585 |
586 | //#region functions to be called outside this module
587 |
588 | handleAppState() {
589 |
590 | // no need to set the name twice
591 | if (this._appName !== null) {
592 | return;
593 | }
594 |
595 | // try to set the app name
596 | this._setAppName();
597 |
598 | // if the name has changed and is not equal to the original name
599 | // force the service to update app controls
600 | if (this._appName && this._appName !== this._originalAppName) {
601 | AppSoundVolumeControl._service?.forceUpdate();
602 | }
603 | }
604 |
605 | getInputVolume() {
606 | return this._getVolume(this._inputStreams);
607 | }
608 |
609 | getOutputVolume() {
610 | return this._getVolume(this._outputStreams);
611 | }
612 |
613 | setInputVolume(volume) {
614 | this._setVolume(this._inputStreams, volume);
615 | }
616 |
617 | setOutputVolume(volume) {
618 | this._setVolume(this._outputStreams, volume);
619 | }
620 |
621 | /*
622 | * volume: negative or positive integer -100..-1, 1..100
623 | */
624 | addOutputVolume(volume) {
625 |
626 | if (!volume || !this.hasOutput()) {
627 | return;
628 | }
629 |
630 | volume = this._getVolume(this._outputStreams, false) + (volume / 100);
631 |
632 | this._setVolume(this._outputStreams, volume);
633 |
634 | this._showOSD(this._outputStreams[0], false);
635 | }
636 |
637 | toggleOutputMute() {
638 |
639 | if (!this.hasOutput()) {
640 | return;
641 | }
642 |
643 | const isMuted = this._isMuted(this._outputStreams);
644 |
645 | this._toggleMute(this._outputStreams, isMuted);
646 |
647 | this._showOSD(this._outputStreams[0], !isMuted);
648 | }
649 |
650 | hasInput() {
651 | return this._inputStreams?.length > 0;
652 | }
653 |
654 | hasOutput() {
655 | return this._outputStreams?.length > 0;
656 | }
657 |
658 | //#endregion functions to be called outside this module
659 |
660 | _canAcceptStream(stream) {
661 |
662 | if (!stream || !stream.name) {
663 | return false;
664 | }
665 |
666 | if (this._appName === null) {
667 | this._setAppName();
668 | }
669 |
670 | // Sometimes name of the app stream is not equal to the name of the app
671 | // But it contains name of the app
672 | // For ex: Google Chrome create input streams called 'Google Chrome input'
673 | return stream.name.includes(this._appName || this._originalAppName);
674 | }
675 |
676 | _setAppName() {
677 |
678 | if (!this._app) {
679 | // just in case set dummy name to avoid calling this method twice
680 | this._appName = '';
681 | }
682 |
683 | // A workaround to handle Chrome Apps and probably something else
684 | // Chrome Apps share Google Chrome's sound streams
685 | // To identify proper streams for such apps we need to get name of the parent app
686 |
687 | const appWindows = this._app.get_windows();
688 |
689 | if (!appWindows || !appWindows.length) {
690 | return;
691 | }
692 |
693 | let appName = null;
694 |
695 | if (appWindows[0].wm_class) {
696 |
697 | let searchResult = Shell.AppSystem.search(appWindows[0].wm_class);
698 |
699 | // it's an array of arrays [[],[],[]]
700 | if (searchResult?.length && searchResult[0]?.length) {
701 |
702 | for (let appId of searchResult[0]) {
703 |
704 | let app = Shell.AppSystem.get_default().lookup_app(appId);
705 |
706 | if (!app || !app.get_windows()?.length) {
707 | continue;
708 | }
709 |
710 | appName = app.get_name();
711 |
712 | break;
713 | }
714 |
715 | }
716 | }
717 |
718 | this._appName = appName || this._originalAppName;
719 | }
720 |
721 | _getVolume(streams, isMuted) {
722 |
723 | if (!streams || !streams.length) {
724 | return 0;
725 | }
726 |
727 | let result = 1;
728 |
729 | for (let appStream of streams) {
730 | result = Math.min(appStream.getVolume(isMuted), result);
731 | }
732 |
733 | return result;
734 | }
735 |
736 | _setVolume(streams, volume) {
737 |
738 | if (!streams || !streams.length) {
739 | return;
740 | }
741 |
742 | for (let appStream of streams) {
743 |
744 | if (appStream.isMuted()) {
745 | appStream.toggleMute();
746 | }
747 |
748 | appStream.setVolume(volume);
749 | }
750 | }
751 |
752 | _toggleMute(streams, isMuted) {
753 |
754 | if (!streams || !streams.length) {
755 | return;
756 | }
757 |
758 | for (let appStream of streams) {
759 | if (appStream.isMuted() === isMuted) {
760 | appStream.toggleMute();
761 | }
762 | }
763 | }
764 |
765 | _isMuted(streams) {
766 |
767 | if (!streams || !streams.length) {
768 | return false;
769 | }
770 |
771 | for (let appStream of streams) {
772 | if (appStream.isMuted()) {
773 | return true;
774 | }
775 | }
776 |
777 | return false;
778 | }
779 |
780 | _showOSD(stream, isMuted) {
781 | super._showOSD(stream, isMuted, this._originalAppName);
782 | }
783 |
784 | }
785 |
786 | //#endregion app sound volume control
--------------------------------------------------------------------------------
/extension/settings/aboutPage.js:
--------------------------------------------------------------------------------
1 | import GObject from 'gi://GObject';
2 | import { gettext as _ } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
3 |
4 | import { SettingsPageTemplate } from './pageTemplate.js';
5 |
6 | export const AboutPage = GObject.registerClass(
7 | class Rocketbar__AboutPage extends SettingsPageTemplate {
8 |
9 | _init(metadata) {
10 | super._init({
11 | title: _('About'),
12 | name: 'AboutPage',
13 | icon: 'help-about-symbolic'
14 | });
15 |
16 | this._metadata = metadata;
17 |
18 | this._createLayout();
19 | }
20 |
21 | _createLayout() {
22 |
23 | this.addGroup(null, [
24 | this.createLabel(this._metadata.name + _(' Version'), this._metadata.version + '.0'),
25 | this.createLink(_('Release Notes'), this._metadata.url + '/releases')
26 | ]);
27 |
28 | this.addGroup(_('Useful Links'), [
29 | this.createLink(_('Report an issue'), this._metadata.url + '/issues'),
30 | this.createLink(_('Share your ideas'), this._metadata.url + '/discussions/categories/ideas')
31 | ]);
32 |
33 | this.addGroup(_('Credits'), [
34 | this.createLink('App Icons Taskbar', 'https://gitlab.com/AndrewZaech/aztaskbar'),
35 | this.createLink('Dash to Dock', 'https://github.com/micheleg/dash-to-dock'),
36 | this.createLink('Overview Clicking', 'https://github.com/mechtifs/overview-clicking'),
37 | this.createLink('Volume Scroller', 'https://github.com/trflynn89/gnome-shell-volume-scroller'),
38 | this.createLink('Fullscreen Hot Corner', 'https://github.com/soal/gnome-shell-fullscreen-hot-corner'),
39 | this.createLink('Just Perfection', 'https://gitlab.gnome.org/jrahmatzadeh/just-perfection')
40 | ]);
41 | }
42 |
43 | }
44 | );
45 |
--------------------------------------------------------------------------------
/extension/settings/behaviorPage.js:
--------------------------------------------------------------------------------
1 | import GObject from 'gi://GObject';
2 | import { gettext as _ } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
3 |
4 | import { SettingsPageTemplate } from './pageTemplate.js';
5 |
6 | export const BehaviorPage = GObject.registerClass(
7 | class Rocketbar__BehaviorPage extends SettingsPageTemplate {
8 |
9 | _init(settings) {
10 |
11 | super._init({
12 | title: _('Behavior'),
13 | name: 'BehaviorPage',
14 | icon: 'applications-engineering-symbolic',
15 | settings: settings
16 | });
17 |
18 | this._populateOptions();
19 | }
20 |
21 | _populateOptions() {
22 |
23 | // Taskbar
24 | this._addTaskbarOptions();
25 |
26 | // Notification Service
27 | this.addGroup(_('Notification Service'), [
28 | this.createSwitch(_('Enable Unity Launcher API support'), 'notification-service-enable-unity-dbus',
29 | _('Use Unity Launcher API DBus interface to count notifications for apps')),
30 | this.createSwitch(_('Count Window Demands Attention notifications for apps'), 'notification-service-count-attention-sources')
31 | ]);
32 |
33 | // Panel
34 | this._addPanelOptions();
35 |
36 | // Sound Volume Control
37 | this._addSoundControlOptions();
38 |
39 | // Activities
40 | this._addActivitiesOptions();
41 |
42 | // Overview
43 | this.addGroup(_('Overview'), [
44 | this.createSwitch(_('Enable empty space clicks in Overview'), 'overview-enable-empty-space-clicks',
45 | _('Left button click to close the Overview, Right button click to show the App Grid'))
46 | ]);
47 |
48 | // Hot Corner
49 | this.addGroup(_('Hot Corner'), [
50 | this.createSwitch(_('Enable Fullscreen Hot Corner'), 'hotcorner-enable-in-fullscreen')
51 | ]);
52 |
53 | // Lock Screen
54 | this.addGroup(_('Lock Screen'), [
55 | this.createSwitch(_('Force primary input source on Lock Screen'), 'lockscreen-primary-input',
56 | _('Experimental feature'))
57 | ]);
58 |
59 | // Switcher Popups
60 | this._addSwitcherPopupOptions();
61 | }
62 |
63 | _addTaskbarOptions() {
64 |
65 | const activateBehaviorOptions = [
66 | { label: _('New window'), value: 'new_window' },
67 | { label: _('Move windows'), value: 'move_windows' },
68 | ];
69 |
70 | this.addVisibilityControl([this.addGroup(_('Taskbar'), [
71 | this.createSwitch(_('Enable Drag and Drop'), 'appbutton-enable-drag-and-drop',
72 | _('Reorder apps in the taskbar using Drag and Drop')),
73 | this.createSwitch(_('Enable Minimize action'), 'appbutton-enable-minimize-action',
74 | _('Allow to minimize single app windows by clicking apps in the taskbar')),
75 | this.createSwitch(_('Require click to open context menus'), 'appbutton-menu-require-click'),
76 | ...this.addVisibilityControl([
77 | this.createSwitch(_('Middle click to toggle app sound mute'), 'appbutton-middle-button-sound-mute',
78 | _('By default Middle click is used to open new app windows and to close the first app window when Ctrl is pressed')),
79 | this.createSwitch(_('Scroll to change app sound volume'), 'appbutton-scroll-change-sound-volume')
80 | ], { 'appbutton-enable-sound-control': value => value }),
81 | ...this.addVisibilityControl(
82 | [this.createSwitch(_('Scroll to cycle app windows'), 'appbutton-enable-scroll')], {
83 | 'appbutton-enable-sound-control': null,
84 | 'appbutton-scroll-change-sound-volume': value => (
85 | this._settings.get_boolean('appbutton-enable-sound-control') ?
86 | !value : true
87 | )
88 | }),
89 | ...this.addVisibilityControl([this.createPicklist(
90 | _('Running apps activation behavior'), 'appbutton-running-app-activate-behavior',
91 | activateBehaviorOptions,
92 | _('Controls the behavior when an app is running but has no windows on the active workspace, supports isolated workspaces only, ' +
93 | 'can be configured separately for each app via an app menu')
94 | )], { 'taskbar-isolate-workspaces': value => value })
95 | ])], { 'taskbar-enabled': value => value });
96 | }
97 |
98 | _addPanelOptions() {
99 |
100 | const scrollActionOptions = [
101 | { label: _('None'), value: 'none' },
102 | { label: _('Change Sound Volume'), value: 'change_sound_volume' },
103 | { label: _('Switch Workspace'), value: 'switch_workspace' }
104 | ];
105 |
106 | this.addGroup(_('Panel'), [
107 | this.createSwitch(_('Require click to activate menu buttons'), 'panel-menu-require-click'),
108 | this.createSwitch(_('Middle click to toggle sound mute'), 'panel-enable-middle-button',
109 | _('Press middle button on an empty space of the panel')),
110 | this.createPicklist(_('Scroll action'), 'panel-scroll-action', scrollActionOptions)
111 | ]);
112 | }
113 |
114 | _addSoundControlOptions() {
115 |
116 | const volumeChangeSpeedOptions = [
117 | { label: _('Slowest'), value: 1 },
118 | { label: _('Slow'), value: 2 },
119 | { label: _('Normal'), value: 4 },
120 | { label: _('Fast'), value: 6 },
121 | { label: _('Faster'), value: 8 },
122 | { label: _('Turbo'), value: 10 }
123 | ];
124 |
125 | this.addVisibilityControl([this.addGroup(_('Sound Volume Control'), [
126 | this.createPicklist(
127 | _('Volume change speed'), 'sound-volume-control-change-speed',
128 | volumeChangeSpeedOptions
129 | ),
130 | this.createPicklist(
131 | _('Volume change speed when Ctrl pressed'), 'sound-volume-control-change-speed-ctrl',
132 | volumeChangeSpeedOptions
133 | )
134 | ])], {
135 | 'taskbar-enabled': null,
136 | 'appbutton-enable-sound-control': null,
137 | 'appbutton-scroll-change-sound-volume': value => (
138 | this._settings.get_boolean('taskbar-enabled') &&
139 | this._settings.get_boolean('appbutton-enable-sound-control') ?
140 | value : false
141 | ),
142 | 'panel-scroll-action': value => value === 'change_sound_volume'
143 | });
144 | }
145 |
146 | _addActivitiesOptions() {
147 |
148 | const clickOptions = [
149 | { label: _('None'), value: 'none' },
150 | { label: _('Left Button'), value: 'left_button' },
151 | { label: _('Right Button'), value: 'right_button' },
152 | { label: _('Middle Button'), value: 'middle_button' }
153 | ];
154 |
155 | this.addGroup(_('Activities'), [
156 | this.createPicklist(
157 | _('Click Activities to show the App Grid'), 'activities-show-apps-button',
158 | clickOptions
159 | )
160 | ]);
161 | }
162 |
163 | _addSwitcherPopupOptions() {
164 | this.addGroup(_('Switcher Popups'), [
165 | this.createSwitch(_('Override show delay'), 'switcherpopup-enable-show-delay'),
166 | ...this.addVisibilityControl([
167 | this.createSpinButton(
168 | _('Show Delay'), 'switcherpopup-show-delay',
169 | { min: 0, max: 500, step: 50 }
170 | )
171 | ], { 'switcherpopup-enable-show-delay': value => value }),
172 | this.createSwitch(_('Do not grab focus'), 'switcherpopup-enable-handler'),
173 | ]);
174 | }
175 |
176 | }
177 | );
178 |
--------------------------------------------------------------------------------
/extension/settings/customizePage.js:
--------------------------------------------------------------------------------
1 | import GObject from 'gi://GObject';
2 | import { gettext as _ } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
3 |
4 | import { SettingsPageTemplate } from './pageTemplate.js';
5 |
6 | export const CustomizePage = GObject.registerClass(
7 | class Rocketbar__CustomizePage extends SettingsPageTemplate {
8 |
9 | _init(settings) {
10 |
11 | super._init({
12 | title: _('Customize'),
13 | name: 'CustomizePage',
14 | icon: 'applications-utilities-symbolic',
15 | settings: settings
16 | });
17 |
18 | this._options = [];
19 |
20 | this._emptyMessage = this.addGroup(null, [
21 | this.createMessage(_('No customizations available'))
22 | ]);
23 |
24 | this._populateOptions();
25 |
26 | this._toggleEmptyMessage();
27 | }
28 |
29 | _populateOptions() {
30 |
31 | // create sections in the order we want to see them on the UI
32 | const taskbarOptions = this._addTaskbarOptions();
33 | const appButtonOptions = this._addAppButtonOptions();
34 | const indicatorOptions = this._addIndicatorOptions();
35 | const notificationBadgeOptions = this._addNotificationBadgeOptions();
36 | const tooltipOptions = this._addTooltipOptions();
37 |
38 | this._options = [...this._options, ...this.addVisibilityControl([
39 | taskbarOptions,
40 | appButtonOptions,
41 | indicatorOptions,
42 | notificationBadgeOptions,
43 | tooltipOptions
44 | ], {
45 | 'taskbar-enabled': value => value,
46 | 'appbutton-enable-indicators': null,
47 | 'appbutton-enable-notification-badges': null,
48 | 'appbutton-enable-tooltips': null
49 | }, option => {
50 |
51 | if (!option) {
52 | this._toggleEmptyMessage();
53 | return;
54 | }
55 |
56 | if (!option.visible) {
57 | return;
58 | }
59 |
60 | let settingsKey = null;
61 |
62 | if (option === indicatorOptions) {
63 | settingsKey = 'appbutton-enable-indicators';
64 | } else if (option === notificationBadgeOptions) {
65 | settingsKey = 'appbutton-enable-notification-badges';
66 | } else if (option === tooltipOptions) {
67 | settingsKey = 'appbutton-enable-tooltips';
68 | } else return;
69 |
70 | option.visible = this._settings.get_boolean(settingsKey);
71 |
72 | }), ...this.addVisibilityControl([
73 | this._addNotificationCounterOptions()
74 | ], {
75 | 'notification-counter-enabled': value => value
76 | }, option => {
77 | if (!option) {
78 | this._toggleEmptyMessage();
79 | }
80 | })];
81 | }
82 |
83 | _addTaskbarOptions() {
84 |
85 | const positionOptions = [
86 | { label: _('Left'), value: 'left' },
87 | { label: _('Center'), value: 'center' },
88 | { label: _('Right'), value: 'right' }
89 | ];
90 |
91 | return this.addGroup(_('Taskbar'), [
92 | this.createPicklist(
93 | _('Position'), 'taskbar-position',
94 | positionOptions
95 | ),
96 | this.createSpinButton(
97 | _('Position Offset'), 'taskbar-position-offset',
98 | { min: 0, max: 15 }
99 | ),
100 | this.createSwitch(_('Preserve Position'), 'taskbar-preserve-position',
101 | _('Prevent position changes caused by other extensions in the panel'))
102 | ]);
103 | }
104 |
105 | _addAppButtonOptions() {
106 |
107 | const backlightColorRow = this.createColorButton(_('Backlight Color'), 'appbutton-backlight-color');
108 | backlightColorRow.activatable_widget.use_alpha = false;
109 |
110 | return this.addGroup(_('App Buttons'), [
111 | this.createSlider(
112 | _('Icon Size'), 'appbutton-icon-size',
113 | { min: 16, max: 64, marks: [16, 24, 32, 48, 64] },
114 | _('Can be configured separately for each app via an app menu')
115 | ),
116 | this.createSpinButton(
117 | _('Icon Horizontal Padding'), 'appbutton-icon-padding',
118 | { min: 0, max: 20 }
119 | ),
120 | this.createSpinButton(
121 | _('Icon Vertical Padding'), 'appbutton-icon-vertical-padding',
122 | { min: 0, max: 20 }
123 | ),
124 | this.createSpinButton(
125 | _('Roundness'), 'appbutton-roundness',
126 | { min: 0, max: 100 }
127 | ),
128 | this.createSpinButton(
129 | _('Spacing'), 'appbutton-spacing',
130 | { min: 0, max: 10 }
131 | ),
132 | this.createSwitch(_('Dominant Color Backlight'), 'appbutton-backlight-dominant-color'),
133 | ...this.addVisibilityControl([
134 | backlightColorRow
135 | ], { 'appbutton-backlight-dominant-color': value => !value }),
136 | this.createSpinButton(
137 | _('Backlight Intensity'), 'appbutton-backlight-intensity',
138 | { min: 0, max: 9 }
139 | )
140 | ]);
141 | }
142 |
143 | _addIndicatorOptions() {
144 |
145 | const positionOptions = [
146 | { label: _('Top'), value: 'top' },
147 | { label: _('Bottom'), value: 'bottom' }
148 | ];
149 |
150 | return this.addGroup(_('Indicators'), [
151 | this.createPicklist(
152 | _('Position'), 'indicator-position',
153 | positionOptions
154 | ),
155 | this.createSpinButton(
156 | _('Limit'), 'indicator-display-limit',
157 | { min: 1, max: 5 },
158 | _('The maximum number of indicators to display on top of app buttons')
159 | ),
160 | this.createSwitch(_('Active Dominant Color'), 'indicator-dominant-color-active'),
161 | ...this.addVisibilityControl([
162 | this.createColorButton(_('Active Color'), 'indicator-color-active')
163 | ], { 'indicator-dominant-color-active': value => !value }),
164 | this.createSwitch(_('Inactive Dominant Color'), 'indicator-dominant-color-inactive'),
165 | ...this.addVisibilityControl([
166 | this.createColorButton(_('Inactive Color'), 'indicator-color-inactive')
167 | ], { 'indicator-dominant-color-inactive': value => !value }),
168 |
169 | this.createSpinButton(
170 | _('Inactive Width'), 'indicator-width-inactive',
171 | { min: 1, max: 100 }
172 | ),
173 | this.createSpinButton(
174 | _('Active Width'), 'indicator-width-active',
175 | { min: 1, max: 100 }
176 | ),
177 | this.createSpinButton(
178 | _('Inactive Height'), 'indicator-height-inactive',
179 | { min: 1, max: 100 }
180 | ),
181 | this.createSpinButton(
182 | _('Active Height'), 'indicator-height-active',
183 | { min: 1, max: 100 }
184 | ),
185 | this.createSpinButton(
186 | _('Inactive Roundness'), 'indicator-roundness-inactive',
187 | { min: 0, max: 100 }
188 | ),
189 | this.createSpinButton(
190 | _('Active Roundness'), 'indicator-roundness-active',
191 | { min: 0, max: 100 }
192 | ),
193 | ...this.addVisibilityControl([
194 | this.createSpinButton(
195 | _('Inactive Spacing'), 'indicator-spacing-inactive',
196 | { min: 0, max: 50 }
197 | ),
198 | this.createSpinButton(
199 | _('Active Spacing'), 'indicator-spacing-active',
200 | { min: 0, max: 50 }
201 | )
202 | ], { 'indicator-display-limit': value => value > 1 })
203 | ]);
204 | }
205 |
206 | _addNotificationBadgeOptions() {
207 |
208 | const positionOptions = [
209 | { label: _('Top Left'), value: 'top_left' },
210 | { label: _('Top Right'), value: 'top_right' },
211 | { label: _('Bottom Left'), value: 'bottom_left' },
212 | { label: _('Bottom Right'), value: 'bottom_right' }
213 | ];
214 |
215 | return this.addGroup(_('Notification Badges'), [
216 | this.createColorButton(_('Color'), 'notification-badge-color'),
217 | this.createColorButton(_('Border Color'), 'notification-badge-border-color'),
218 | this.createPicklist(
219 | _('Position'), 'notification-badge-position',
220 | positionOptions
221 | ),
222 | this.createSpinButton(
223 | _('Size'), 'notification-badge-size',
224 | { min: 2, max: 10 }
225 | ),
226 | this.createSpinButton(
227 | _('Margin'), 'notification-badge-margin',
228 | { min: 0, max: 15 }
229 | )
230 | ]);
231 | }
232 |
233 | _addTooltipOptions() {
234 | return this.addGroup(_('Tooltips'), [
235 | this.createSpinButton(
236 | _('Show Delay'), 'tooltip-show-delay',
237 | { min: 100, max: 2000, step: 100 }
238 | ),
239 | this.createSpinButton(
240 | _('Max Width'), 'tooltip-max-width',
241 | { min: 200, max: 1000, step: 50 }
242 | )
243 | ]);
244 | }
245 |
246 | _addNotificationCounterOptions() {
247 | return this.addGroup(_('Notification Counter'), [
248 | this.createSwitch(_('Hide when empty'), 'notification-counter-hide-empty'),
249 | this.createSwitch(_('Center clock'), 'notification-counter-center-clock'),
250 | this.createSpinButton(
251 | _('Max Count'), 'notification-counter-max-count',
252 | { min: 1, max: 999 }
253 | ),
254 | this.createSpinButton(
255 | _('Font Size'), 'notification-counter-font-size',
256 | { min: 8, max: 20 }
257 | ),
258 | this.createSpinButton(
259 | _('Roundness'), 'notification-counter-roundness',
260 | { min: 0, max: 50 }
261 | ),
262 | this.createSpinButton(
263 | _('Top Margin'), 'notification-counter-margin-top',
264 | { min: 0, max: 10 }
265 | ),
266 | ...this.addVisibilityControl([
267 | this.createColorButton(_('Empty Color'), 'notification-counter-color-empty')
268 | ], { 'notification-counter-hide-empty': value => !value }),
269 | this.createColorButton(_('Not Empty Color'), 'notification-counter-color-not-empty'),
270 | this.createColorButton(_('Text Color'), 'notification-counter-text-color'),
271 | ...this.addVisibilityControl([
272 | this.createColorButton(_('Do Not Disturb - Empty Color'), 'notification-counter-color-empty-dnd'),
273 | ], { 'notification-counter-hide-empty': value => !value }),
274 | this.createColorButton(_('Do Not Disturb - Not Empty Color'), 'notification-counter-color-not-empty-dnd'),
275 | this.createColorButton(_('Do Not Disturb - Text Color'), 'notification-counter-text-color-dnd'),
276 | ]);
277 | }
278 |
279 | _toggleEmptyMessage() {
280 |
281 | if (!this._options.length) {
282 | return;
283 | }
284 |
285 | const visibleOptions = this._options.filter(option => option.visible);
286 |
287 | if (visibleOptions.length) {
288 | this._emptyMessage.hide();
289 | return;
290 | }
291 |
292 | this._emptyMessage.show();
293 | }
294 |
295 | }
296 | );
297 |
--------------------------------------------------------------------------------
/extension/settings/generalPage.js:
--------------------------------------------------------------------------------
1 | import GObject from 'gi://GObject';
2 | import { gettext as _ } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
3 |
4 | import { SettingsPageTemplate } from './pageTemplate.js';
5 |
6 | export const GeneralPage = GObject.registerClass(
7 | class Rocketbar__GeneralPage extends SettingsPageTemplate {
8 |
9 | _init(settings) {
10 |
11 | super._init({
12 | title: _('General'),
13 | name: 'GeneralPage',
14 | icon: 'preferences-system-symbolic',
15 | settings: settings
16 | });
17 |
18 | this._populateOptions();
19 | }
20 |
21 | _populateOptions() {
22 |
23 | // Taskbar
24 | this._addTaskbarOptions();
25 |
26 | // Notification Counter
27 | this.addGroup(_('Notification Counter'), [
28 | this.createSwitch(_('Enabled'), 'notification-counter-enabled')
29 | ]);
30 |
31 | // Overview
32 | this._addOverviewOptions();
33 |
34 | }
35 |
36 | _addTaskbarOptions() {
37 | this.addGroup(_('Taskbar'), [
38 | this.createSwitch(_('Enabled'), 'taskbar-enabled'),
39 | ...this.addVisibilityControl([
40 | this.createSwitch(_('Show Favorites'), 'taskbar-show-favorites'),
41 | this.createSwitch(_('Isolate Workspaces'), 'taskbar-isolate-workspaces'),
42 | this.createSwitch(_('Enable Indicators'), 'appbutton-enable-indicators'),
43 | this.createSwitch(_('Enable Notification Badges'), 'appbutton-enable-notification-badges'),
44 | this.createSwitch(_('Enable Tooltips'), 'appbutton-enable-tooltips'),
45 | this.createSwitch(_('Enable Sound Volume Control'), 'appbutton-enable-sound-control',
46 | _('Experimental feature'))
47 | ], { 'taskbar-enabled': value => value })
48 | ]);
49 | }
50 |
51 | _addOverviewOptions() {
52 | this.addGroup(_('Overview'), [
53 | this.createSwitch(_('Kill the Dash'), 'overview-kill-dash',
54 | _('Hide the Dash from Overview and prevent it from rerendering behind the scene'))
55 | ]);
56 | }
57 |
58 | }
59 | );
60 |
--------------------------------------------------------------------------------
/extension/settings/pageTemplate.js:
--------------------------------------------------------------------------------
1 | import Adw from 'gi://Adw';
2 | import Gdk from 'gi://Gdk';
3 | import GLib from 'gi://GLib';
4 | import GObject from 'gi://GObject';
5 | import Gtk from 'gi://Gtk';
6 |
7 | export const SettingsPageTemplate = GObject.registerClass(
8 | class Rocketbar__SettingsPageTemplate extends Adw.PreferencesPage {
9 |
10 | _init(params) {
11 | super._init({
12 | title: params.title,
13 | name: params.name,
14 | icon_name: params.icon
15 | });
16 | this._settings = params.settings;
17 | }
18 |
19 | addGroup(title, options) {
20 |
21 | const newGroup = new Adw.PreferencesGroup({
22 | title: title
23 | });
24 |
25 | this.add(newGroup);
26 |
27 | options?.forEach(option => newGroup.add(option));
28 |
29 | return newGroup;
30 | }
31 |
32 | createSwitch(title, settingsKey, subtitle) {
33 |
34 | const newSwitch = new Gtk.Switch({
35 | valign: Gtk.Align.CENTER
36 | });
37 |
38 | newSwitch.set_active(this._settings.get_boolean(settingsKey));
39 |
40 | newSwitch.connect('notify::active', (widget) => {
41 | this._settings.set_boolean(settingsKey, widget.get_active());
42 | });
43 |
44 | const switchRow = new Adw.ActionRow({
45 | title: title,
46 | subtitle: subtitle ? subtitle : null,
47 | activatable_widget: newSwitch
48 | });
49 |
50 | switchRow.add_suffix(newSwitch);
51 |
52 | return switchRow;
53 | }
54 |
55 | createPicklist(title, settingsKey, options, subtitle) {
56 |
57 | let stringOptions = false;
58 | let values = [];
59 | let picklistOptions = new Gtk.StringList();
60 |
61 | options.forEach((option) => {
62 |
63 | if (!stringOptions) {
64 | stringOptions = !Number.isInteger(option.value);
65 | }
66 |
67 | values.push(option.value);
68 |
69 | picklistOptions.append(option.label)
70 | });
71 |
72 | const selectedIndex = values.indexOf(
73 | stringOptions ?
74 | this._settings.get_string(settingsKey) :
75 | this._settings.get_int(settingsKey)
76 | );
77 |
78 | const picklistRow = new Adw.ComboRow({
79 | title: title,
80 | subtitle: subtitle ? subtitle : null,
81 | model: picklistOptions,
82 | selected: selectedIndex >= 0 ? selectedIndex : 0
83 | });
84 |
85 | picklistRow.connect("notify::selected", (widget) => {
86 |
87 | const value = values[widget.selected];
88 |
89 | if (stringOptions) {
90 | this._settings.set_string(settingsKey, value);
91 | return;
92 | }
93 |
94 | this._settings.set_int(settingsKey, value);
95 | });
96 |
97 | return picklistRow;
98 | }
99 |
100 | createSpinButton(title, settingsKey, params, subtitle) {
101 |
102 | const spinButton = new Gtk.SpinButton({
103 | adjustment: new Gtk.Adjustment({
104 | lower: params.min || 0,
105 | upper: params.max || 0,
106 | step_increment: params.step || 1
107 | }),
108 | climb_rate: 1,
109 | digits: 0,
110 | numeric: true,
111 | valign: Gtk.Align.CENTER
112 | });
113 |
114 | spinButton.set_value(this._settings.get_int(settingsKey));
115 |
116 | spinButton.connect('value-changed', (widget) => {
117 |
118 | if (spinButton._changeTimeout) {
119 | GLib.source_remove(spinButton._changeTimeout);
120 | }
121 |
122 | spinButton._changeTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 300, () => {
123 | this._settings.set_int(settingsKey, widget.get_value());
124 | spinButton._changeTimeout = null;
125 | return GLib.SOURCE_REMOVE;
126 | });
127 |
128 | });
129 |
130 | const spinButtonRow = new Adw.ActionRow({
131 | title: title,
132 | subtitle: subtitle ? subtitle : null,
133 | activatable_widget: spinButton
134 | });
135 |
136 | spinButtonRow.add_suffix(spinButton);
137 |
138 | return spinButtonRow;
139 | }
140 |
141 | createSlider(title, settingsKey, params, subtitle) {
142 |
143 | const slider = new Gtk.Scale({
144 | adjustment: new Gtk.Adjustment({
145 | lower: params.min || 0,
146 | upper: params.max || 1,
147 | step_increment: params.step || 1
148 | }),
149 | digits: 0,
150 | hexpand: true,
151 | draw_value: true,
152 | value_pos: (
153 | params.marks && params.marks.length ?
154 | Gtk.PositionType.BOTTOM :
155 | Gtk.PositionType.RIGHT
156 | ),
157 | valign: Gtk.Align.CENTER
158 | });
159 |
160 | if (params.marks) {
161 | params.marks.forEach(mark => slider.add_mark(mark, Gtk.PositionType.TOP, mark.toString()));
162 | }
163 |
164 | slider.width_request = 200;
165 |
166 | slider.set_value(this._settings.get_int(settingsKey));
167 |
168 | slider.connect('value-changed', (widget) => {
169 |
170 | if (slider._changeTimeout) {
171 | GLib.source_remove(slider._changeTimeout);
172 | }
173 |
174 | slider._changeTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 300, () => {
175 | this._settings.set_int(settingsKey, widget.get_value());
176 | slider._changeTimeout = null;
177 | return GLib.SOURCE_REMOVE;
178 | });
179 |
180 | });
181 |
182 | const sliderRow = new Adw.ActionRow({
183 | title: title,
184 | subtitle: subtitle ? subtitle : null,
185 | activatable_widget: slider
186 | });
187 |
188 | sliderRow.add_suffix(slider);
189 |
190 | return sliderRow;
191 | }
192 |
193 | createColorButton(title, settingsKey, subtitle) {
194 |
195 | const color = new Gdk.RGBA();
196 | color.parse(this._settings.get_string(settingsKey));
197 |
198 | const colorButton = new Gtk.ColorButton({
199 | rgba: color,
200 | use_alpha: true,
201 | valign: Gtk.Align.CENTER
202 | });
203 |
204 | colorButton.connect('color-set', (widget) => {
205 | this._settings.set_string(settingsKey, widget.get_rgba().to_string());
206 | });
207 |
208 | const colorButtonRow = new Adw.ActionRow({
209 | title: title,
210 | subtitle: subtitle ? subtitle : null,
211 | activatable_widget: colorButton
212 | });
213 |
214 | colorButtonRow.add_suffix(colorButton);
215 |
216 | return colorButtonRow;
217 | }
218 |
219 | createLink(title, url) {
220 |
221 | const link = new Gtk.LinkButton({
222 | uri: url,
223 | opacity: 0
224 | });
225 |
226 | const linkRow = new Adw.ActionRow({
227 | title: title,
228 | activatable_widget: link
229 | });
230 |
231 | linkRow.add_suffix(link);
232 |
233 | return linkRow;
234 | }
235 |
236 | createLabel(title, text) {
237 |
238 | const label = new Gtk.Label({
239 | label: text
240 | });
241 |
242 | const labelRow = new Adw.ActionRow({
243 | title: title
244 | });
245 |
246 | labelRow.add_suffix(label);
247 |
248 | return labelRow;
249 | }
250 |
251 | createMessage(text) {
252 | return new Gtk.Label({
253 | label: `${text}`,
254 | use_markup: true,
255 | vexpand: true,
256 | valign: Gtk.Align.FILL
257 | });
258 | }
259 |
260 | addVisibilityControl(widgets = [], settingsKeys = {}, callback) {
261 |
262 | if (!settingsKeys || !widgets.length) {
263 | return widgets;
264 | }
265 |
266 | const updateVisibility = () => {
267 |
268 | let visibilityState = false;
269 |
270 | for (let settingsKey in settingsKeys) {
271 |
272 | const keyHandler = settingsKeys[settingsKey];
273 |
274 | if (!keyHandler) {
275 | continue;
276 | }
277 |
278 | let value = this._settings.get_value(settingsKey);
279 |
280 | // only boolean and string values are supported for now
281 | switch (value.get_type_string()) {
282 | case 'b':
283 | value = this._settings.get_boolean(settingsKey);
284 | break;
285 | case 's':
286 | value = this._settings.get_string(settingsKey);
287 | break;
288 | case 'i':
289 | value = this._settings.get_int(settingsKey);
290 | break;
291 | }
292 |
293 | if (keyHandler(value)) {
294 | visibilityState = true;
295 | break;
296 | }
297 | }
298 |
299 | widgets.forEach(widget => {
300 |
301 | widget.visible = visibilityState
302 |
303 | if (callback) {
304 | callback(widget);
305 | }
306 |
307 | });
308 |
309 | if (callback) {
310 | callback();
311 | }
312 | };
313 |
314 | updateVisibility();
315 |
316 | for (let settingsKey in settingsKeys) {
317 | this._settings.connect(`changed::${settingsKey}`, updateVisibility);
318 | }
319 |
320 | return widgets;
321 | }
322 |
323 | }
324 | );
325 |
--------------------------------------------------------------------------------
/extension/stylesheet.css:
--------------------------------------------------------------------------------
1 | /* button */
2 |
3 | .rocketbar__button {
4 | border-width: 0;
5 | }
6 | .rocketbar__button:hover,
7 | .rocketbar__button:focus,
8 | .rocketbar__button:active {
9 | background: rgba(0, 0, 0, 0.01);
10 | }
11 |
12 | /* tooltip */
13 |
14 | .rocketbar__tooltip {
15 | border: 1px solid rgba(255, 255, 255, 0.1);
16 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
17 | }
18 | .rocketbar__tooltip
19 | .rocketbar__tooltip_counter {
20 | margin-left: 8px;
21 | }
22 | .rocketbar__tooltip
23 | .rocketbar__tooltip_counter StIcon {
24 | height: 15px;
25 | width: 15px;
26 | color: rgba(255, 255, 255, 0.85);
27 | }
28 | .rocketbar__tooltip
29 | .rocketbar__tooltip_counter StLabel {
30 | margin-left: 5px;
31 | }
32 |
33 | /* popup menu */
34 |
35 | .rocketbar__popup-menu {
36 | /* shrink menu width as much as possible */
37 | max-width: 0px;
38 | min-width: 300px;
39 | }
40 | .rocketbar__popup-menu
41 | .popup-separator-menu-item {
42 | font-size: 0.8em;
43 | }
44 | .rocketbar__popup-menu
45 | .rocketbar__popup-menu_section-title {
46 | margin-top: 10px;
47 | }
48 | .rocketbar__popup-menu
49 | .popup-menu-icon {
50 | margin-left: 10px;
51 | }
52 |
53 | /* date menu */
54 |
55 | .rocketbar__date-menu {
56 | -natural-hpadding: 0;
57 | -minimum-hpadding: 0;
58 | }
59 |
60 | /* notification counter */
61 |
62 | .rocketbar__notification-counter {
63 | font-weight: bold;
64 | text-align: center;
65 | }
--------------------------------------------------------------------------------
/extension/ui/appButtonIndicator.js:
--------------------------------------------------------------------------------
1 | /* exported AppButtonIndicator */
2 |
3 | //#region imports
4 |
5 | import Clutter from 'gi://Clutter';
6 | import St from 'gi://St';
7 |
8 | // custom modules import
9 | import { Config } from '../utils/config.js';
10 | import { Connections } from '../utils/connections.js';
11 |
12 | //#endregion imports
13 |
14 | export class AppButtonIndicator {
15 |
16 | constructor(appButton, layout, settings) {
17 |
18 | this._appButton = appButton;
19 | this._layout = layout;
20 | this._settings = settings;
21 | this._indicators = null;
22 | this._isActive = false;
23 | this._dominantColor = null;
24 |
25 | this._handleSettings();
26 |
27 | this._createConnections();
28 | }
29 |
30 | //#region public methods
31 |
32 | destroy() {
33 |
34 | this._layout = null;
35 |
36 | this._connections?.destroy();
37 |
38 | this._destroyIndicators();
39 | }
40 |
41 | rerender() {
42 | this._updateIndicators();
43 | }
44 |
45 | updateStyle() {
46 | this._updateIndicatorsStyle();
47 | }
48 |
49 | //#endregion public methods
50 |
51 | //#region private methods
52 |
53 | _createConnections() {
54 | this._connections = new Connections();
55 | this._connections.addScope(this._settings, [
56 | 'changed::indicator-width-inactive',
57 | 'changed::indicator-width-active',
58 | 'changed::indicator-height-inactive',
59 | 'changed::indicator-height-active',
60 | 'changed::indicator-roundness-inactive',
61 | 'changed::indicator-roundness-active',
62 | 'changed::indicator-spacing-inactive',
63 | 'changed::indicator-spacing-active',
64 | 'changed::indicator-dominant-color-active',
65 | 'changed::indicator-dominant-color-inactive',
66 | 'changed::indicator-color-active',
67 | 'changed::indicator-color-inactive',
68 | 'changed::indicator-position',
69 | 'changed::indicator-display-limit'], () => this._handleSettings());
70 | }
71 |
72 | _handleSettings() {
73 |
74 | if (this._config) {
75 | this._config.update();
76 | } else {
77 | this._setConfig();
78 | }
79 |
80 | this._config.handleChanged(['maxIndicators'], () => this.rerender());
81 |
82 | if (!this._config.hasOld()) {
83 | return;
84 | }
85 |
86 | this._config.handleChanged([
87 | 'width',
88 | 'activeWidth',
89 | 'height',
90 | 'activeHeight',
91 | 'roundness',
92 | 'activeRoundness',
93 | 'spacing',
94 | 'activeSpacing',
95 | 'dominantColor',
96 | 'activeDominantColor',
97 | 'color',
98 | 'activeColor',
99 | 'position'
100 | ], () => this._updateIndicatorsStyle());
101 | }
102 |
103 | _setConfig() {
104 | this._config = new Config(() => ({
105 | width: this._settings.get_int('indicator-width-inactive'),
106 | activeWidth: this._settings.get_int('indicator-width-active'),
107 | height: this._settings.get_int('indicator-height-inactive'),
108 | activeHeight: this._settings.get_int('indicator-height-active'),
109 | roundness: this._settings.get_int('indicator-roundness-inactive'),
110 | activeRoundness: this._settings.get_int('indicator-roundness-active'),
111 | spacing: this._settings.get_int('indicator-spacing-inactive'),
112 | activeSpacing: this._settings.get_int('indicator-spacing-active'),
113 | color: this._settings.get_string('indicator-color-inactive'),
114 | activeColor: this._settings.get_string('indicator-color-active'),
115 | position: this._settings.get_string('indicator-position'),
116 | dominantColor: this._settings.get_boolean('indicator-dominant-color-inactive'),
117 | activeDominantColor: this._settings.get_boolean('indicator-dominant-color-active'),
118 | maxIndicators: this._settings.get_int('indicator-display-limit')
119 | }));
120 | }
121 |
122 | _updateIndicators() {
123 |
124 | const oldIsActive = this._isActive;
125 |
126 | // set active state
127 | this._isActive = this._appButton.isActive;
128 |
129 | // no need to display indicators
130 | if (!this._appButton.windows) {
131 | this._destroyIndicators();
132 | return;
133 | }
134 |
135 | // count the maximum number of indicators to display
136 | let maxIndicators = (
137 | this._appButton.windows > this._config.values.maxIndicators ?
138 | this._config.values.maxIndicators :
139 | this._appButton.windows
140 | );
141 |
142 | const indicatorsLength = this._indicators?.length || 0;
143 |
144 | // no need to change indicators
145 | if (indicatorsLength === maxIndicators) {
146 |
147 | if (oldIsActive !== this._isActive) {
148 | this._updateIndicatorsStyle();
149 | }
150 |
151 | return;
152 | }
153 |
154 | // check if some idicators should be destroyed
155 | // this will be executed in case we have more than one indicator
156 | if (indicatorsLength > maxIndicators) {
157 |
158 | let indicatorsToDestroy = this._indicators.splice(maxIndicators, indicatorsLength - maxIndicators);
159 |
160 | for (let i = 0, l = indicatorsToDestroy.length; i < l; ++i) {
161 | this._destroyIndicator(indicatorsToDestroy[i]);
162 | }
163 |
164 | } else {
165 |
166 | // don't create more than we need to display
167 | maxIndicators -= indicatorsLength;
168 |
169 | // create new indicators
170 | for (let i = 0; i < maxIndicators; ++i) {
171 | this._addIndicator();
172 | }
173 | }
174 |
175 | this._updateIndicatorsStyle();
176 | }
177 |
178 | _addIndicator() {
179 |
180 | if (!this._layout) {
181 | return;
182 | }
183 |
184 | if (!this._indicators) {
185 | this._indicators = [];
186 | }
187 |
188 | const indicator = new St.Bin({
189 | name: 'taskbar-appButton-indicator',
190 | x_expand: false,
191 | y_expand: true,
192 | x_align: Clutter.ActorAlign.CENTER,
193 | y_align: Clutter.ActorAlign.START,
194 | opacity: 0
195 | });
196 |
197 | this._indicators.push(indicator);
198 |
199 | this._layout.add_actor(indicator);
200 |
201 | indicator.ease({
202 | opacity: 255,
203 | duration: 300,
204 | mode: Clutter.AnimationMode.EASE_OUT_QUAD
205 | });
206 | }
207 |
208 | _updateIndicatorsStyle() {
209 |
210 | if (!this._indicators?.length) {
211 | return;
212 | }
213 |
214 | const config = this._config.values;
215 |
216 | this._dominantColor = (
217 | (config.dominantColor || config.activeDominantColor) && this._appButton.dominantColor ?
218 | `rgb(${this._appButton.dominantColor.r}, ${this._appButton.dominantColor.g}, ${this._appButton.dominantColor.b})` :
219 | null
220 | );
221 |
222 | const position = (
223 | config.position === 'top' ?
224 | Clutter.ActorAlign.START :
225 | Clutter.ActorAlign.END
226 | );
227 |
228 | for (let i = 0, l = this._indicators.length; i < l; ++i) {
229 | this._indicators[i].style = this._getIndicatorStyle(i);
230 | this._indicators[i].y_align = position;
231 | }
232 | }
233 |
234 | _getIndicatorStyle(index) {
235 |
236 | const config = this._config.values;
237 |
238 | const backgroundColor = (
239 | this._isActive ? (config.activeDominantColor && this._dominantColor ? this._dominantColor : config.activeColor) :
240 | (config.dominantColor && this._dominantColor ? this._dominantColor : config.color)
241 | );
242 |
243 | const [ width, height, roundness, spacing ] = (
244 | this._isActive ? [ config.activeWidth, config.activeHeight, config.activeRoundness, config.activeSpacing ] :
245 | [ config.width, config.height, config.roundness, config.spacing ]
246 | );
247 |
248 |
249 | let result = (
250 | `background-color: ${backgroundColor};` +
251 | `width: ${width}px;` +
252 | `height: ${height}px;` +
253 | `border-radius: ${roundness}px;`
254 | );
255 |
256 | const indicatorsLength = this._indicators?.length || 0;
257 |
258 | // check if no more indicators exist
259 | if (indicatorsLength <= 1) {
260 | return result;
261 | }
262 |
263 | // add margins when multiple idicators exist
264 |
265 | const margin = width + spacing;
266 |
267 | if (index === 0 || index < (indicatorsLength - 1)) {
268 | const marginOffset = indicatorsLength - 1 - index;
269 | result += `margin-right: ${margin * marginOffset}px;`;
270 | }
271 |
272 | if (index > 0) {
273 | result += `margin-left: ${margin * index}px;`;
274 | }
275 |
276 | return result;
277 | }
278 |
279 | _destroyIndicators() {
280 |
281 | if (!this._indicators?.length) {
282 | return;
283 | }
284 |
285 | for (let i = 0, l = this._indicators.length; i < l; ++i) {
286 | this._destroyIndicator(this._indicators[i]);
287 | }
288 |
289 | this._indicators = null;
290 | }
291 |
292 | _destroyIndicator(indicator) {
293 |
294 | if (!indicator) {
295 | return;
296 | }
297 |
298 | indicator.remove_all_transitions();
299 |
300 | // no animation in this case
301 | if (!this._layout) {
302 | indicator.destroy();
303 | indicator = null;
304 | return;
305 | }
306 |
307 | indicator.ease({
308 | opacity: 0,
309 | duration: 200,
310 | mode: Clutter.AnimationMode.EASE_OUT_QUAD,
311 | onComplete: () => {
312 | indicator.destroy();
313 | indicator = null;
314 | }
315 | });
316 | }
317 |
318 | //#endregion private methods
319 |
320 | }
321 |
--------------------------------------------------------------------------------
/extension/ui/appButtonMenu.js:
--------------------------------------------------------------------------------
1 | /* exported AppButtonMenu */
2 |
3 | //#region imports
4 |
5 | import Clutter from 'gi://Clutter';
6 | import St from 'gi://St';
7 |
8 | import * as Main from 'resource:///org/gnome/shell/ui/main.js';
9 | import * as BoxPointer from 'resource:///org/gnome/shell/ui/boxpointer.js';
10 | import { AppMenu } from 'resource:///org/gnome/shell/ui/appMenu.js';
11 | import { Slider } from 'resource:///org/gnome/shell/ui/slider.js';
12 | import { PopupMenuSection,
13 | PopupSeparatorMenuItem,
14 | PopupSubMenuMenuItem,
15 | PopupBaseMenuItem,
16 | Ornament } from 'resource:///org/gnome/shell/ui/popupMenu.js';
17 |
18 | // custom modules import
19 | import { Timeout } from '../utils/timeout.js';
20 |
21 | //#endregion imports
22 |
23 | class SubMenuItem {
24 |
25 | constructor(title, parentMenu) {
26 |
27 | this._updateTimeout = null;
28 |
29 | this.actor = new PopupSubMenuMenuItem(title);
30 |
31 | parentMenu.addMenuItem(this.actor);
32 | parentMenu.addMenuItem(new PopupSeparatorMenuItem());
33 | }
34 |
35 | addMenuItem(menuItem) {
36 | this.actor.menu.addMenuItem(menuItem);
37 | }
38 |
39 | addAction(title, callback) {
40 | return this.actor.menu.addAction(title, callback);
41 | }
42 |
43 | queueUpdate(callback) {
44 |
45 | if (this._updateTimeout || !callback) {
46 | return;
47 | }
48 |
49 | this._updateTimeout = Timeout.redraw().run(() => {
50 |
51 | if (!this._updateTimeout) {
52 | return;
53 | }
54 |
55 | this._updateTimeout = null;
56 |
57 | callback();
58 |
59 | });
60 | }
61 |
62 | destroy() {
63 | this._updateTimeout?.destroy();
64 | this._updateTimeout = null;
65 | }
66 |
67 | }
68 |
69 | class AppButtonMenuBase extends AppMenu {
70 |
71 | constructor(appButton, settings) {
72 |
73 | super(appButton, St.Side.TOP, {
74 | favoritesSection: true,
75 | showSingleWindows: true,
76 | });
77 |
78 | this._appButton = appButton;
79 | this._settings = settings;
80 |
81 | this._hasValidAppId = this._appSystem.lookup_app(this._appButton.appId) !== null;
82 |
83 | this.blockSourceEvents = true;
84 |
85 | // override styles
86 | this.actor.remove_style_class_name('app-menu');
87 | this.actor.add_style_class_name('panel-menu aggregate-menu rocketbar__popup-menu');
88 |
89 | this._updateDetailsVisibility();
90 |
91 | Main.panel.menuManager.addMenu(this);
92 | }
93 |
94 | destroy() {
95 |
96 | this.close(false);
97 |
98 | this._app = null;
99 |
100 | super.destroy();
101 | }
102 |
103 | open() {
104 |
105 | if (!this._config) {
106 |
107 | this._setConfig();
108 |
109 | this.setApp(this._appButton.app);
110 |
111 | Main.uiGroup.add_actor(this.actor);
112 |
113 | } else {
114 | this._handleSettings();
115 | }
116 |
117 | // set correct position
118 | this._setPosition();
119 |
120 | // animate open by default
121 | super.open(BoxPointer.PopupAnimation.FULL);
122 | }
123 |
124 |
125 | _handleSettings() {
126 | const oldConfig = this._config || {};
127 |
128 | this._setConfig();
129 |
130 | if (oldConfig.isolateWorkspaces !== this._config.isolateWorkspaces) {
131 | this._updateWindowsSection();
132 | }
133 |
134 | if (oldConfig.showFavorites !== this._config.showFavorites) {
135 | this._updateFavoriteItem();
136 | }
137 | }
138 |
139 | _setConfig() {
140 | this._config = {
141 | showFavorites: this._settings.get_boolean('taskbar-show-favorites'),
142 | isolateWorkspaces: this._settings.get_boolean('taskbar-isolate-workspaces')
143 | };
144 | }
145 |
146 | _setPosition() {
147 |
148 | const [x, y] = this._appButton.get_transformed_position();
149 |
150 | // set position based on location of app button
151 | this.actor._arrowSide = (
152 | y < 100 ?
153 | St.Side.TOP :
154 | St.Side.BOTTOM
155 | );
156 | }
157 |
158 | //#region default methods override
159 |
160 | _onKeyPress(actor, event) {
161 |
162 | let key = event?.get_key_symbol();
163 |
164 | // Space and Return keys are reserved for the app button
165 | if (key === Clutter.KEY_space || key === Clutter.KEY_Return) {
166 | return Clutter.EVENT_PROPAGATE;
167 | }
168 |
169 | return super._onKeyPress(actor, event);
170 | }
171 |
172 | _updateFavoriteItem() {
173 | super._updateFavoriteItem();
174 |
175 | if (!this._toggleFavoriteItem.visible) {
176 | return;
177 | }
178 |
179 | if (!this._config.showFavorites) {
180 | this._toggleFavoriteItem.hide();
181 | return;
182 | }
183 |
184 | const isFavorite = this._appFavorites.isFavorite(this._app.id);
185 |
186 | if (this._config.showFavorites && !isFavorite) {
187 | this._toggleFavoriteItem.label.text = _('Pin');
188 | }
189 | }
190 |
191 | _updateWindowsSection() {
192 |
193 | if (!this._app) {
194 | return;
195 | }
196 |
197 | if (!this._config.isolateWorkspaces) {
198 | super._updateWindowsSection();
199 | return;
200 | }
201 |
202 | // show windows from the current workspace only
203 | // using a trick to avoid complete overriding of the method
204 |
205 | const originalApp = this._app;
206 |
207 | const workspaceIndex = global.workspace_manager.get_active_workspace_index();
208 |
209 | this._app = {
210 | get_windows: () => originalApp.get_windows().filter(window => window.get_workspace().index() === workspaceIndex),
211 | get_name: () => originalApp.get_name()
212 | };
213 |
214 | super._updateWindowsSection();
215 |
216 | this._app = originalApp;
217 | }
218 |
219 | _updateDetailsVisibility() {
220 |
221 | // hide the item for apps without valid app Id
222 | // For example: OpenOffice DesktopEditors
223 | if (!this._hasValidAppId) {
224 | this._detailsItem.visible = false;
225 | return;
226 | }
227 |
228 | super._updateDetailsVisibility();
229 | }
230 |
231 | //#endregion default methods override
232 |
233 | }
234 |
235 | export class AppButtonMenu extends AppButtonMenuBase {
236 |
237 | //#region public methods
238 |
239 | constructor(appButton, settings, iconProvider) {
240 | super(appButton, settings);
241 |
242 | this._iconProvider = iconProvider;
243 |
244 | this._addSoundControlSection();
245 |
246 | this._addCustomizeSection();
247 |
248 | // move Quit item to the end of the app menu
249 | this.moveMenuItem(this._quitItem, this.numMenuItems);
250 | }
251 |
252 | destroy() {
253 |
254 | this._soundControlSection?.destroy();
255 |
256 | this._customizeSection?.destroy();
257 |
258 | this._stopApplyConfigOverride();
259 |
260 | super.destroy();
261 | }
262 |
263 | open() {
264 | this._updateSountControlSection();
265 |
266 | super.open();
267 | }
268 |
269 |
270 | setApp(app) {
271 | super.setApp(app);
272 |
273 | if (!app) {
274 | return;
275 | }
276 |
277 | this._customizeSection?.queueUpdate(() => this._populateCustomizeSection());
278 | }
279 |
280 | //#endregion public methods
281 |
282 | //#region private methods
283 |
284 | _handleSettings() {
285 | super._handleSettings();
286 |
287 | this._customizeSection?.queueUpdate(() => this._updateCustomizeSection());
288 | }
289 |
290 | _setConfig() {
291 | super._setConfig();
292 |
293 | this._config.defaultIconSize = this._settings.get_int('appbutton-icon-size');
294 | this._config.configOverride = this._appButton.configOverride.get();
295 | }
296 |
297 | _addSoundControlSection() {
298 | this._soundControlSection = new SubMenuItem(_('Sound Volume Control'), this);
299 |
300 | [this._soundOutputSliderItem, this._soundOutputSlider] = this._createSoundSliderMenuItem();
301 | [this._soundInputSliderItem, this._soundInputSlider] = this._createSoundSliderMenuItem(true);
302 |
303 | this._soundControlSection.addMenuItem(this._soundOutputSliderItem);
304 | this._soundControlSection.addMenuItem(this._soundInputSliderItem);
305 | }
306 |
307 | _addCustomizeSection() {
308 |
309 | // Don't allow customizations for app buttons without valid app Id
310 | // For example: OnlyOffice creates a separete window called DesktopEditors
311 | // This window always has some random app Id
312 | // and there is no simple way to workaround that weird behavior
313 | if (!this._hasValidAppId) {
314 | return;
315 | }
316 |
317 | // add Customize section to the app menu
318 | this._customizeSection = new SubMenuItem(_('Customize'), this);
319 | }
320 |
321 | _createSoundSliderMenuItem(isInput) {
322 | const menuItem = new PopupBaseMenuItem({
323 | activate: false
324 | });
325 |
326 | menuItem.setOrnament(Ornament.HIDDEN);
327 |
328 | menuItem.add_child(new St.Icon({
329 | icon_name: (
330 | isInput ?
331 | 'audio-input-microphone-symbolic' :
332 | 'audio-speakers-symbolic'
333 | ),
334 | style_class: 'popup-menu-icon'
335 | }));
336 |
337 | const slider = this._createSlider(menuItem, () => {
338 |
339 | if (!this._appButton.soundVolumeControl ||
340 | this._soundControlSection._isUpdating) {
341 | return;
342 | }
343 |
344 | if (isInput) {
345 | this._appButton.soundVolumeControl.setInputVolume(slider.value);
346 | } else {
347 | this._appButton.soundVolumeControl.setOutputVolume(slider.value);
348 | }
349 |
350 | });
351 |
352 | menuItem.add_child(slider);
353 |
354 | return [menuItem, slider];
355 | }
356 |
357 | _updateSountControlSection() {
358 |
359 | this._soundControlSection.actor.hide();
360 |
361 | if (!this._appButton.soundVolumeControl) {
362 | return;
363 | }
364 |
365 | if (this._appButton.soundVolumeControl.hasOutput()) {
366 | this._soundControlSection.actor.show();
367 | this._soundOutputSliderItem.show();
368 | } else {
369 | this._soundOutputSliderItem.hide();
370 | }
371 |
372 | if (this._appButton.soundVolumeControl.hasInput()) {
373 | this._soundControlSection.actor.show();
374 | this._soundInputSliderItem.show();
375 | } else {
376 | this._soundInputSliderItem.hide();
377 | }
378 |
379 | if (!this._soundControlSection.actor.visible) {
380 | return;
381 | }
382 |
383 | this._soundControlSection.queueUpdate(() => {
384 |
385 | if (!this._appButton.soundVolumeControl) {
386 | return;
387 | }
388 |
389 | this._soundControlSection._isUpdating = true;
390 |
391 | if (this._soundOutputSliderItem.visible) {
392 | this._soundOutputSlider.value = this._appButton.soundVolumeControl.getOutputVolume();
393 | }
394 |
395 | if (this._soundInputSliderItem.visible) {
396 | this._soundInputSlider.value = this._appButton.soundVolumeControl.getInputVolume();
397 | }
398 |
399 | this._soundControlSection._isUpdating = false;
400 |
401 | });
402 | }
403 |
404 | _populateCustomizeSection() {
405 |
406 | if (!this._customizeSection) {
407 | return;
408 | }
409 |
410 | // Add Activate Running Behavior items
411 |
412 | this._activateBehaviorSection = new PopupMenuSection();
413 |
414 | this._activateBehaviorSection.addMenuItem(this._createSeparator(_('Activation Behavior')));
415 |
416 | this._activateBehaviorNewWindowItem = this._activateBehaviorSection.addAction(
417 | _('New window'),
418 | () => this._setActivationBehaviorValue('new_window')
419 | );
420 | this._activateBehaviorMoveWindowsItem = this._activateBehaviorSection.addAction(
421 | _('Move windows'),
422 | () => this._setActivationBehaviorValue('move_windows')
423 | );
424 |
425 | this._customizeSection.addMenuItem(this._activateBehaviorSection);
426 |
427 | // add Icon customization section
428 |
429 | this._customIconSection = new PopupMenuSection();
430 |
431 | this._customIconSection.addMenuItem(this._createSeparator(_('Icon')));
432 |
433 | this._customIconSetItem = this._customIconSection.addAction(
434 | _('Import'),
435 | () => this._importIconFromClipboard().then(iconPath => {
436 |
437 | if (!iconPath) {
438 | return;
439 | }
440 |
441 | this._setCustomIcon(iconPath);
442 | })
443 | );
444 | this._customIconResetItem = this._customIconSection.addAction(
445 | _('Reset to default'),
446 | () => this._setCustomIcon(null)
447 | );
448 |
449 | this._customizeSection.addMenuItem(this._customIconSection);
450 |
451 | // add Icon Size customization item
452 |
453 | this._customizeSection.addMenuItem(this._createSeparator(_('Icon Size')));
454 | this._customizeSection.addMenuItem(this._createIconSizeSliderMenuItem());
455 |
456 | // add Reset All item
457 | this._customizeSection.addMenuItem(this._createSeparator());
458 | this._resetAllItem = this._customizeSection.addAction(
459 | _('Reset all to default'),
460 | () => this._appButton.configOverride.reset()
461 | );
462 |
463 | this._updateCustomizeSection();
464 | }
465 |
466 | _updateCustomizeSection() {
467 |
468 | if (!this._customizeSection) {
469 | return;
470 | }
471 |
472 | this._customizeSection._isUpdating = true;
473 |
474 | this._setIconSizeSliderValue();
475 |
476 | this._setIconSizeSliderOverdrive();
477 |
478 | this._activateBehaviorSection.actor.visible = this._config.isolateWorkspaces;
479 |
480 | this._setActivationBehaviorValue();
481 |
482 | this._updateCustomIconSection();
483 |
484 | this._resetAllItem.actor.reactive = !this._appButton.configOverride.isEmpty();
485 |
486 | this._customizeSection._isUpdating = false;
487 | }
488 |
489 | _setActivationBehaviorValue(value) {
490 |
491 | if (!this._activateBehaviorSection.actor.visible) {
492 | return;
493 | }
494 |
495 | if (value) {
496 | this._config.configOverride.activateRunningBehavior = value;
497 | }
498 |
499 | const menuItems = [
500 | this._activateBehaviorNewWindowItem,
501 | this._activateBehaviorMoveWindowsItem
502 | ];
503 |
504 | let selectedMenuItem = null;
505 |
506 | switch (this._config.configOverride.activateRunningBehavior) {
507 | case 'new_window':
508 | selectedMenuItem = this._activateBehaviorNewWindowItem;
509 | break;
510 | case 'move_windows':
511 | selectedMenuItem = this._activateBehaviorMoveWindowsItem;
512 | break
513 | }
514 |
515 | menuItems.forEach(item => {
516 | if (item === selectedMenuItem) {
517 | item.setOrnament(Ornament.DOT);
518 | return;
519 | }
520 | item.setOrnament(Ornament.NONE);
521 | });
522 |
523 | this._applyConfigOverride();
524 | }
525 |
526 | _createIconSizeSliderMenuItem() {
527 |
528 | const menuItem = new PopupBaseMenuItem({
529 | activate: false
530 | });
531 |
532 | const valueLabel = new St.Label({
533 | text: '16', // min size by default
534 | y_expand: true,
535 | y_align: Clutter.ActorAlign.CENTER,
536 | });
537 |
538 | this._iconSizeSlider = this._createSlider(menuItem, () => {
539 |
540 | const value = this._getIconSizeSliderValue();
541 |
542 | valueLabel.text = value.toString()
543 |
544 | this._config.configOverride.iconSize = value;
545 |
546 | this._applyConfigOverride();
547 |
548 | });
549 |
550 | menuItem.add_child(this._iconSizeSlider);
551 | menuItem.add_child(valueLabel);
552 |
553 | return menuItem;
554 | }
555 |
556 | _createSlider(menuItem, onchange) {
557 | const result = new Slider(0);
558 |
559 | menuItem.connect('key-press-event', (actor, event) => {
560 | return result.emit('key-press-event', event);
561 | });
562 |
563 | result.connect('notify::value', onchange);
564 |
565 | return result;
566 | }
567 |
568 | _createSeparator(title) {
569 | const separator = new PopupSeparatorMenuItem(title);
570 |
571 | if (!title) {
572 | return separator;
573 | }
574 |
575 | separator.add_style_class_name('rocketbar__popup-menu_section-title');
576 |
577 | return separator;
578 | }
579 |
580 | _setIconSizeSliderValue() {
581 |
582 | const [sliderMaxValue, sliderValueOffset] = this._getIconSizeSliderBounds();
583 |
584 | this._iconSizeSlider.value = (this._config.configOverride.iconSize - sliderValueOffset) / sliderMaxValue;
585 | }
586 |
587 | _setIconSizeSliderOverdrive() {
588 |
589 | const [sliderMaxValue, sliderValueOffset] = this._getIconSizeSliderBounds();
590 |
591 | this._iconSizeSlider.overdriveStart = (this._config.defaultIconSize - sliderValueOffset) / sliderMaxValue;
592 | }
593 |
594 | _getIconSizeSliderValue() {
595 |
596 | const [sliderMaxValue, sliderValueOffset] = this._getIconSizeSliderBounds();
597 |
598 | return Math.round(sliderMaxValue * this._iconSizeSlider.value) + sliderValueOffset;
599 | }
600 |
601 | _getIconSizeSliderBounds() {
602 |
603 | const iconSizeMax = 64;
604 | const iconSizeMin = 16;
605 | const sliderMaxValue = iconSizeMax - iconSizeMin;
606 |
607 | return [sliderMaxValue, iconSizeMin];
608 | }
609 |
610 | _updateCustomIconSection() {
611 |
612 | // hide this section by default if no custom icon selected
613 | this._customIconSection.actor.visible = !!this._config.configOverride.customIconPath;
614 | this._customIconResetItem.visible = this._customIconSection.actor.visible;
615 |
616 | this._customIconSetItem.hide();
617 |
618 | // check if there is a valid icon in the clipboard
619 | this._importIconFromClipboard().then(iconPath => {
620 |
621 | if (!this.isOpen || !iconPath ||
622 | // no reason to import the same icon twice
623 | iconPath === this._config.configOverride.customIconPath) {
624 | return;
625 | }
626 |
627 | this._customIconSection.actor.show();
628 | this._customIconSetItem.show();
629 |
630 | // replace label of the menu item
631 |
632 | if (this._customIconSetItem.label.text.includes(':')) {
633 | this._customIconSetItem.label.text = this._customIconSetItem.label.text.split(':')[0];
634 | }
635 |
636 | const iconPathParts = iconPath.split('/');
637 |
638 | this._customIconSetItem.label.text += ': ' + iconPathParts[iconPathParts.length - 1];
639 |
640 | });
641 | }
642 |
643 | _importIconFromClipboard() {
644 | return new Promise(resolve => St.Clipboard.get_default().get_text(
645 | St.ClipboardType.CLIPBOARD,
646 | (clipboard, iconPath) => resolve(
647 | this._iconProvider.getCustomIcon(iconPath) !== null ?
648 | iconPath : null
649 | )
650 | ));
651 | }
652 |
653 | _setCustomIcon(iconPath) {
654 |
655 | this._config.configOverride.customIconPath = iconPath;
656 |
657 | this._applyConfigOverride();
658 | }
659 |
660 | _applyConfigOverride() {
661 |
662 | // no need to apply the config override
663 | if (this._customizeSection?._isUpdating ||
664 | this._appButton.configOverride.equals(this._config.configOverride)) {
665 | return;
666 | }
667 |
668 | this._stopApplyConfigOverride();
669 |
670 | this._applyConfigOverrideTimeout = Timeout.idle(300).run(() => {
671 |
672 | this._applyConfigOverrideTimeout = null;
673 |
674 | this._appButton.configOverride.set(this._config.configOverride);
675 |
676 | this._resetAllItem.actor.reactive = true;
677 | });
678 | }
679 |
680 | _stopApplyConfigOverride() {
681 | this._applyConfigOverrideTimeout?.destroy();
682 | this._applyConfigOverrideTimeout = null;
683 | }
684 |
685 | //#endregion private methods
686 |
687 | }
688 |
--------------------------------------------------------------------------------
/extension/ui/appButtonNotificationBadge.js:
--------------------------------------------------------------------------------
1 | /* exported AppButtonIndicator */
2 |
3 | import Clutter from 'gi://Clutter';
4 | import St from 'gi://St';
5 | import * as IconGrid from 'resource:///org/gnome/shell/ui/iconGrid.js';
6 |
7 | export class AppButtonNotificationBadge {
8 |
9 | constructor(appButton, layout, settings) {
10 |
11 | this._appButton = appButton;
12 | this._layout = layout;
13 | this._settings = settings;
14 | this._notificationBadge = null;
15 |
16 | this.updateConfig();
17 | }
18 |
19 | //#region public methods
20 |
21 | destroy() {
22 |
23 | this._layout = null;
24 |
25 | this._update();
26 | }
27 |
28 | updateConfig() {
29 | const oldConfig = this._config;
30 |
31 | this._setConfig();
32 |
33 | if (!oldConfig) {
34 | this.rerender();
35 | return;
36 | }
37 |
38 | if (oldConfig.notificationBadgeColor !== this._config.notificationBadgeColor ||
39 | oldConfig.notificationBadgeBorderColor !== this._config.notificationBadgeBorderColor ||
40 | oldConfig.notificationBadgePosition !== this._config.notificationBadgePosition ||
41 | oldConfig.notificationBadgeSize !== this._config.notificationBadgeSize ||
42 | oldConfig.notificationBadgeMargin !== this._config.notificationBadgeMargin) {
43 | this._updateStyle();
44 | }
45 | }
46 |
47 | rerender() {
48 | this._update();
49 | }
50 |
51 | //#endregion public methods
52 |
53 | //#region private methods
54 |
55 | _setConfig() {
56 | this._config = {
57 | notificationBadgeColor: this._settings.get_string('notification-badge-color'),
58 | notificationBadgeBorderColor: this._settings.get_string('notification-badge-border-color'),
59 | notificationBadgePosition: this._settings.get_string('notification-badge-position'),
60 | notificationBadgeSize: this._settings.get_int('notification-badge-size'),
61 | notificationBadgeMargin: this._settings.get_int('notification-badge-margin')
62 | };
63 | }
64 |
65 | _update() {
66 |
67 | const oldNotificationCount = this._notificationCount || 0;
68 |
69 | this._notificationCount = this._appButton.notifications;
70 |
71 | const show = (
72 | this._layout &&
73 | this._notificationCount > 0
74 | );
75 |
76 | if (!show) {
77 |
78 | if (this._notificationBadge) {
79 | this._notificationBadge.remove_all_transitions();
80 |
81 | // destroy without animation
82 | if (!this._layout) {
83 | this._notificationBadge.destroy();
84 | this._notificationBadge = null;
85 | return;
86 | }
87 |
88 | // reasign badge instance
89 | let oldNotificationBadge = this._notificationBadge;
90 | this._notificationBadge = null;
91 |
92 | // animate and destroy
93 | oldNotificationBadge.ease({
94 | opacity: 0,
95 | scale_x: 0.75,
96 | scale_y: 0.75,
97 | duration: 200,
98 | mode: Clutter.AnimationMode.EASE_OUT_QUAD,
99 | onComplete: () => {
100 | oldNotificationBadge.destroy();
101 | }
102 | });
103 | }
104 |
105 | return;
106 | }
107 |
108 | if (this._notificationBadge) {
109 |
110 | // zoom out the badge when new notifications comes up
111 | if (oldNotificationCount < this._notificationCount) {
112 | IconGrid.zoomOutActor(this._notificationBadge);
113 | }
114 |
115 | return;
116 | }
117 |
118 | this._notificationBadge = new St.Bin({
119 | name: 'taskbar-appButton-notification-badge',
120 | x_expand: true,
121 | y_expand: true,
122 | x_align: Clutter.ActorAlign.END,
123 | y_align: Clutter.ActorAlign.END,
124 | opacity: 0,
125 | scale_x: 0.75,
126 | scale_y: 0.75
127 | });
128 |
129 | this._updateStyle();
130 |
131 | this._layout.add_actor(this._notificationBadge);
132 |
133 | this._notificationBadge.set_pivot_point(0.5, 0.5);
134 |
135 | this._notificationBadge.ease({
136 | opacity: 255,
137 | scale_x: 1,
138 | scale_y: 1,
139 | duration: 300,
140 | mode: Clutter.AnimationMode.EASE_OUT_QUAD
141 | });
142 | }
143 |
144 | _updateStyle() {
145 |
146 | if (!this._notificationBadge) {
147 | return;
148 | }
149 |
150 | this._notificationBadge.style = (
151 | `background-color: ${this._config.notificationBadgeColor};` +
152 | `width: ${this._config.notificationBadgeSize}px;` +
153 | `height: ${this._config.notificationBadgeSize}px;` +
154 | `border-radius: ${this._config.notificationBadgeSize}px;` +
155 | `border: 1px solid ${this._config.notificationBadgeBorderColor};`
156 | );
157 |
158 | // set position
159 |
160 | this._notificationBadge.style += (
161 | this._config.notificationBadgePosition === 'top_left' ||
162 | this._config.notificationBadgePosition === 'top_right' ?
163 | `margin-top: ${this._config.notificationBadgeMargin}px;` :
164 | `margin-bottom: ${this._config.notificationBadgeMargin}px;`
165 | ) + (
166 | this._config.notificationBadgePosition === 'top_left' ||
167 | this._config.notificationBadgePosition === 'bottom_left' ?
168 | `margin-left: ${this._config.notificationBadgeMargin}px;` :
169 | `margin-right: ${this._config.notificationBadgeMargin}px;`
170 | );
171 |
172 | this._notificationBadge.y_align = (
173 | this._config.notificationBadgePosition === 'top_left' ||
174 | this._config.notificationBadgePosition === 'top_right' ?
175 | Clutter.ActorAlign.START :
176 | Clutter.ActorAlign.END
177 | );
178 |
179 | this._notificationBadge.x_align = (
180 | this._config.notificationBadgePosition === 'top_left' ||
181 | this._config.notificationBadgePosition === 'bottom_left' ?
182 | Clutter.ActorAlign.START :
183 | Clutter.ActorAlign.END
184 | );
185 |
186 | }
187 |
188 | //#endregion private methods
189 |
190 | }
191 |
--------------------------------------------------------------------------------
/extension/ui/appButtonTooltip.js:
--------------------------------------------------------------------------------
1 | /* exported AppButtonTooltip */
2 |
3 | //#region imports
4 |
5 | import Clutter from 'gi://Clutter';
6 | import St from 'gi://St';
7 | import * as Main from 'resource:///org/gnome/shell/ui/main.js';
8 |
9 | // custom modules import
10 | import { Timeout } from '../utils/timeout.js';
11 |
12 | //#endregion imports
13 |
14 | class TooltipCounter {
15 |
16 | constructor(icon, minCount) {
17 |
18 | this._minCount = minCount || 0;
19 |
20 | // create layout
21 |
22 | this.actor = new St.BoxLayout({
23 | name: 'appButton-tooltip-counter',
24 | style_class: 'rocketbar__tooltip_counter'
25 | });
26 |
27 | this.actor.add_actor(new St.Icon({
28 | name: 'appButton-tooltip-counter-icon',
29 | gicon: icon
30 | }));
31 |
32 | this._label = new St.Label({
33 | name: 'appButton-tooltip-counter-text'
34 | });
35 |
36 | this.actor.add_actor(this._label);
37 | }
38 |
39 | setCount(count) {
40 |
41 | count = count || 0;
42 |
43 | this._label.text = count.toString();
44 |
45 | if (count < this._minCount) {
46 | this.actor.hide();
47 | return;
48 | }
49 |
50 | this.actor.show();
51 | }
52 | }
53 |
54 | export class AppButtonTooltip {
55 |
56 | //#region public methods
57 |
58 | constructor(appButton, settings, iconProvider) {
59 |
60 | this._appButton = appButton;
61 | this._iconProvider = iconProvider;
62 |
63 | this._maxWidth = settings.get_int('tooltip-max-width');
64 |
65 | const showDelay = settings.get_int('tooltip-show-delay');
66 |
67 | this._showTimeout = Timeout.default(showDelay).run(() => {
68 | this._showTimeout = null;
69 | this._show();
70 | });
71 | }
72 |
73 | rerender() {
74 | this._update();
75 | }
76 |
77 | destroy(animation) {
78 |
79 | this._showTimeout?.destroy();
80 |
81 | if (!this._tooltip) {
82 | return;
83 | }
84 |
85 | this._tooltip.remove_all_transitions();
86 |
87 | if (animation) {
88 | this._tooltip.ease({
89 | opacity: 0,
90 | duration: 200,
91 | mode: Clutter.AnimationMode.EASE_OUT_QUAD,
92 | onComplete: () => this._tooltip.destroy()
93 | });
94 | return;
95 | }
96 |
97 | this._tooltip.destroy();
98 | }
99 |
100 | //#endregion public methods
101 |
102 | //#region private methods
103 |
104 | _show() {
105 |
106 | this._createTooltip();
107 |
108 | this._update();
109 |
110 | this._tooltip.ease({
111 | opacity: 255,
112 | duration: 300,
113 | mode: Clutter.AnimationMode.EASE_OUT_QUAD
114 | });
115 | }
116 |
117 | _createTooltip() {
118 |
119 | this._tooltip = new St.BoxLayout({
120 | name: 'appButton-tooltip',
121 | style_class: 'dash-label rocketbar__tooltip',
122 | opacity: 0
123 | });
124 |
125 | // create tooltip text
126 |
127 | this._tooltipText = new St.Label({
128 | name: 'appButton-tooltip-text',
129 | style: `max-width: ${this._maxWidth}px;`
130 | });
131 |
132 | this._tooltip.add_actor(this._tooltipText);
133 |
134 | // create windows counter
135 |
136 | this._windowsCounter = new TooltipCounter(this._iconProvider.getIcon('window-symbolic'), 2);
137 |
138 | this._tooltip.add_actor(this._windowsCounter.actor);
139 |
140 | // create notifications counter
141 |
142 | this._notificationsCounter = new TooltipCounter(this._iconProvider.getIcon('notification-symbolic'), 1);
143 |
144 | this._tooltip.add_actor(this._notificationsCounter.actor);
145 |
146 | // create sound icons
147 |
148 | this._soundOutputVolume = new TooltipCounter(this._iconProvider.getIcon('audio-speakers-symbolic'));
149 | this._soundInputVolume = new TooltipCounter(this._iconProvider.getIcon('audio-input-microphone-symbolic'));
150 |
151 | this._tooltip.add_actor(this._soundOutputVolume.actor);
152 | this._tooltip.add_actor(this._soundInputVolume.actor);
153 |
154 | // all ui elements created!
155 |
156 | Main.layoutManager.addChrome(this._tooltip);
157 | }
158 |
159 | _update() {
160 |
161 | if (!this._tooltip) {
162 | return;
163 | }
164 |
165 | this._updateAppTitle();
166 | this._updateWindowsCount();
167 | this._updateNotificationsCount();
168 | this._updateSoundVolumeIndicators();
169 |
170 | this._setPosition();
171 | }
172 |
173 | _updateAppTitle() {
174 | this._tooltipText.text = (
175 | this._appButton.activeWindow ?
176 | this._appButton.activeWindow.title :
177 | this._appButton.app.get_name()
178 | );
179 | }
180 |
181 | _updateWindowsCount() {
182 | this._windowsCounter.setCount(this._appButton.windows);
183 | }
184 |
185 | _updateNotificationsCount() {
186 | this._notificationsCounter.setCount(this._appButton.notifications);
187 | }
188 |
189 | _updateSoundVolumeIndicators() {
190 |
191 | if (this._appButton?.soundVolumeControl?.hasOutput()) {
192 | this._soundOutputVolume.setCount(
193 | Math.round(this._appButton.soundVolumeControl.getOutputVolume() * 100)
194 | );
195 | this._soundOutputVolume.actor.show();
196 | } else {
197 | this._soundOutputVolume.actor.hide();
198 | }
199 |
200 | if (this._appButton?.soundVolumeControl?.hasInput()) {
201 | this._soundInputVolume.setCount(
202 | Math.round(this._appButton.soundVolumeControl.getInputVolume() * 100)
203 | );
204 | this._soundInputVolume.actor.show();
205 | } else {
206 | this._soundInputVolume.actor.hide();
207 | }
208 |
209 | }
210 |
211 | _setPosition() {
212 |
213 | if (!this._tooltip) {
214 | return;
215 | }
216 |
217 | let [x, y] = this._appButton.get_transformed_position();
218 |
219 | const [appButtonWidth, appButtonHeight] = [
220 | this._appButton.allocation.get_width(),
221 | this._appButton.allocation.get_height()
222 | ];
223 |
224 | const [tooltipWidth, tooltipHeight] = this._tooltip.get_size();
225 |
226 | const xOffset = Math.floor((appButtonWidth - tooltipWidth) / 2);
227 |
228 | // define a static vertical offset
229 | const yOffset = 3;
230 |
231 | // if app button is on top of the screen
232 | if (y < 100) {
233 | y = y + appButtonHeight + yOffset;
234 | } else {
235 | y = y - tooltipHeight - yOffset;
236 | }
237 |
238 | x = Math.clamp(x + xOffset, 0, global.stage.width - tooltipWidth);
239 |
240 | this._tooltip.set_position(x, y);
241 | }
242 |
243 | //#endregion private methods
244 |
245 | }
246 |
--------------------------------------------------------------------------------
/extension/ui/notificationCounter.js:
--------------------------------------------------------------------------------
1 | /* exported NotificationCounter */
2 |
3 | //#region imports
4 |
5 | import Clutter from 'gi://Clutter';
6 | import GObject from 'gi://GObject';
7 | import St from 'gi://St';
8 | import * as Main from 'resource:///org/gnome/shell/ui/main.js';
9 |
10 | // custom modules import
11 | import { NotificationHandler } from '../services/notificationService.js';
12 | import { Timeout } from '../utils/timeout.js';
13 | import { Connections } from '../utils/connections.js';
14 |
15 | //#endregion imports
16 |
17 | class NotificationCounterContainer {
18 |
19 | constructor(notificationCounter, dndCallback) {
20 |
21 | this._dateMenu = Main.panel.statusArea.dateMenu;
22 | this._notificationCounter = notificationCounter;
23 | this._dndCallback = dndCallback;
24 | this._connections = null;
25 | this._container = null;
26 |
27 | this._setNotificationCounter();
28 | }
29 |
30 | destroy() {
31 | this._removeNotificationCounter();
32 | }
33 |
34 | getDndState() {
35 | return this._dateMenu?._indicator?._settings?.get_boolean('show-banners') === false;
36 | }
37 |
38 | _setNotificationCounter() {
39 |
40 | if (this._container || !this._dateMenu || !this._dateMenu._clockDisplay) {
41 | return;
42 | }
43 |
44 | this._connections = new Connections();
45 |
46 | // hide the indicator and prevent it from displaying
47 | // also handle Do not disturb state
48 | this._connections.add(this._dateMenu._indicator, 'notify::visible', indicator => indicator.hide());
49 | this._connections.add(this._dateMenu._indicator?._settings, 'changed::show-banners', () => this._dndCallback());
50 | this._dateMenu._indicator?.hide();
51 |
52 | // remember the class of the clock display
53 | this._clockDisplayStyleClass = this._dateMenu._clockDisplay?.style_class;
54 |
55 | // remove extra padding
56 | this._dateMenu.add_style_class_name('rocketbar__date-menu');
57 |
58 | // create a container for the notification counter with a delay
59 | // the delay helps to avoid animations stuttering
60 | this._initTimeout = Timeout.init().run(() => this._createContainer());
61 | }
62 |
63 | _createContainer() {
64 |
65 | const dateMenuContainer = this._dateMenu.get_children()[0];
66 |
67 | if (!dateMenuContainer) {
68 | return;
69 | }
70 |
71 | // remove clock display from the original container
72 | dateMenuContainer.remove_child(Main.panel.statusArea.dateMenu._clockDisplay);
73 |
74 | // create a custom container and make it look as a panel button
75 | this._container = new St.BoxLayout({
76 | name: 'notification-counter_container',
77 | style_class: this._clockDisplayStyleClass
78 | });
79 |
80 | // add items to the container
81 | this._container.add_child(Main.panel.statusArea.dateMenu._clockDisplay);
82 | this._container.add_child(this._notificationCounter);
83 |
84 | // remove a css class from the clock display
85 | // don't want it to look like a button anymore
86 | this._dateMenu._clockDisplay.style_class = null;
87 |
88 | dateMenuContainer.add_child(this._container);
89 | }
90 |
91 | _removeNotificationCounter() {
92 |
93 | this._initTimeout?.destroy();
94 |
95 | this._connections?.destroy();
96 |
97 | if (!this._container || !this._dateMenu || !this._dateMenu._clockDisplay) {
98 | return;
99 | }
100 |
101 | // restore the indicator
102 | this._dateMenu?._indicator?._sync();
103 |
104 | // remove children we don't want to destroy from the container
105 | // before destroying the container itself
106 | this._container.remove_all_children();
107 | this._container.destroy();
108 |
109 | const dateMenuContainer = this._dateMenu.get_children()[0];
110 |
111 | if (!dateMenuContainer) {
112 | return;
113 | }
114 |
115 | // restore the original css class for the clock display
116 | this._dateMenu._clockDisplay.set_style_class_name(this._clockDisplayStyleClass);
117 |
118 | // restore date menu padding
119 | this._dateMenu.remove_style_class_name('rocketbar__date-menu');
120 |
121 | // add the clock display into the original container
122 | dateMenuContainer.insert_child_at_index(this._dateMenu._clockDisplay, 1);
123 | }
124 |
125 | }
126 |
127 | export const NotificationCounter = GObject.registerClass(
128 | class Rocketbar__NotificationCounter extends St.BoxLayout {
129 |
130 | constructor(settings) {
131 |
132 | super({ name: 'notification-counter' });
133 |
134 | this._settings = settings;
135 | this._totalCount = 0;
136 | this._count = 0;
137 | this._isDnd = false;
138 |
139 | this._setConfig();
140 |
141 | this._createCounter();
142 |
143 | this._createConnections();
144 |
145 | this._notificationHandler = new NotificationHandler(
146 | count => this._setCount(count),
147 | this._settings, null
148 | );
149 |
150 | this._container = new NotificationCounterContainer(this, () => this._updateDndState());
151 | }
152 |
153 | _createCounter() {
154 |
155 | this._counter = new St.Label({
156 | name: 'notification-counter_counter',
157 | style_class: 'rocketbar__notification-counter',
158 | x_align: Clutter.ActorAlign.CENTER,
159 | y_align: Clutter.ActorAlign.CENTER,
160 | text: '0',
161 | opacity: 0,
162 | visible: false
163 | });
164 |
165 | this._counter.set_pivot_point(0.5, 0.5);
166 |
167 | // create a spacer to display between the clock display and the counter
168 | const spacer = new St.Label({
169 | name: 'notification-counter_spacer',
170 | y_align: Clutter.ActorAlign.CENTER,
171 | text: '0',
172 | opacity: 0
173 | });
174 | // the spacer visibility should be controlled by the counter visibility
175 | this._counter.bind_property('visible', spacer, 'visible', GObject.BindingFlags.SYNC_CREATE);
176 |
177 | this.add_actor(spacer);
178 | this.add_actor(this._counter);
179 | }
180 |
181 | _createConnections() {
182 |
183 | this.connect('destroy', () => this._destroy());
184 |
185 | this._connections = new Connections();
186 |
187 | this._connections.add(St.Settings.get(), 'notify::font-name', () => this._update());
188 |
189 | this._connections.add(this, 'notify::mapped', () => {
190 |
191 | // disconnect it to prevent from executing more than once
192 | this._connections.remove('notify::mapped');
193 |
194 | this._updateDndState();
195 |
196 | // let's wait for notification service a bit
197 | this._initTimeout = Timeout.idle(100).run(() => {
198 | // means we don't need to call the update
199 | // when the first update has been initiated by the notification service
200 | if (!this._updateTimeout) {
201 | this._update();
202 | }
203 | });
204 |
205 | });
206 |
207 | // handle settings
208 | this._connections.addScope(this._settings, [
209 | 'changed::notification-counter-hide-empty',
210 | 'changed::notification-counter-center-clock',
211 | 'changed::notification-counter-max-count',
212 | 'changed::notification-counter-font-size',
213 | 'changed::notification-counter-roundness',
214 | 'changed::notification-counter-margin-top',
215 | 'changed::notification-counter-color-empty',
216 | 'changed::notification-counter-color-not-empty',
217 | 'changed::notification-counter-text-color',
218 | 'changed::notification-counter-color-empty-dnd',
219 | 'changed::notification-counter-color-not-empty-dnd',
220 | 'changed::notification-counter-text-color-dnd'
221 | ], () => this._handleSettings());
222 | }
223 |
224 | _handleSettings() {
225 | const oldConfig = this._config || {};
226 |
227 | this._setConfig();
228 |
229 | if (this._config.hideEmpty !== oldConfig.hideEmpty ||
230 | this._config.centerClock !== oldConfig.centerClock) {
231 |
232 | if (this._canShow()) {
233 | this._counter.show();
234 | } else {
235 | this._counter.hide()
236 | }
237 |
238 | this._updateClockMargin();
239 |
240 | return;
241 | }
242 |
243 | if (this._config.maxCount !== oldConfig.maxCount) {
244 |
245 | this._setCount(this._totalCount);
246 |
247 | return;
248 | }
249 |
250 | this._updateStyle();
251 |
252 | if (this._config.fontSize !== oldConfig.fontSize) {
253 | this._updateClockMargin();
254 | }
255 | }
256 |
257 | _setConfig() {
258 | this._config = {
259 | hideEmpty: this._settings.get_boolean('notification-counter-hide-empty'),
260 | centerClock: this._settings.get_boolean('notification-counter-center-clock'),
261 | maxCount: this._settings.get_int('notification-counter-max-count'),
262 | fontSize: this._settings.get_int('notification-counter-font-size'),
263 | roundness: this._settings.get_int('notification-counter-roundness'),
264 | marginTop: this._settings.get_int('notification-counter-margin-top'),
265 | colorEmpty: this._settings.get_string('notification-counter-color-empty'),
266 | colorNotEmpty: this._settings.get_string('notification-counter-color-not-empty'),
267 | textColor: this._settings.get_string('notification-counter-text-color'),
268 | colorEmptyDnd: this._settings.get_string('notification-counter-color-empty-dnd'),
269 | colorNotEmptyDnd: this._settings.get_string('notification-counter-color-not-empty-dnd'),
270 | textColorDnd: this._settings.get_string('notification-counter-text-color-dnd')
271 | };
272 | }
273 |
274 | _destroy() {
275 |
276 | this._initTimeout?.destroy();
277 | this._updateTimeout?.destroy();
278 |
279 | this._connections.destroy();
280 |
281 | this._counter.remove_all_transitions();
282 |
283 | this._container?.destroy();
284 | this._container = null;
285 |
286 | this._notificationHandler?.destroy();
287 | }
288 |
289 | _setCount(count) {
290 |
291 | this._totalCount = count;
292 |
293 | if (count > this._config.maxCount) {
294 | count = this._config.maxCount;
295 | }
296 |
297 | if (this._count === count) {
298 | return;
299 | }
300 |
301 | this._count = count;
302 |
303 | this._update();
304 | }
305 |
306 | _update() {
307 |
308 | if (!this._container) {
309 | return;
310 | }
311 |
312 | this._updateTimeout?.destroy();
313 |
314 | if (!this._isValid()) {
315 | // a workaround for the first update
316 | this._updateTimeout = Timeout.idle(100).run(() => {
317 | this._update();
318 | this._updateTimeout = null;
319 | });
320 | return;
321 | }
322 |
323 | // the validation before calling the method is required
324 | if (this._canShow()) {
325 | this._updateClockMargin();
326 | }
327 |
328 | this._counter.remove_all_transitions();
329 |
330 | // add a fancy animation
331 | this._counter.ease({
332 | scale_x: 0,
333 | scale_y: 0,
334 | duration: 100,
335 | mode: Clutter.AnimationMode.EASE_OUT_QUAD,
336 | onComplete: () => {
337 |
338 | // update the counter when it's hidden
339 |
340 | this._counter.text = this._count.toString();
341 |
342 | if (!this._canShow()) {
343 | this._updateClockMargin();
344 | this._counter.hide()
345 | return;
346 | }
347 |
348 | this._counter.show();
349 |
350 | this._updateStyle();
351 |
352 | this._updateClockMargin();
353 |
354 | this._counter.ease({
355 | opacity: 255,
356 | scale_x: 1,
357 | scale_y: 1,
358 | duration: 200,
359 | mode: Clutter.AnimationMode.EASE_OUT_QUAD
360 | });
361 | }
362 | });
363 | }
364 |
365 | _updateClockMargin() {
366 |
367 | const parent = this.get_parent();
368 |
369 | if (!parent) {
370 | return;
371 | }
372 |
373 | if (!this._config.centerClock || !this._canShow()) {
374 | parent.style = null;
375 | return;
376 | }
377 |
378 | if (this.width) {
379 | parent.style = `margin-left: ${this.width}px;`;
380 | }
381 |
382 | }
383 |
384 | _canShow() {
385 | return this._count || !this._config.hideEmpty;
386 | }
387 |
388 | _updateDndState() {
389 |
390 | this._isDnd = this._container?.getDndState();
391 |
392 | this._updateStyle();
393 | }
394 |
395 | _updateStyle() {
396 |
397 | if (!this._isValid()) {
398 | return;
399 | }
400 |
401 | const isNotEmpty = this._count > 0;
402 | const isShort = this._count < 10;
403 |
404 | const borderColor = (
405 | this._isDnd ?
406 | this._config.colorEmptyDnd :
407 | this._config.colorEmpty
408 | );
409 |
410 | const backgroundColor = isNotEmpty ? (
411 | this._isDnd ?
412 | this._config.colorNotEmptyDnd :
413 | this._config.colorNotEmpty
414 | ) : 'transparent';
415 |
416 | const textColor = isNotEmpty ? (
417 | this._isDnd ?
418 | this._config.textColorDnd :
419 | this._config.textColor
420 | ) : 'transparent';
421 |
422 | // use predefined values for now
423 | const padding = isShort ? 0 : 3;
424 | const borderSize = isNotEmpty ? 0 : 2;
425 |
426 | this._counter.style = (
427 | `font-size: ${this._config.fontSize}px;` +
428 | `padding: 0 ${padding}px;` +
429 | `border: ${borderSize}px solid ${borderColor};` +
430 | `border-radius: ${this._config.roundness}px;` +
431 | `background-color: ${backgroundColor};` +
432 | `color: ${textColor};`
433 | );
434 |
435 | let height = this._counter.height;
436 |
437 | // do some kind of mathemagic
438 | height = height - borderSize * 4;
439 |
440 | this._counter.style += (
441 | `height: ${height}px;` +
442 | `min-width: ${height}px;` +
443 | `margin-top: ${this._config.marginTop}px;`
444 | );
445 | }
446 |
447 | _isValid() {
448 | return this.mapped && this.get_stage() !== null;
449 | }
450 |
451 | }
452 | );
453 |
--------------------------------------------------------------------------------
/extension/utils/config.js:
--------------------------------------------------------------------------------
1 | /* exported Config */
2 |
3 | export class Config {
4 |
5 | constructor(callback = () => null) {
6 | this._callback = callback;
7 | this._old = null;
8 | this.values = null; // { field => value }
9 | this.update();
10 | }
11 |
12 | update() {
13 |
14 | if (!this._callback) {
15 | return;
16 | }
17 |
18 | this._old = this.values;
19 | this.values = this._callback();
20 | }
21 |
22 | hasOld() {
23 | return !!this._old;
24 | }
25 |
26 | handleChanged(fields = [], callback = () => {}) {
27 |
28 | if (!this._old) {
29 | callback();
30 | return;
31 | }
32 |
33 | if (!this.values) {
34 | return;
35 | }
36 |
37 | for (let field of fields) {
38 | if (this._old[field] !== this.values[field]) {
39 | callback();
40 | return;
41 | }
42 | }
43 | }
44 |
45 | }
--------------------------------------------------------------------------------
/extension/utils/connections.js:
--------------------------------------------------------------------------------
1 | /* exported Connections */
2 |
3 | export class Connections {
4 |
5 | constructor() {
6 | this._connections = new Map();
7 | }
8 |
9 | destroy() {
10 | this._connections.forEach(connection => {
11 | connection.target.disconnect(connection.id);
12 | });
13 | this._connections = null;
14 | }
15 |
16 | addScope(target, scope, callback) {
17 |
18 | if (!target || !scope || !callback) {
19 | return;
20 | }
21 |
22 | for (let event of scope) {
23 | this.add(target, event, callback);
24 | }
25 | }
26 |
27 | removeScope(scope) {
28 |
29 | if (!scope) {
30 | return;
31 | }
32 |
33 | for (let event of scope) {
34 | this.remove(event);
35 | }
36 | }
37 |
38 | add(target, event, callback) {
39 |
40 | if (!target || !event || !callback ||
41 | this._connections.has(event)) {
42 | return;
43 | }
44 |
45 | this._connections.set(event, {
46 | target: target,
47 | id: target.connect(event, callback)
48 | });
49 | }
50 |
51 | remove(event) {
52 |
53 | if (!event || !this._connections.has(event)) {
54 | return;
55 | }
56 |
57 | const connection = this._connections.get(event);
58 |
59 | connection.target.disconnect(connection.id);
60 |
61 | this._connections.delete(event);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/extension/utils/dominantColorExtractor.js:
--------------------------------------------------------------------------------
1 | /* exported DominantColorExtractor */
2 |
3 | import Gio from 'gi://Gio';
4 | import St from 'gi://St';
5 | import GdkPixbuf from 'gi://GdkPixbuf';
6 |
7 | const DOMINANT_COLOR_SAMPLE_SIZE = 20;
8 |
9 | /**
10 | * Credit: Dash to Dock
11 | * https://github.com/micheleg/dash-to-dock
12 | */
13 | export class DominantColorExtractor {
14 |
15 | constructor(iconProvider, icon) {
16 | this._iconProvider = iconProvider;
17 | this._icon = icon;
18 | }
19 |
20 | /**
21 | * The backlight color choosing algorithm was mostly ported to javascript from the
22 | * Unity7 C++ source of Canonicals:
23 | * https://bazaar.launchpad.net/~unity-team/unity/trunk/view/head:/launcher/LauncherIcon.cpp
24 | * so it more or less works the same way.
25 | */
26 | getColor() {
27 | let pixBuf = this._getIconPixBuf();
28 |
29 | if (pixBuf === null) {
30 | // return white color in edge cases
31 | return {
32 | r: 255,
33 | g: 255,
34 | b: 255
35 | };
36 | }
37 |
38 | let pixels = pixBuf.get_pixels();
39 |
40 | let total = 0,
41 | rTotal = 0,
42 | gTotal = 0,
43 | bTotal = 0;
44 |
45 | let resample_y = 1,
46 | resample_x = 1;
47 |
48 | // Resampling of large icons
49 | // We resample icons larger than twice the desired size, as the resampling
50 | // to a size s
51 | // DOMINANT_COLOR_SAMPLE_SIZE < s < 2*DOMINANT_COLOR_SAMPLE_SIZE,
52 | // most of the case exactly DOMINANT_COLOR_SAMPLE_SIZE as the icon size is tipycally
53 | // a multiple of it.
54 | let width = pixBuf.get_width();
55 | let height = pixBuf.get_height();
56 |
57 | // Resample
58 | if (height >= 2 * DOMINANT_COLOR_SAMPLE_SIZE) {
59 | resample_y = Math.floor(height/DOMINANT_COLOR_SAMPLE_SIZE);
60 | }
61 |
62 | if (width >= 2 * DOMINANT_COLOR_SAMPLE_SIZE) {
63 | resample_x = Math.floor(width/DOMINANT_COLOR_SAMPLE_SIZE);
64 | }
65 |
66 | if (resample_x !==1 || resample_y !== 1) {
67 | pixels = this._resamplePixels(pixels, resample_x, resample_y);
68 | }
69 |
70 | // computing the limit outside the for (where it would be repeated at each iteration)
71 | // for performance reasons
72 | let limit = pixels.length;
73 |
74 | for (let offset = 0; offset < limit; offset+=4) {
75 | let r = pixels[offset],
76 | g = pixels[offset + 1],
77 | b = pixels[offset + 2],
78 | a = pixels[offset + 3];
79 |
80 | let saturation = (Math.max(r, g, b) - Math.min(r, g, b));
81 | let relevance = 0.1 * 255 * 255 + 0.9 * a * saturation;
82 |
83 | rTotal += r * relevance;
84 | gTotal += g * relevance;
85 | bTotal += b * relevance;
86 |
87 | total += relevance;
88 | }
89 |
90 | total = total * 255;
91 |
92 | let r = rTotal / total,
93 | g = gTotal / total,
94 | b = bTotal / total;
95 |
96 | let hsv = this._RGBtoHSV(r * 255, g * 255, b * 255);
97 |
98 | if (hsv.s > 0.15) {
99 | hsv.s = 0.65;
100 | }
101 |
102 | hsv.v = 0.90;
103 |
104 | return this._HSVtoRGB(hsv.h, hsv.s, hsv.v);
105 | }
106 |
107 | /**
108 | * Try to get the pixel buffer for the current icon, if not fail gracefully
109 | */
110 | _getIconPixBuf() {
111 |
112 | // Unable to load the icon texture, use fallback
113 | if (!this._icon || this._icon instanceof St.Icon === false) {
114 | return null;
115 | }
116 |
117 | const iconTexture = this._icon.get_gicon();
118 |
119 | // Unable to load the icon texture, use fallback
120 | if (iconTexture === null) {
121 | return null;
122 | }
123 |
124 | if (iconTexture instanceof Gio.FileIcon) {
125 | // Use GdkPixBuf to load the pixel buffer from the provided file path
126 | return GdkPixbuf.Pixbuf.new_from_file(iconTexture.get_file().get_path());
127 | }
128 |
129 | // for some applications iconTexture.get_gicon() returns St.ImageContent
130 | // it doesn have get_names function
131 | // for ex: Open Office
132 | // TODO: no solution as of now
133 | if (!iconTexture.get_names) {
134 | return null;
135 | }
136 |
137 | // Get the pixel buffer from the icon theme
138 | const iconInfo = this._iconProvider.getIconInfo(
139 | iconTexture.get_names()[0],
140 | DOMINANT_COLOR_SAMPLE_SIZE
141 | );
142 |
143 | if (iconInfo !== null) {
144 | if (iconInfo.load_icon) {
145 | return iconInfo.load_icon();
146 | }
147 | if (iconInfo.get_file) {
148 | return GdkPixbuf.Pixbuf.new_from_file(iconInfo.get_file().get_path());
149 | }
150 | }
151 |
152 | return null;
153 | }
154 |
155 | /**
156 | * Downsample large icons before scanning for the backlight color to
157 | * improve performance.
158 | *
159 | * @param pixBuf
160 | * @param pixels
161 | * @param resampleX
162 | * @param resampleY
163 | *
164 | * @return [];
165 | */
166 | _resamplePixels (pixels, resampleX, resampleY) {
167 | let resampledPixels = [];
168 |
169 | // computing the limit outside the for (where it would be repeated at each iteration)
170 | // for performance reasons
171 | let limit = pixels.length / (resampleX * resampleY) / 4;
172 |
173 | for (let i = 0; i < limit; ++i) {
174 | let pixel = i * resampleX * resampleY;
175 |
176 | resampledPixels.push(pixels[pixel * 4]);
177 | resampledPixels.push(pixels[pixel * 4 + 1]);
178 | resampledPixels.push(pixels[pixel * 4 + 2]);
179 | resampledPixels.push(pixels[pixel * 4 + 3]);
180 | }
181 |
182 | return resampledPixels;
183 | }
184 |
185 | // Convert hsv ([0-1, 0-1, 0-1]) to rgb ([0-255, 0-255, 0-255]).
186 | // Following algorithm in https://en.wikipedia.org/wiki/HSL_and_HSV
187 | // here with h = [0,1] instead of [0, 360]
188 | // Accept either (h,s,v) independently or {h:h, s:s, v:v} object.
189 | // Return {r:r, g:g, b:b} object.
190 | _HSVtoRGB(h, s, v) {
191 |
192 | if (arguments.length === 1) {
193 | s = h.s;
194 | v = h.v;
195 | h = h.h;
196 | }
197 |
198 | let r, g, b;
199 | let c = v * s;
200 | let h1 = h * 6;
201 | let x = c * (1 - Math.abs(h1 % 2 - 1));
202 | let m = v - c;
203 |
204 | if (h1 <=1) {
205 | r = c + m, g = x + m, b = m;
206 | } else if (h1 <=2) {
207 | r = x + m, g = c + m, b = m;
208 | } else if (h1 <=3) {
209 | r = m, g = c + m, b = x + m;
210 | } else if (h1 <=4) {
211 | r = m, g = x + m, b = c + m;
212 | } else if (h1 <=5) {
213 | r = x + m, g = m, b = c + m;
214 | } else {
215 | r = c + m, g = m, b = x + m;
216 | }
217 |
218 | return {
219 | r: Math.round(r * 255),
220 | g: Math.round(g * 255),
221 | b: Math.round(b * 255)
222 | };
223 | }
224 |
225 | // Convert rgb ([0-255, 0-255, 0-255]) to hsv ([0-1, 0-1, 0-1]).
226 | // Following algorithm in https://en.wikipedia.org/wiki/HSL_and_HSV
227 | // here with h = [0,1] instead of [0, 360]
228 | // Accept either (r,g,b) independently or {r:r, g:g, b:b} object.
229 | // Return {h:h, s:s, v:v} object.
230 | _RGBtoHSV(r, g, b) {
231 |
232 | if (arguments.length === 1) {
233 | r = r.r;
234 | g = r.g;
235 | b = r.b;
236 | }
237 |
238 | let h, s, v;
239 |
240 | let M = Math.max(r, g, b);
241 | let m = Math.min(r, g, b);
242 | let c = M - m;
243 |
244 | if (c === 0) {
245 | h = 0;
246 | } else if (M === r) {
247 | h = ((g - b) / c) % 6;
248 | } else if (M === g) {
249 | h = (b - r) / c + 2;
250 | } else {
251 | h = (r - g) / c + 4;
252 | }
253 |
254 | h = h / 6;
255 | v = M / 255;
256 |
257 | if (M !== 0) {
258 | s = c / M;
259 | } else {
260 | s = 0;
261 | }
262 |
263 | return {
264 | h: h,
265 | s: s,
266 | v: v
267 | };
268 | }
269 |
270 | }
271 |
--------------------------------------------------------------------------------
/extension/utils/favorites.js:
--------------------------------------------------------------------------------
1 | /* exported Favorites */
2 |
3 | import Shell from 'gi://Shell';
4 | import { getAppFavorites } from 'resource:///org/gnome/shell/ui/appFavorites.js';
5 | import { Connections } from './connections.js';
6 |
7 | export class Favorites {
8 |
9 | constructor(callback) {
10 | this._callback = callback;
11 | this._apps = null;
12 | this._appFavorites = getAppFavorites();
13 | this._connections = new Connections();
14 | this._connections.add(Shell.AppSystem.get_default(), 'installed-changed', () => this._handleInstalledChanged());
15 | this._connections.add(this._appFavorites, 'changed', () => this._handleChanged());
16 | }
17 |
18 | destroy() {
19 | this._connections.destroy();
20 | }
21 |
22 | getApps() {
23 |
24 | if (!this._apps) {
25 | this._apps = this._appFavorites.getFavorites();
26 | }
27 |
28 | return this._apps;
29 | }
30 |
31 | addApp(appId) {
32 | this._appFavorites.addFavorite(appId);
33 | }
34 |
35 | moveAppToPosition(appId, position) {
36 |
37 | // in this case we can't relay on this._apps
38 | // because it can be null at some point of time
39 | const appIds = this._appFavorites._getIds();
40 |
41 | const oldPosition = appIds.indexOf(appId);
42 |
43 | // check if the position hasn't changed
44 | if (position === oldPosition) {
45 | return;
46 | }
47 |
48 | this._appFavorites.moveFavoriteToPos(appId, position);
49 | }
50 |
51 | _handleChanged() {
52 |
53 | if (this._apps && this._callback) {
54 | this._callback();
55 | }
56 |
57 | this._apps = null;
58 | }
59 |
60 | _handleInstalledChanged() {
61 |
62 | const oldAppIds = this._getAppIds();
63 | const newAppIds = this._appFavorites._getIds().toString();
64 |
65 | // nothing has changed
66 | if (oldAppIds === newAppIds) {
67 | return;
68 | }
69 |
70 | this._apps = null;
71 |
72 | if (this._callback) {
73 | this._callback();
74 | }
75 | }
76 |
77 | _getAppIds() {
78 |
79 | if (!this._apps) {
80 | return '';
81 | }
82 |
83 | let result = [];
84 |
85 | for (let i = 0, l = this._apps.length; i < l; ++i) {
86 | result.push(this._apps[i].id);
87 | }
88 |
89 | return result.toString();
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/extension/utils/iconProvider.js:
--------------------------------------------------------------------------------
1 | /* exported IconProvider */
2 |
3 | import Gio from 'gi://Gio';
4 | import St from 'gi://St';
5 |
6 | export class IconProvider {
7 | constructor(extensionPath) {
8 | this._iconTheme = new St.IconTheme();
9 | this._assetsPath = `${extensionPath}/assets/icons/`;
10 | }
11 |
12 | getIcon(iconName, iconSize) {
13 | const iconInfo = this.getIconInfo(iconName, iconSize);
14 |
15 | if (iconInfo) {
16 | let iconPath = null;
17 | if (iconInfo.get_filename) {
18 | iconPath = iconInfo.get_filename();
19 | } else if (iconInfo.get_file) {
20 | iconPath = iconInfo.get_file().get_path();
21 | }
22 |
23 | if (iconPath) {
24 | return Gio.Icon.new_for_string(iconPath);
25 | }
26 | }
27 |
28 | return this.getCustomIcon(this._assetsPath + iconName + '.svg');
29 | }
30 |
31 | getIconInfo(iconName, iconSize) {
32 | return this._iconTheme.lookup_icon(iconName, iconSize, 0);
33 | }
34 |
35 | getCustomIcon(iconPath) {
36 |
37 | if (!iconPath || !iconPath.length) {
38 | return null;
39 | }
40 |
41 | // a simple validation to check that the iconPath looks like a real path
42 | // NOTE: it's not the safest way to validate the icon, but it's fast
43 | if (!iconPath.startsWith('/') || !(
44 | // only .png and .svg files supported for now
45 | iconPath.endsWith('.png') ||
46 | iconPath.endsWith('.svg')
47 | )) {
48 | return null;
49 | }
50 |
51 | // check that the path exists
52 | const iconFile = Gio.File.new_for_path(iconPath);
53 |
54 | if (!iconFile.query_exists(null)) {
55 | return null;
56 | }
57 |
58 | // create GIcon if everything looks fine
59 | return Gio.Icon.new_for_string(iconFile.get_path());
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/extension/utils/launcherAPI.js:
--------------------------------------------------------------------------------
1 | /* exported LauncherAPI */
2 |
3 | import Gio from 'gi://Gio';
4 |
5 | export class LauncherAPI {
6 |
7 | static _instance = null;
8 |
9 | static instance() {
10 |
11 | if (!LauncherAPI._instance) {
12 | LauncherAPI._instance = new LauncherAPI();
13 | }
14 |
15 | return LauncherAPI._instance;
16 | }
17 |
18 | static destroy() {
19 | LauncherAPI._instance?.destroy();
20 | LauncherAPI._instance = null;
21 | }
22 |
23 | constructor() {
24 | this._dbusId = Gio.DBus.session.own_name(
25 | 'com.canonical.Unity',
26 | Gio.BusNameOwnerFlags.ALLOW_REPLACEMENT | Gio.BusNameOwnerFlags.REPLACE,
27 | null,
28 | () => this._dbusId = null
29 | );
30 | }
31 |
32 | destroy() {
33 |
34 | if (!this._dbusId) {
35 | return;
36 | }
37 |
38 | Gio.DBus.session.unown_name(this._dbusId);
39 | }
40 |
41 | subscribe(callback) {
42 |
43 | if (!callback) {
44 | return null;
45 | }
46 |
47 | return Gio.DBus.session.signal_subscribe(
48 | null, 'com.canonical.Unity.LauncherEntry',
49 | null, null, null,
50 | Gio.DBusSignalFlags.NONE,
51 | (connection, sender, path, name, signal, params) => callback(params)
52 | );
53 | }
54 |
55 | unsubscribe(id) {
56 |
57 | if (!id) {
58 | return;
59 | }
60 |
61 | Gio.DBus.session.signal_unsubscribe(id);
62 | }
63 |
64 | }
--------------------------------------------------------------------------------
/extension/utils/positionProvider.js:
--------------------------------------------------------------------------------
1 | /* exported PositionProvider */
2 |
3 | import * as Main from 'resource:///org/gnome/shell/ui/main.js';
4 |
5 | export class PositionProvider {
6 |
7 | constructor(actor) {
8 | this._actor = actor;
9 | this._position = 'left';
10 | this._offset = 0;
11 | }
12 |
13 | destroy() {
14 | this.togglePositionLock(false);
15 | }
16 |
17 | setPosition(position = 'left', offset = 0) {
18 |
19 | this._position = position;
20 | this._offset = offset;
21 |
22 | this._handlePosition();
23 | }
24 |
25 | togglePositionLock(locked = false, callback) {
26 |
27 | if (!this._actor) {
28 | return;
29 | }
30 |
31 | if (locked && !this._positionLockId) {
32 |
33 | this._positionLockId = this._actor.connect('notify::position', () => {
34 |
35 | if (this._handlePosition() && callback) {
36 | callback();
37 | }
38 |
39 | });
40 |
41 | return;
42 | }
43 |
44 | if (!locked && this._positionLockId) {
45 | this._actor.disconnect(this._positionLockId);
46 | this._positionLockId = null;
47 | }
48 | }
49 |
50 | _handlePosition() {
51 |
52 | let targetParent = null;
53 |
54 | switch (this._position) {
55 | case 'left':
56 | targetParent = Main.panel._leftBox;
57 | break;
58 | case 'center':
59 | targetParent = Main.panel._centerBox;
60 | break;
61 | case 'right':
62 | targetParent = Main.panel._rightBox;
63 | break;
64 | }
65 |
66 | if (!targetParent) {
67 | return false;
68 | }
69 |
70 | const parent = this._actor?.mapped ? this._actor.get_parent() : null;
71 |
72 | if (parent && parent === targetParent) {
73 |
74 | if (this._offset > targetParent.get_n_children() ||
75 | parent.get_child_at_index(this._offset) === this._actor) {
76 | return false;
77 | }
78 |
79 | targetParent.set_child_at_index(this._actor, this._offset);
80 |
81 | return true;
82 | }
83 |
84 | if (parent) {
85 | parent.remove_actor(this._actor);
86 | }
87 |
88 | targetParent.insert_child_at_index(this._actor, this._offset);
89 |
90 | return true;
91 | }
92 |
93 | }
94 |
--------------------------------------------------------------------------------
/extension/utils/scrollHandler.js:
--------------------------------------------------------------------------------
1 | /* exported ScrollHandler */
2 |
3 | import Clutter from 'gi://Clutter';
4 |
5 | export class ScrollHandler {
6 |
7 | constructor(actor, callback) {
8 |
9 | if (!actor) {
10 | return;
11 | }
12 |
13 | this._actor = actor;
14 |
15 | this._scrollHandler = this._actor.connect(
16 | 'scroll-event',
17 | (actor, event) => this._handleScroll(event)
18 | );
19 |
20 | this._callback = callback;
21 | }
22 |
23 | destroy() {
24 | this._actor?.disconnect(this._scrollHandler);
25 | }
26 |
27 | _handleScroll(event) {
28 |
29 | if (!this._callback || !event) {
30 | return Clutter.EVENT_PROPAGATE;
31 | }
32 |
33 | const scrollDirection = event?.get_scroll_direction();
34 |
35 | // handle only 2 directions: UP and DOWN
36 | if (scrollDirection !== Clutter.ScrollDirection.UP &&
37 | scrollDirection !== Clutter.ScrollDirection.DOWN) {
38 | return Clutter.EVENT_PROPAGATE;
39 | }
40 |
41 | const isCtrlPressed = (event.get_state() & Clutter.ModifierType.CONTROL_MASK) != 0;
42 |
43 | return this._callback([scrollDirection, isCtrlPressed]);
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/extension/utils/timeout.js:
--------------------------------------------------------------------------------
1 | /* exported Timeout */
2 |
3 | import GLib from 'gi://GLib';
4 | import Meta from 'gi://Meta';
5 |
6 | export class Timeout {
7 |
8 | static init() {
9 | return Timeout.idle(400);
10 | }
11 |
12 | static default(delay = 0) {
13 | return new Timeout(GLib.PRIORITY_DEFAULT, delay);
14 | }
15 |
16 | static idle(delay = 0) {
17 | return new Timeout(delay ? GLib.PRIORITY_DEFAULT_IDLE : Meta.LaterType.IDLE, delay);
18 | }
19 |
20 | static low(delay = 0) {
21 | return new Timeout(GLib.PRIORITY_LOW, delay);
22 | }
23 |
24 | static redraw() {
25 | return new Timeout(Meta.LaterType.BEFORE_REDRAW);
26 | }
27 |
28 | constructor(priority, delay) {
29 | this._priority = priority;
30 | this._delay = delay;
31 | this._id = null;
32 | }
33 |
34 | run(callback) {
35 |
36 | const handler = () => {
37 |
38 | this._id = null;
39 |
40 | if (callback) {
41 | callback();
42 | }
43 |
44 | return GLib.SOURCE_REMOVE;
45 | };
46 |
47 | const laters = global.compositor?.get_laters();
48 |
49 | this._id = (
50 |
51 | this._priority === Meta.LaterType.BEFORE_REDRAW ||
52 | this._priority === Meta.LaterType.IDLE ?
53 |
54 | (laters ? laters.add(this._priority, handler) : Meta.later_add(this._priority, handler)) :
55 |
56 | GLib.timeout_add(this._priority, this._delay, handler)
57 | );
58 |
59 | return this;
60 | }
61 |
62 | destroy() {
63 |
64 | if (!this._id) {
65 | return;
66 | }
67 |
68 | if (this._priority === Meta.LaterType.BEFORE_REDRAW ||
69 | this._priority === Meta.LaterType.IDLE) {
70 |
71 | const laters = global.compositor?.get_laters();
72 |
73 | if (laters) {
74 | laters.remove(this._id);
75 | } else {
76 | Meta.later_remove(this._id);
77 | }
78 |
79 | return;
80 | }
81 |
82 | GLib.source_remove(this._id);
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/install:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ./build
4 |
5 | gnome-extensions install --force *.shell-extension.zip
6 |
7 | rm *.shell-extension.zip
--------------------------------------------------------------------------------
/media/customize.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/linux-is-awesome/gnome_extension_rocketbar/6eb9f013d9ac020aa3a04b9fa86b13dc85ecbe83/media/customize.png
--------------------------------------------------------------------------------
/media/get-it-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/linux-is-awesome/gnome_extension_rocketbar/6eb9f013d9ac020aa3a04b9fa86b13dc85ecbe83/media/get-it-logo.png
--------------------------------------------------------------------------------
/media/taskbar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/linux-is-awesome/gnome_extension_rocketbar/6eb9f013d9ac020aa3a04b9fa86b13dc85ecbe83/media/taskbar.jpg
--------------------------------------------------------------------------------
/media/taskbar_bottom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/linux-is-awesome/gnome_extension_rocketbar/6eb9f013d9ac020aa3a04b9fa86b13dc85ecbe83/media/taskbar_bottom.png
--------------------------------------------------------------------------------
/media/taskbar_top.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/linux-is-awesome/gnome_extension_rocketbar/6eb9f013d9ac020aa3a04b9fa86b13dc85ecbe83/media/taskbar_top.png
--------------------------------------------------------------------------------
/uninstall:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | uuid=`cat ./extension/metadata.json | grep -oP '(?<="uuid": ")[^"]*'`
4 |
5 | gnome-extensions uninstall $uuid
--------------------------------------------------------------------------------
/update-pot:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ExtensionName=`cat ./extension/metadata.json | grep -oP '(?<="name": ")[^"]*'`
4 |
5 | cd ./extension
6 |
7 | xgettext --output=./locale/${ExtensionName,,}.pot ./*/*.js
--------------------------------------------------------------------------------