├── .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 | [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://github.com/linux-is-awesome/gnome_extension_rocketbar/blob/master/LICENSE) 3 | 4 | ![](/media/taskbar.jpg) 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 | ![](/media/taskbar_top.png) 39 | 40 | - Taskbar on bottom 41 | 42 | ![](/media/taskbar_bottom.png) 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 | ![](/media/customize.png) 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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /extension/assets/icons/window-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 --------------------------------------------------------------------------------