From c968bf909f058f86fce803d386bb9d35b784a886 Mon Sep 17 00:00:00 2001 From: Adriel Sand Date: Wed, 12 Jan 2022 14:55:33 -0300 Subject: [PATCH] initial commit --- .config/brave-flags.conf | 1 + .config/dconf/user | Bin 0 -> 60309 bytes .config/electron-flags.conf | 1 + .config/fish/config.fish | 8 + .config/fish/fish_variables | 32 + .config/fish/functions/fish_prompt.fish | 139 + .config/pop-shell/config.json | 43 + .../Move_Clock@rmy.pobox.com/extension.js | 46 + .../Move_Clock@rmy.pobox.com/metadata.json | 12 + .../arch-update@RaphaelRochet/LICENCE.txt | 674 ++++ .../arch-update@RaphaelRochet/README.md | 212 ++ .../arch-update@RaphaelRochet/extension.js | 509 +++ .../icons/arch-error-symbolic.svg | 233 ++ .../icons/arch-lit-symbolic.svg | 171 + .../icons/arch-unknown-symbolic.svg | 395 +++ .../icons/arch-updates-symbolic.svg | 316 ++ .../icons/arch-uptodate-symbolic.svg | 171 + .../locale/ar/LC_MESSAGES/arch-update.mo | Bin 0 -> 3811 bytes .../locale/ca/LC_MESSAGES/arch-update.mo | Bin 0 -> 2940 bytes .../locale/cs_CZ/LC_MESSAGES/arch-update.mo | Bin 0 -> 2500 bytes .../locale/de_DE/LC_MESSAGES/arch-update.mo | Bin 0 -> 3643 bytes .../locale/eo/LC_MESSAGES/arch-update.mo | Bin 0 -> 3188 bytes .../locale/es/LC_MESSAGES/arch-update.mo | Bin 0 -> 3655 bytes .../locale/et/LC_MESSAGES/arch-update.mo | Bin 0 -> 2947 bytes .../locale/et/arch-update.mo | Bin 0 -> 2948 bytes .../locale/fa_IR/LC_MESSAGES/arch-update.mo | Bin 0 -> 3347 bytes .../locale/fi_FI/LC_MESSAGES/arch-update.mo | Bin 0 -> 3170 bytes .../locale/fr/LC_MESSAGES/arch-update.mo | Bin 0 -> 3615 bytes .../locale/he_IL/LC_MESSAGES/arch-update.mo | Bin 0 -> 3259 bytes .../locale/he_IL/LC_MESSAGES/he_IL.mo | Bin 0 -> 3301 bytes .../locale/it_IT/LC_MESSAGES/arch-update.mo | Bin 0 -> 3328 bytes .../locale/ko/LC_MESSAGES/arch-update.mo | Bin 0 -> 3223 bytes .../locale/nb_NO/LC_MESSAGES/arch-update.mo | Bin 0 -> 2374 bytes .../locale/nl/LC_MESSAGES/arch-update.mo | Bin 0 -> 3546 bytes .../locale/pl/LC_MESSAGES/arch-update.mo | Bin 0 -> 2006 bytes .../locale/pt_BR/LC_MESSAGES/arch-update.mo | Bin 0 -> 3447 bytes .../locale/ro/LC_MESSAGES/arch-update.mo | Bin 0 -> 3588 bytes .../locale/ru_RU/LC_MESSAGES/arch-update.mo | Bin 0 -> 4755 bytes .../locale/sk/LC_MESSAGES/arch-update.mo | Bin 0 -> 3320 bytes .../locale/sr/LC_MESSAGES/arch-update.mo | Bin 0 -> 3713 bytes .../sr@latin/LC_MESSAGES/arch-update.mo | Bin 0 -> 2972 bytes .../locale/sv/LC_MESSAGES/arch-update.mo | Bin 0 -> 3229 bytes .../locale/tr_TR/LC_MESSAGES/arch-update.mo | Bin 0 -> 3299 bytes .../locale/uk_UA/LC_MESSAGES/arch-update.mo | Bin 0 -> 4661 bytes .../locale/zh_CN/LC_MESSAGES/arch-update.mo | Bin 0 -> 3141 bytes .../arch-update@RaphaelRochet/metadata.json | 12 + .../arch-update@RaphaelRochet/prefs.js | 67 + .../arch-update@RaphaelRochet/prefs.xml | 459 +++ .../schemas/gschemas.compiled | Bin 0 -> 1285 bytes ...e.shell.extensions.arch-update.gschema.xml | 111 + .../arch-update@RaphaelRochet/stylesheet.css | 36 + .../dash-to-dock@micxgx.gmail.com/COPYING | 339 ++ .../dash-to-dock@micxgx.gmail.com/README.md | 45 + .../dash-to-dock@micxgx.gmail.com/Settings.ui | 2799 +++++++++++++++++ .../appIconIndicators.js | 1031 ++++++ .../dash-to-dock@micxgx.gmail.com/appIcons.js | 1435 +++++++++ .../dash-to-dock@micxgx.gmail.com/dash.js | 1113 +++++++ .../dbusmenuUtils.js | 274 ++ .../dash-to-dock@micxgx.gmail.com/docking.js | 2340 ++++++++++++++ .../extension.js | 21 + .../fileManager1API.js | 154 + .../intellihide.js | 321 ++ .../launcherAPI.js | 281 ++ .../locale/ar/LC_MESSAGES/dashtodock.mo | Bin 0 -> 8303 bytes .../locale/cs/LC_MESSAGES/dashtodock.mo | Bin 0 -> 10321 bytes .../locale/de/LC_MESSAGES/dashtodock.mo | Bin 0 -> 8726 bytes .../locale/el/LC_MESSAGES/dashtodock.mo | Bin 0 -> 12052 bytes .../locale/es/LC_MESSAGES/dashtodock.mo | Bin 0 -> 10083 bytes .../locale/eu/LC_MESSAGES/dashtodock.mo | Bin 0 -> 10183 bytes .../locale/fr/LC_MESSAGES/dashtodock.mo | Bin 0 -> 10218 bytes .../locale/gl/LC_MESSAGES/dashtodock.mo | Bin 0 -> 8819 bytes .../locale/hu/LC_MESSAGES/dashtodock.mo | Bin 0 -> 9276 bytes .../locale/id/LC_MESSAGES/dashtodock.mo | Bin 0 -> 8560 bytes .../locale/it/LC_MESSAGES/dashtodock.mo | Bin 0 -> 9880 bytes .../locale/ja/LC_MESSAGES/dashtodock.mo | Bin 0 -> 12122 bytes .../locale/nb/LC_MESSAGES/dashtodock.mo | Bin 0 -> 9203 bytes .../locale/nl/LC_MESSAGES/dashtodock.mo | Bin 0 -> 10077 bytes .../locale/pl/LC_MESSAGES/dashtodock.mo | Bin 0 -> 10048 bytes .../locale/pt/LC_MESSAGES/dashtodock.mo | Bin 0 -> 9261 bytes .../locale/pt_BR/LC_MESSAGES/dashtodock.mo | Bin 0 -> 9895 bytes .../locale/ru/LC_MESSAGES/dashtodock.mo | Bin 0 -> 12849 bytes .../locale/sk/LC_MESSAGES/dashtodock.mo | Bin 0 -> 6290 bytes .../locale/sr/LC_MESSAGES/dashtodock.mo | Bin 0 -> 11186 bytes .../locale/sr@latin/LC_MESSAGES/dashtodock.mo | Bin 0 -> 8824 bytes .../locale/sv/LC_MESSAGES/dashtodock.mo | Bin 0 -> 10093 bytes .../locale/tr/LC_MESSAGES/dashtodock.mo | Bin 0 -> 10080 bytes .../locale/uk_UA/LC_MESSAGES/dashtodock.mo | Bin 0 -> 15770 bytes .../locale/zh_CN/LC_MESSAGES/dashtodock.mo | Bin 0 -> 9442 bytes .../locale/zh_TW/LC_MESSAGES/dashtodock.mo | Bin 0 -> 9484 bytes .../locations.js | 660 ++++ .../media/glossy.svg | 139 + .../media/highlight_stacked_bg.svg | 82 + .../media/highlight_stacked_bg_h.svg | 82 + .../media/logo.svg | 528 ++++ .../metadata.json | 14 + .../dash-to-dock@micxgx.gmail.com/prefs.js | 1029 ++++++ .../schemas/gschemas.compiled | Bin 0 -> 7431 bytes ....shell.extensions.dash-to-dock.gschema.xml | 566 ++++ .../stylesheet.css | 528 ++++ .../dash-to-dock@micxgx.gmail.com/theming.js | 561 ++++ .../dash-to-dock@micxgx.gmail.com/utils.js | 416 +++ .../windowPreview.js | 607 ++++ .../COPYING | 13 + .../LICENSE | 130 + .../convenience.js | 110 + .../dynamic-panel-transparency.pot | 162 + .../events.js | 231 ++ .../extension.js | 571 ++++ .../intellifade.js | 252 ++ .../LC_MESSAGES/dynamic-panel-transparency.mo | Bin 0 -> 2715 bytes .../LC_MESSAGES/dynamic-panel-transparency.po | 231 ++ .../LC_MESSAGES/dynamic-panel-transparency.mo | Bin 0 -> 2542 bytes .../LC_MESSAGES/dynamic-panel-transparency.po | 235 ++ .../LC_MESSAGES/dynamic-panel-transparency.mo | Bin 0 -> 2464 bytes .../LC_MESSAGES/dynamic-panel-transparency.po | 233 ++ .../LC_MESSAGES/dynamic-panel-transparency.mo | Bin 0 -> 2483 bytes .../LC_MESSAGES/dynamic-panel-transparency.po | 208 ++ .../LC_MESSAGES/dynamic-panel-transparency.mo | Bin 0 -> 2510 bytes .../LC_MESSAGES/dynamic-panel-transparency.po | 236 ++ .../LC_MESSAGES/dynamic-panel-transparency.mo | Bin 0 -> 1324 bytes .../LC_MESSAGES/dynamic-panel-transparency.po | 177 ++ .../LC_MESSAGES/dynamic-panel-transparency.mo | Bin 0 -> 3246 bytes .../LC_MESSAGES/dynamic-panel-transparency.po | 196 ++ .../LC_MESSAGES/dynamic-panel-transparency.mo | Bin 0 -> 2669 bytes .../LC_MESSAGES/dynamic-panel-transparency.po | 196 ++ .../LC_MESSAGES/dynamic-panel-transparency.mo | Bin 0 -> 2318 bytes .../LC_MESSAGES/dynamic-panel-transparency.po | 232 ++ .../metadata.json | 13 + .../prefs.js | 370 +++ .../prefs.ui | 1053 +++++++ .../schemas/gschemas.compiled | Bin 0 -> 1756 bytes ...ons.dynamic-panel-transparency.gschema.xml | 119 + .../settings.js | 269 ++ .../styles/background/panel-custom.dpt.css | 2 + .../styles/background/panel.dpt.css | 2 + .../foreground/panel-icon-shadow.dpt.css | 2 + .../panel-maximized-text-color.dpt.css | 3 + .../foreground/panel-text-color.dpt.css | 3 + .../foreground/panel-text-shadow.dpt.css | 1 + .../panel-transition-duration.dpt.css | 1 + .../stylesheet.css | 22 + .../theming.js | 505 +++ .../transitions.js | 274 ++ .../util.js | 240 ++ .../extension.js | 47 + .../metadata.json | 18 + .../prefs.js | 82 + .../schemas/gschemas.compiled | Bin 0 -> 332 bytes ...xtensions.go-to-last-workspace.gschema.xml | 12 + .../utils.js | 46 + .../gsconnect@andyholmes.github.io/config.js | 19 + .../extension.js | 455 +++ .../gsconnect-preferences | 114 + .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 4545 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 18968 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 12420 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 15539 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 12683 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 17659 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 17948 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 14551 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 393 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 14635 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 15657 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 10072 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 12811 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 15065 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 15723 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 7966 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 14894 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 16200 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 15340 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 18738 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 12325 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 12495 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 10271 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 14874 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 16559 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 14101 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 11004 bytes .../metadata.json | 11 + .../nautilus-gsconnect.py | 183 ++ ...gnome.Shell.Extensions.GSConnect.gresource | Bin 0 -> 54612 bytes .../preferences/__init__.js | 41 + .../preferences/device.js | 1108 +++++++ .../preferences/keybindings.js | 312 ++ .../preferences/service.js | 657 ++++ .../gsconnect@andyholmes.github.io/prefs.js | 28 + .../schemas/gschemas.compiled | Bin 0 -> 5077 bytes ...ome.Shell.Extensions.GSConnect.gschema.xml | 187 ++ .../service/__init__.js | 401 +++ .../service/backends/lan.js | 985 ++++++ .../service/components/__init__.js | 67 + .../service/components/atspi.js | 312 ++ .../service/components/clipboard.js | 283 ++ .../service/components/contacts.js | 703 +++++ .../service/components/input.js | 641 ++++ .../service/components/mpris.js | 1029 ++++++ .../service/components/notification.js | 440 +++ .../service/components/pulseaudio.js | 265 ++ .../service/components/session.js | 116 + .../service/components/sound.js | 185 ++ .../service/components/upower.js | 226 ++ .../service/core.js | 738 +++++ .../service/daemon.js | 728 +++++ .../service/device.js | 1051 +++++++ .../service/manager.js | 500 +++ .../service/nativeMessagingHost.js | 215 ++ .../service/plugin.js | 258 ++ .../service/plugins/battery.js | 429 +++ .../service/plugins/clipboard.js | 178 ++ .../service/plugins/connectivity_report.js | 162 + .../service/plugins/contacts.js | 456 +++ .../service/plugins/findmyphone.js | 245 ++ .../service/plugins/mousepad.js | 319 ++ .../service/plugins/mpris.js | 902 ++++++ .../service/plugins/notification.js | 713 +++++ .../service/plugins/photo.js | 241 ++ .../service/plugins/ping.js | 69 + .../service/plugins/presenter.js | 52 + .../service/plugins/runcommand.js | 250 ++ .../service/plugins/sftp.js | 565 ++++ .../service/plugins/share.js | 483 +++ .../service/plugins/sms.js | 527 ++++ .../service/plugins/systemvolume.js | 200 ++ .../service/plugins/telephony.js | 241 ++ .../service/ui/__init__.js | 49 + .../service/ui/contacts.js | 638 ++++ .../service/ui/legacyMessaging.js | 223 ++ .../service/ui/messaging.js | 1312 ++++++++ .../service/ui/mousepad.js | 299 ++ .../service/ui/notification.js | 174 + .../service/ui/service.js | 248 ++ .../service/utils/dbus.js | 279 ++ .../service/utils/uri.js | 167 + .../shell/__init__.js | 43 + .../shell/clipboard.js | 380 +++ .../shell/device.js | 379 +++ .../shell/gmenu.js | 649 ++++ .../shell/keybindings.js | 102 + .../shell/notification.js | 439 +++ .../shell/tooltip.js | 302 ++ .../shell/utils.js | 218 ++ .../stylesheet.css | 122 + .../utils/remote.js | 516 +++ .../extension.js | 47 + .../metadata.json | 12 + .../convenience.js | 93 + .../extension.js | 71 + .../metadata.json | 14 + .../prefs.js | 140 + .../schemas/gschemas.compiled | Bin 0 -> 429 bytes .../extension.js | 57 + .../metadata.json | 14 + .../base.js | 729 +++++ .../convenience.js | 352 +++ .../extension.js | 267 ++ .../icons/blank.png | Bin 0 -> 133 bytes .../license | 674 ++++ .../sound-output-device-chooser.mo | Bin 0 -> 1582 bytes .../sound-output-device-chooser.mo | Bin 0 -> 1661 bytes .../sound-output-device-chooser.mo | Bin 0 -> 1594 bytes .../sound-output-device-chooser.mo | Bin 0 -> 2891 bytes .../sound-output-device-chooser.mo | Bin 0 -> 3129 bytes .../sound-output-device-chooser.mo | Bin 0 -> 1642 bytes .../sound-output-device-chooser.mo | Bin 0 -> 1632 bytes .../sound-output-device-chooser.mo | Bin 0 -> 1883 bytes .../sound-output-device-chooser.mo | Bin 0 -> 4780 bytes .../metadata.json | 19 + .../prefs.js | 328 ++ .../schemas/gschemas.compiled | Bin 0 -> 1248 bytes ...ns.sound-output-device-chooser.gschema.xml | 101 + .../ui/prefs-dialog.glade | 956 ++++++ .../ui/prefs-dialog40.glade | 655 ++++ .../libpulse_introspect.cpython-310.pyc | Bin 0 -> 9748 bytes .../libpulse_introspect.cpython-39.pyc | Bin 0 -> 9908 bytes .../utils/libpulse_introspect.py | 544 ++++ .../utils/pa_helper.py | 141 + .../README | 6 + .../compat.js | 51 + .../convenience.js | 94 + .../extension.js | 2592 +++++++++++++++ .../gpu_usage.sh | 49 + .../locale/ar/LC_MESSAGES/system-monitor.mo | Bin 0 -> 4303 bytes .../locale/ca/LC_MESSAGES/system-monitor.mo | Bin 0 -> 4243 bytes .../locale/cs/LC_MESSAGES/system-monitor.mo | Bin 0 -> 3909 bytes .../locale/de/LC_MESSAGES/system-monitor.mo | Bin 0 -> 4208 bytes .../es_ES/LC_MESSAGES/system-monitor.mo | Bin 0 -> 4264 bytes .../es_MX/LC_MESSAGES/system-monitor.mo | Bin 0 -> 1962 bytes .../locale/fa/LC_MESSAGES/system-monitor.mo | Bin 0 -> 2536 bytes .../locale/fi/LC_MESSAGES/system-monitor.mo | Bin 0 -> 4250 bytes .../locale/fr/LC_MESSAGES/system-monitor.mo | Bin 0 -> 6292 bytes .../locale/hu/LC_MESSAGES/system-monitor.mo | Bin 0 -> 5927 bytes .../locale/it/LC_MESSAGES/system-monitor.mo | Bin 0 -> 4330 bytes .../locale/ja/LC_MESSAGES/system-monitor.mo | Bin 0 -> 3639 bytes .../locale/ko/LC_MESSAGES/system-monitor.mo | Bin 0 -> 5643 bytes .../nl_NL/LC_MESSAGES/system-monitor.mo | Bin 0 -> 4335 bytes .../locale/pl/LC_MESSAGES/system-monitor.mo | Bin 0 -> 2228 bytes .../locale/pt/LC_MESSAGES/system-monitor.mo | Bin 0 -> 4246 bytes .../pt_BR/LC_MESSAGES/system-monitor.mo | Bin 0 -> 4265 bytes .../locale/ro/LC_MESSAGES/system-monitor.mo | Bin 0 -> 2186 bytes .../locale/ru/LC_MESSAGES/system-monitor.mo | Bin 0 -> 4672 bytes .../locale/sk/LC_MESSAGES/system-monitor.mo | Bin 0 -> 4331 bytes .../locale/tr/LC_MESSAGES/system-monitor.mo | Bin 0 -> 4412 bytes .../locale/uk/LC_MESSAGES/system-monitor.mo | Bin 0 -> 4835 bytes .../zh_CN/LC_MESSAGES/system-monitor.mo | Bin 0 -> 5603 bytes .../metadata.json | 19 + .../prefs.js | 553 ++++ .../schemas/gschemas.compiled | Bin 0 -> 6516 bytes ...hell.extensions.system-monitor.gschema.xml | 463 +++ .../stylesheet.css | 204 ++ .../AppManager.js | 152 + .../TrayIndicator.js | 192 ++ .../extension.js | 97 + .../metadata.json | 16 + .../preferences/AppChooser.js | 56 + .../preferences/AppRow.js | 54 + .../preferences/AppRow.xml | 92 + .../preferences/Prefs.css | 6 + .../preferences/Prefs.js | 124 + .../preferences/Prefs.xml | 566 ++++ .../trayIconsReloaded@selfmade.pl/prefs.js | 8 + .../schemas/gschemas.compiled | Bin 0 -> 1204 bytes ...l.extensions.trayIconsReloaded.gschema.xml | 68 + .../stylesheet.css | 11 + .../extensions/workspaces-bar@fthx/README.md | 4 + .../workspaces-bar@fthx/extension.js | 146 + .../workspaces-bar@fthx/metadata.json | 14 + .../workspaces-bar@fthx/stylesheet.css | 67 + .../gnome-shell/gnome-overrides-migrated | 0 330 files changed, 61257 insertions(+) create mode 100644 .config/brave-flags.conf create mode 100644 .config/dconf/user create mode 120000 .config/electron-flags.conf create mode 100644 .config/fish/config.fish create mode 100644 .config/fish/fish_variables create mode 100644 .config/fish/functions/fish_prompt.fish create mode 100644 .config/pop-shell/config.json create mode 100644 .local/share/gnome-shell/extensions/Move_Clock@rmy.pobox.com/extension.js create mode 100644 .local/share/gnome-shell/extensions/Move_Clock@rmy.pobox.com/metadata.json create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/LICENCE.txt create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/README.md create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/extension.js create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/icons/arch-error-symbolic.svg create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/icons/arch-lit-symbolic.svg create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/icons/arch-unknown-symbolic.svg create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/icons/arch-updates-symbolic.svg create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/icons/arch-uptodate-symbolic.svg create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/ar/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/ca/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/cs_CZ/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/de_DE/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/eo/LC_MESSAGES/arch-update.mo create mode 100755 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/es/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/et/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/et/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/fa_IR/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/fi_FI/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/fr/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/he_IL/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/he_IL/LC_MESSAGES/he_IL.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/it_IT/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/ko/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/nb_NO/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/nl/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/pl/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/pt_BR/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/ro/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/ru_RU/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/sk/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/sr/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/sr@latin/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/sv/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/tr_TR/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/uk_UA/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/zh_CN/LC_MESSAGES/arch-update.mo create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/metadata.json create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/prefs.js create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/prefs.xml create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/schemas/gschemas.compiled create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/schemas/org.gnome.shell.extensions.arch-update.gschema.xml create mode 100644 .local/share/gnome-shell/extensions/arch-update@RaphaelRochet/stylesheet.css create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/COPYING create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/README.md create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/Settings.ui create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/appIconIndicators.js create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/appIcons.js create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/dash.js create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/dbusmenuUtils.js create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/docking.js create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/extension.js create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/fileManager1API.js create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/intellihide.js create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/launcherAPI.js create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/ar/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/cs/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/de/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/el/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/es/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/eu/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/fr/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/gl/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/hu/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/id/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/it/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/ja/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/nb/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/nl/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/pl/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/pt/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/pt_BR/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/ru/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/sk/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/sr/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/sr@latin/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/sv/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/tr/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/uk_UA/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/zh_CN/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/zh_TW/LC_MESSAGES/dashtodock.mo create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locations.js create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/media/glossy.svg create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/media/highlight_stacked_bg.svg create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/media/highlight_stacked_bg_h.svg create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/media/logo.svg create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/metadata.json create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/prefs.js create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/schemas/gschemas.compiled create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/schemas/org.gnome.shell.extensions.dash-to-dock.gschema.xml create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/stylesheet.css create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/theming.js create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/utils.js create mode 100644 .local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/windowPreview.js create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/COPYING create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/LICENSE create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/convenience.js create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/dynamic-panel-transparency.pot create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/events.js create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/extension.js create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/intellifade.js create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/ar/LC_MESSAGES/dynamic-panel-transparency.mo create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/ar/LC_MESSAGES/dynamic-panel-transparency.po create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/de/LC_MESSAGES/dynamic-panel-transparency.mo create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/de/LC_MESSAGES/dynamic-panel-transparency.po create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/es/LC_MESSAGES/dynamic-panel-transparency.mo create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/es/LC_MESSAGES/dynamic-panel-transparency.po create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/fr/LC_MESSAGES/dynamic-panel-transparency.mo create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/fr/LC_MESSAGES/dynamic-panel-transparency.po create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/pt_BR/LC_MESSAGES/dynamic-panel-transparency.mo create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/pt_BR/LC_MESSAGES/dynamic-panel-transparency.po create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/ru/LC_MESSAGES/dynamic-panel-transparency.mo create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/ru/LC_MESSAGES/dynamic-panel-transparency.po create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/sr/LC_MESSAGES/dynamic-panel-transparency.mo create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/sr/LC_MESSAGES/dynamic-panel-transparency.po create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/sr@latin/LC_MESSAGES/dynamic-panel-transparency.mo create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/sr@latin/LC_MESSAGES/dynamic-panel-transparency.po create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/zh_CN/LC_MESSAGES/dynamic-panel-transparency.mo create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/zh_CN/LC_MESSAGES/dynamic-panel-transparency.po create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/metadata.json create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/prefs.js create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/prefs.ui create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/schemas/gschemas.compiled create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/schemas/org.gnome.shell.extensions.dynamic-panel-transparency.gschema.xml create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/settings.js create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/background/panel-custom.dpt.css create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/background/panel.dpt.css create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/foreground/panel-icon-shadow.dpt.css create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/foreground/panel-maximized-text-color.dpt.css create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/foreground/panel-text-color.dpt.css create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/foreground/panel-text-shadow.dpt.css create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/transitions/panel-transition-duration.dpt.css create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/stylesheet.css create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/theming.js create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/transitions.js create mode 100644 .local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/util.js create mode 100644 .local/share/gnome-shell/extensions/gnome-shell-go-to-last-workspace@github.com/extension.js create mode 100644 .local/share/gnome-shell/extensions/gnome-shell-go-to-last-workspace@github.com/metadata.json create mode 100644 .local/share/gnome-shell/extensions/gnome-shell-go-to-last-workspace@github.com/prefs.js create mode 100644 .local/share/gnome-shell/extensions/gnome-shell-go-to-last-workspace@github.com/schemas/gschemas.compiled create mode 100644 .local/share/gnome-shell/extensions/gnome-shell-go-to-last-workspace@github.com/schemas/org.gnome.shell.extensions.go-to-last-workspace.gschema.xml create mode 100644 .local/share/gnome-shell/extensions/gnome-shell-go-to-last-workspace@github.com/utils.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/config.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/extension.js create mode 100755 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/gsconnect-preferences create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/ar/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/be/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/ca/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/cs/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/da/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/de/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/es/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/et/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/fa/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/fi/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/fr/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/gl/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/hu/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/it/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/lt/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/nl_BE/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/nl_NL/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/pl/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/pt_BR/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/ru/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/sk/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/sr/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/sr@latin/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/tr/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/uk/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/zh_CN/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/zh_TW/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/metadata.json create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/nautilus-gsconnect.py create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/org.gnome.Shell.Extensions.GSConnect.gresource create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/preferences/__init__.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/preferences/device.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/preferences/keybindings.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/preferences/service.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/prefs.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/schemas/gschemas.compiled create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/schemas/org.gnome.Shell.Extensions.GSConnect.gschema.xml create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/__init__.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/backends/lan.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/__init__.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/atspi.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/clipboard.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/contacts.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/input.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/mpris.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/notification.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/pulseaudio.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/session.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/sound.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/upower.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/core.js create mode 100755 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/daemon.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/device.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/manager.js create mode 100755 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/nativeMessagingHost.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugin.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/battery.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/clipboard.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/connectivity_report.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/contacts.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/findmyphone.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/mousepad.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/mpris.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/notification.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/photo.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/ping.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/presenter.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/runcommand.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/sftp.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/share.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/sms.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/systemvolume.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/telephony.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/__init__.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/contacts.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/legacyMessaging.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/messaging.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/mousepad.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/notification.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/service.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/utils/dbus.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/utils/uri.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/__init__.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/clipboard.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/device.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/gmenu.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/keybindings.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/notification.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/tooltip.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/utils.js create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/stylesheet.css create mode 100644 .local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/utils/remote.js create mode 100644 .local/share/gnome-shell/extensions/instantworkspaceswitcher@amalantony.net/extension.js create mode 100644 .local/share/gnome-shell/extensions/instantworkspaceswitcher@amalantony.net/metadata.json create mode 100644 .local/share/gnome-shell/extensions/move-workspaceSwitcherPopup@GithubUser699.github.com/convenience.js create mode 100644 .local/share/gnome-shell/extensions/move-workspaceSwitcherPopup@GithubUser699.github.com/extension.js create mode 100644 .local/share/gnome-shell/extensions/move-workspaceSwitcherPopup@GithubUser699.github.com/metadata.json create mode 100644 .local/share/gnome-shell/extensions/move-workspaceSwitcherPopup@GithubUser699.github.com/prefs.js create mode 100644 .local/share/gnome-shell/extensions/move-workspaceSwitcherPopup@GithubUser699.github.com/schemas/gschemas.compiled create mode 100644 .local/share/gnome-shell/extensions/notification-position@drugo.dev/extension.js create mode 100644 .local/share/gnome-shell/extensions/notification-position@drugo.dev/metadata.json create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/base.js create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/convenience.js create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/extension.js create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/icons/blank.png create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/license create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/de_DE/LC_MESSAGES/sound-output-device-chooser.mo create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/it_IT/LC_MESSAGES/sound-output-device-chooser.mo create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/ko/LC_MESSAGES/sound-output-device-chooser.mo create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/nl/LC_MESSAGES/sound-output-device-chooser.mo create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/pt_BR/LC_MESSAGES/sound-output-device-chooser.mo create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/pt_PT/LC_MESSAGES/sound-output-device-chooser.mo create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/sk/LC_MESSAGES/sound-output-device-chooser.mo create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/sv/LC_MESSAGES/sound-output-device-chooser.mo create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/ta/LC_MESSAGES/sound-output-device-chooser.mo create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/metadata.json create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/prefs.js create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/schemas/gschemas.compiled create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/schemas/org.gnome.shell.extensions.sound-output-device-chooser.gschema.xml create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/ui/prefs-dialog.glade create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/ui/prefs-dialog40.glade create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/utils/__pycache__/libpulse_introspect.cpython-310.pyc create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/utils/__pycache__/libpulse_introspect.cpython-39.pyc create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/utils/libpulse_introspect.py create mode 100644 .local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/utils/pa_helper.py create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/README create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/compat.js create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/convenience.js create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/extension.js create mode 100755 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/gpu_usage.sh create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/ar/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/ca/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/cs/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/de/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/es_ES/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/es_MX/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/fa/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/fi/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/fr/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/hu/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/it/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/ja/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/ko/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/nl_NL/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/pl/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/pt/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/pt_BR/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/ro/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/ru/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/sk/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/tr/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/uk/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/zh_CN/LC_MESSAGES/system-monitor.mo create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/metadata.json create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/prefs.js create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/schemas/gschemas.compiled create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/schemas/org.gnome.shell.extensions.system-monitor.gschema.xml create mode 100644 .local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/stylesheet.css create mode 100644 .local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/AppManager.js create mode 100644 .local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/TrayIndicator.js create mode 100644 .local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/extension.js create mode 100644 .local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/metadata.json create mode 100644 .local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/AppChooser.js create mode 100644 .local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/AppRow.js create mode 100644 .local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/AppRow.xml create mode 100644 .local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/Prefs.css create mode 100644 .local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/Prefs.js create mode 100644 .local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/Prefs.xml create mode 100644 .local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/prefs.js create mode 100644 .local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/schemas/gschemas.compiled create mode 100644 .local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/schemas/org.gnome.shell.extensions.trayIconsReloaded.gschema.xml create mode 100644 .local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/stylesheet.css create mode 100644 .local/share/gnome-shell/extensions/workspaces-bar@fthx/README.md create mode 100644 .local/share/gnome-shell/extensions/workspaces-bar@fthx/extension.js create mode 100644 .local/share/gnome-shell/extensions/workspaces-bar@fthx/metadata.json create mode 100644 .local/share/gnome-shell/extensions/workspaces-bar@fthx/stylesheet.css create mode 100644 .local/share/gnome-shell/gnome-overrides-migrated diff --git a/.config/brave-flags.conf b/.config/brave-flags.conf new file mode 100644 index 0000000..1af1827 --- /dev/null +++ b/.config/brave-flags.conf @@ -0,0 +1 @@ +--enable-features=UseOzonePlatform --ozone-platform=wayland diff --git a/.config/dconf/user b/.config/dconf/user new file mode 100644 index 0000000000000000000000000000000000000000..656dfd0ae9ff16273afeb1505066a418247c1147 GIT binary patch literal 60309 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 k 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 echo here + + # To: + # ┬─[nim@Hattori:~/w/dashboard]─[11:37:14]─[V:django20]─[G:master↑1|●1✚1…1]─[B:85%, 05:41:42 remaining] + # │ 2 15054 0% arrêtée sleep 100000 + # │ 1 15048 0% arrêtée sleep 100000 + # ╰─>$ echo there + + set -l retc red + test $status = 0; and set retc green + + set -q __fish_git_prompt_showupstream + or set -g __fish_git_prompt_showupstream auto + + function _nim_prompt_wrapper + set retc $argv[1] + set -l field_name $argv[2] + set -l field_value $argv[3] + + set_color normal + set_color $retc + echo -n '─' + set_color -o green + echo -n '[' + set_color normal + test -n $field_name + and echo -n $field_name: + set_color $retc + echo -n $field_value + set_color -o green + echo -n ']' + end + + set_color $retc + echo -n '┬─' + set_color -o green + echo -n [ + + if functions -q fish_is_root_user; and fish_is_root_user + set_color -o red + else + set_color -o yellow + end + + echo -n $USER + set_color -o white + echo -n @ + + if [ -z "$SSH_CLIENT" ] + set_color -o blue + else + set_color -o cyan + end + + echo -n (prompt_hostname) + set_color -o white + echo -n :(prompt_pwd) + set_color -o green + echo -n ']' + + # Date + _nim_prompt_wrapper $retc '' (date +%X) + + # Vi-mode + # The default mode prompt would be prefixed, which ruins our alignment. + function fish_mode_prompt + end + + if test "$fish_key_bindings" = fish_vi_key_bindings + or test "$fish_key_bindings" = fish_hybrid_key_bindings + set -l mode + switch $fish_bind_mode + case default + set mode (set_color --bold red)N + case insert + set mode (set_color --bold green)I + case replace_one + set mode (set_color --bold green)R + echo '[R]' + case replace + set mode (set_color --bold cyan)R + case visual + set mode (set_color --bold magenta)V + end + set mode $mode(set_color normal) + _nim_prompt_wrapper $retc '' $mode + end + + + # Virtual Environment + set -q VIRTUAL_ENV_DISABLE_PROMPT + or set -g VIRTUAL_ENV_DISABLE_PROMPT true + set -q VIRTUAL_ENV + and _nim_prompt_wrapper $retc V (basename "$VIRTUAL_ENV") + + # git + set -l prompt_git (fish_git_prompt '%s') + test -n "$prompt_git" + and _nim_prompt_wrapper $retc G $prompt_git + + # Battery status + type -q acpi + and test (acpi -a 2> /dev/null | string match -r off) + and _nim_prompt_wrapper $retc B (acpi -b | cut -d' ' -f 4- | tail -n 1) + + # New line + echo + + # Background jobs + set_color normal + + for job in (jobs) + set_color $retc + echo -n '│ ' + set_color brown + echo $job + end + + set_color normal + set_color $retc + echo -n '╰─>' + set_color -o red + echo -n '$ ' + set_color normal +end diff --git a/.config/pop-shell/config.json b/.config/pop-shell/config.json new file mode 100644 index 0000000..8c23b67 --- /dev/null +++ b/.config/pop-shell/config.json @@ -0,0 +1,43 @@ +{ + "float": [ + { + "class": "pop-shell-example", + "title": "pop-shell-example" + }, + { + "class": "albert", + "title": "albert — Albert" + }, + { + "class": "wofi", + "title": "drun" + }, + { + "class": "VirtualBox Manager" + }, + { + "class": "obs", + "title": "Windowed Projector (Preview)" + }, + { + "class": "X32-Edit" + }, + { + "class": "pensela" + }, + { + "class": "lmms" + }, + { + "class": "wofi" + }, + { + "class": "wofi", + "title": "dmenu" + } + ], + "skiptaskbarhidden": [], + "log_on_focus": false, + "move_pointer_on_switch": false, + "default_pointer_position": "TOP_LEFT" +} \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/Move_Clock@rmy.pobox.com/extension.js b/.local/share/gnome-shell/extensions/Move_Clock@rmy.pobox.com/extension.js new file mode 100644 index 0000000..ed336d2 --- /dev/null +++ b/.local/share/gnome-shell/extensions/Move_Clock@rmy.pobox.com/extension.js @@ -0,0 +1,46 @@ +// Copyright (C) 2011-2017 R M Yorston +// Licence: GPLv2+ + +const Main = imports.ui.main; +const SessionMode = imports.ui.sessionMode; + +function init() { +} + +function enable() { + // do nothing if the clock isn't centred in this mode + if ( Main.sessionMode.panel.center.indexOf('dateMenu') == -1 ) { + return; + } + + let centerBox = Main.panel._centerBox; + let rightBox = Main.panel._rightBox; + let dateMenu = Main.panel.statusArea['dateMenu']; + let children = centerBox.get_children(); + + // only move the clock if it's in the centre box + if ( children.indexOf(dateMenu.container) != -1 ) { + centerBox.remove_actor(dateMenu.container); + + children = rightBox.get_children(); + rightBox.insert_child_at_index(dateMenu.container, children.length-1); + } +} + +function disable() { + // do nothing if the clock isn't centred in this mode + if ( Main.sessionMode.panel.center.indexOf('dateMenu') == -1 ) { + return; + } + + let centerBox = Main.panel._centerBox; + let rightBox = Main.panel._rightBox; + let dateMenu = Main.panel.statusArea['dateMenu']; + let children = rightBox.get_children(); + + // only move the clock back if it's in the right box + if ( children.indexOf(dateMenu.container) != -1 ) { + rightBox.remove_actor(dateMenu.container); + centerBox.add_actor(dateMenu.container); + } +} diff --git a/.local/share/gnome-shell/extensions/Move_Clock@rmy.pobox.com/metadata.json b/.local/share/gnome-shell/extensions/Move_Clock@rmy.pobox.com/metadata.json new file mode 100644 index 0000000..785d9cc --- /dev/null +++ b/.local/share/gnome-shell/extensions/Move_Clock@rmy.pobox.com/metadata.json @@ -0,0 +1,12 @@ +{ + "_generated": "Generated by SweetTooth, do not edit", + "description": "Move clock to left of status menu button", + "name": "Frippery Move Clock", + "shell-version": [ + "40", + "41" + ], + "url": "http://frippery.org/extensions", + "uuid": "Move_Clock@rmy.pobox.com", + "version": 25 +} \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/LICENCE.txt b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/LICENCE.txt new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/LICENCE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/README.md b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/README.md new file mode 100644 index 0000000..4f46661 --- /dev/null +++ b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/README.md @@ -0,0 +1,212 @@ +# arch-update +Update indicator for Arch Linux and GNOME Shell + +## Features +- Uses pacman's «checkupdates» by default and thus does not need root access +- Optional update count display on panel +- Optional notification on new updates (defaults to off) +- Launcher for your favorite update command +- Comes in English, French, Czech, German, Spanish, Brazilian Portuguese, Italian, Polish, Romanian, Arabic, Slovak, Chinese, Serbian, Swedish, Norwegian Bokmal, Russian, Persian, Turkish, Esperanto, Finnish, Dutch, Ukrainian, Korean languages. (Thanks translators !) + +## Requirements +If you use the default "checkupdates" way you will need to install "pacman-contrib". + +## One-click install +It's on extensions.gnome.org : +https://extensions.gnome.org/extension/1010/archlinux-updates-indicator/ + +## Install from AUR +Thanks to michiwend you can install it from Arch Linux User Repository : gnome-shell-extension-arch-update +https://aur.archlinux.org/packages/gnome-shell-extension-arch-update/ + +## Manual install +To install, simply download as zip and unzip contents in ~/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet + +## Changes + +### v45 +- Fixed an error on unloading introduced in v44 + +### v44 +- Minor refactoring + +### v43 +- Gnome 41 +- New translations : Dutch, Korean, Ukrainian +- Updated translations : Simplified Chinese, Russian + +### v42 +- Updated translation : German + +### v41 +- Fixed metadata for extensions website + +### v40 +- Gnome 40 only +- Updated translation : Russian + +### v39 +- Fixed update list empty after suspend +- Fixed update list not fully visible when lots of updates +- Updated translations : Chinese and Spanish + +### v38 +- Fixed crash about Gtk.IconTheme.get_defaults +- Added indicator position setting + +### v37 +- Theme support is back ! Also an option to force built-in icons if needed. + +### v36 +- Gnome 3.36.1 only +- Fixed open prefs from menu + +### v35 +- Gnome 3.36 only +- Fixed a warning about absolete call + +### v34 +- Gnome 3.36 +- New translation : Swedish +- Updated translations : Italian, German + +### v33 +- Removed deprecated code +- Removed support for older GS + +### v32 +- Gnome 3.34 + +### v31 +- Updated translation : Turkish + +### v30 +- Gnome 3.32 + +### v29 +- Update translation : Romanian +- Applied French translation to all French + +### v28 +- Gnome 3.30 +- New translation : Esperanto +- New translation : Finnish +- Updated translation : Brazilian +- Fix indicator alignment +- Fix some errors that could quickly fill log + +### v27 +- Added info about pacman-contrib for checkupdates script +- New translation : Estonian +- Updated translation : Romanian + +### v26 +- Gnome 3.28 +- New translation : Hebrew +- Update translation : Spanish + +### v25 +- Added optional package manager menu entry +- Added requirements in readme +- Updated Slovak translation +- Updated Italian translation +- Fixed a JS Warning +- Fixed a bug that crashes Gnome-SHELL on update + +### v24 +- Gnome 3.26 +- Updated Romanian translation + +### v23 +- Updated translation : Arabic + +### v22 +- Updated translation : Serbian +- New translation : Turkish + +### v21 +- Gnome 3.24 +- New translation : Persian + +### v20 +- Translations updates (German, Spanish) + +### v19 +- Ability to cancel checking +- New translation : Catalan +- Updated translations : Spanish, Brazilian + +### v18 +- Gnome 3.22 +- New preferences window +- Cleaner translations (some text are not translated yet) +- Menu does not close when updating + +### v17 +- New translation : Russian +- Updated translation : Czech + +### v16 +- Add vertical scroll bar on preferences window + +### v15 +- New feature : auto-expand update list +- New translation : Norwegian Bokmal +- Updated translation : Brazilian Portuguese + +### v14 +- Gnome 3.20 compatibility + +### v13 +- New translation : Serbian (sr and sr@latin) +- Updated translation : Spanish +- Minor bug fix + +### v12 +- New translation : Chinese +- Updated translation : Czech + +### v11 +- New option to strip out version numbers +- New translations : Slovak and Arabic +- Updated translations : Brazilian Portuguese, German + +### v10 +- Licence added : GNU GPL v3 +- Updated translations : Polish and Brazilian portuguese + +### v9 +- Added option to change command used to check for updates (for advanced users) +- Added Romanian and Polish translations + +### v8 +- Added Italian language + +### v7 +- Added Brazilian Portuguese translation + +### v6 +- Added Spanish language + +### v5 +- Option to have permanent notifications +- Asynchronous checking - No more 1 sec Shell freeze during updates check ! +- 'Updates pending' menu item can now be expanded to show updates list +- Option to only list new updates in notifications +- Aded "Update Now" action button on notifications + +### v4 +- Run update command from indicator +- Autodetect when updates are done +- Prefs dialog reworked + +### v3 +- Notification option +- Czech and German languages added + +## Credits +All icons are based on Thayer Williams' Archer logo, winner of Arch Linux logo contest. + +Some portions of the extension were inspired from Touchad Indicator and Lock keys. +https://github.com/orangeshirt/gnome-shell-extension-touchpad-indicator +https://github.com/kazysmaster/gnome-shell-extension-lockkeys diff --git a/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/extension.js b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/extension.js new file mode 100644 index 0000000..3cb0344 --- /dev/null +++ b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/extension.js @@ -0,0 +1,509 @@ +/* + This file is part of Arch Linux Updates Indicator + + Arch Linux Updates Indicator is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Arch Linux Updates Indicator is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Arch Linux Updates Indicator. If not, see . + + Copyright 2016 Raphaël Rochet +*/ + +const Clutter = imports.gi.Clutter; + +const St = imports.gi.St; +const GObject = imports.gi.GObject; +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; +const Gtk = imports.gi.Gtk; + +const Main = imports.ui.main; +const Panel = imports.ui.panel; +const PanelMenu = imports.ui.panelMenu; +const PopupMenu = imports.ui.popupMenu; +const MessageTray = imports.ui.messageTray; + +const Util = imports.misc.util; +const ExtensionUtils = imports.misc.extensionUtils; +const ExtensionManager = imports.ui.main.extensionManager; +const Me = ExtensionUtils.getCurrentExtension(); + +const Format = imports.format; +const Gettext = imports.gettext.domain('arch-update'); +const _ = Gettext.gettext; + +/* Options */ +let ALWAYS_VISIBLE = true; +let USE_BUILDIN_ICONS = true; +let SHOW_COUNT = true; +let BOOT_WAIT = 15; // 15s +let CHECK_INTERVAL = 60*60; // 1h +let NOTIFY = false; +let HOWMUCH = 0; +let TRANSIENT = true; +let UPDATE_CMD = "gnome-terminal -e 'sh -c \"sudo pacman -Syu ; echo Done - Press enter to exit; read _\" '"; +let CHECK_CMD = "/usr/bin/checkupdates"; +let MANAGER_CMD = ""; +let PACMAN_DIR = "/var/lib/pacman/local"; +let STRIP_VERSIONS = true; +let AUTO_EXPAND_LIST = 0; + +/* Variables we want to keep when extension is disabled (eg during screen lock) */ +let FIRST_BOOT = 1; +let UPDATES_PENDING = -1; +let UPDATES_LIST = []; + + +function init() { + String.prototype.format = Format.format; + ExtensionUtils.initTranslations("arch-update"); +} + +const ArchUpdateIndicator = GObject.registerClass( + { + _TimeoutId: null, + _FirstTimeoutId: null, + _updateProcess_sourceId: null, + _updateProcess_stream: null, + _updateProcess_pid: null, + _updateList: [], + }, +class ArchUpdateIndicator extends PanelMenu.Button { + + _init() { + super._init(0); + this.updateIcon = new St.Icon({gicon: this._getCustIcon('arch-unknown-symbolic'), style_class: 'system-status-icon'}); + + let box = new St.BoxLayout({ vertical: false, style_class: 'panel-status-menu-box' }); + this.label = new St.Label({ text: '', + y_expand: true, + y_align: Clutter.ActorAlign.CENTER }); + + box.add_child(this.updateIcon); + box.add_child(this.label); + this.add_child(box); + + // Prepare the special menu : a submenu for updates list that will look like a regular menu item when disabled + // Scrollability will also be taken care of by the popupmenu + this.menuExpander = new PopupMenu.PopupSubMenuMenuItem(''); + this.menuExpander.menu.box.style_class = 'arch-updates-list'; + + // Other standard menu items + let settingsMenuItem = new PopupMenu.PopupMenuItem(_('Settings')); + this.updateNowMenuItem = new PopupMenu.PopupMenuItem(_('Update now')); + this.managerMenuItem = new PopupMenu.PopupMenuItem(_('Open package manager')); + + // A special "Checking" menu item with a stop button + this.checkingMenuItem = new PopupMenu.PopupBaseMenuItem( {reactive:false} ); + let checkingLabel = new St.Label({ text: _('Checking') + " …" }); + let cancelButton = new St.Button({ + child: new St.Icon({ icon_name: 'process-stop-symbolic' }), + style_class: 'system-menu-action arch-updates-menubutton', + x_expand: true + }); + cancelButton.set_x_align(Clutter.ActorAlign.END); + this.checkingMenuItem.actor.add_actor( checkingLabel ); + this.checkingMenuItem.actor.add_actor( cancelButton ); + + // A little trick on "check now" menuitem to keep menu opened + this.checkNowMenuItem = new PopupMenu.PopupMenuItem( _('Check now') ); + this.checkNowMenuContainer = new PopupMenu.PopupMenuSection(); + this.checkNowMenuContainer.actor.add_actor(this.checkNowMenuItem.actor); + + // Assemble all menu items into the popup menu + this.menu.addMenuItem(this.menuExpander); + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + this.menu.addMenuItem(this.updateNowMenuItem); + this.menu.addMenuItem(this.checkingMenuItem); + this.menu.addMenuItem(this.checkNowMenuContainer); + this.menu.addMenuItem(this.managerMenuItem); + this.menu.addMenuItem(settingsMenuItem); + + // Bind some events + this.menu.connect('open-state-changed', this._onMenuOpened.bind(this)); + this.checkNowMenuItem.connect('activate', this._checkUpdates.bind(this)); + cancelButton.connect('clicked', this._cancelCheck.bind(this)); + settingsMenuItem.connect('activate', this._openSettings.bind(this)); + this.updateNowMenuItem.connect('activate', this._updateNow.bind(this)); + this.managerMenuItem.connect('activate', this._openManager.bind(this)); + + // Some initial status display + this._showChecking(false); + this._updateMenuExpander(false, _('Waiting first check')); + + // Restore previous updates list if any + this._updateList = UPDATES_LIST; + + // Load settings + this._settings = ExtensionUtils.getSettings('org.gnome.shell.extensions.arch-update'); + this._settings.connect('changed', this._positionChanged.bind(this)); + this._settingsChangedId = this._settings.connect('changed', this._applySettings.bind(this)); + this._applySettings(); + + // Start monitoring external changes + this._startFolderMonitor(); + + if (FIRST_BOOT) { + // Schedule first check only if this is the first extension load + // This won't be run again if extension is disabled/enabled (like when screen is locked) + let that = this; + this._FirstTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, BOOT_WAIT, function () { + that._checkUpdates(); + that._FirstTimeoutId = null; + FIRST_BOOT = 0; + return false; // Run once + }); + } + + } + + _getCustIcon(icon_name) { + // I did not find a way to lookup icon via Gio, so use Gtk + // I couldn't find why, but get_default is sometimes null, hence this additional test + if (!USE_BUILDIN_ICONS && Gtk.IconTheme.get_default()) { + if (Gtk.IconTheme.get_default().has_icon(icon_name)) { + return Gio.icon_new_for_string( icon_name ); + } + } + // Icon not available in theme, or user prefers built in icon + return Gio.icon_new_for_string( Me.dir.get_child('icons').get_path() + "/" + icon_name + ".svg" ); + } + + _positionChanged(){ + this.container.get_parent().remove_actor(this.container); + let boxes = { + 0: Main.panel._leftBox, + 1: Main.panel._centerBox, + 2: Main.panel._rightBox + }; + let p = this._settings.get_int('position'); + let i = this._settings.get_int('position-number'); + boxes[p].insert_child_at_index(this.container, i); + } + + _openSettings() { + Gio.DBus.session.call( + 'org.gnome.Shell.Extensions', + '/org/gnome/Shell/Extensions', + 'org.gnome.Shell.Extensions', + 'OpenExtensionPrefs', + new GLib.Variant('(ssa{sv})', [Me.uuid, '', {}]), + null, + Gio.DBusCallFlags.NONE, + -1, + null); + } + + _openManager() { + Util.spawnCommandLine(MANAGER_CMD); + } + + _updateNow() { + Util.spawnCommandLine(UPDATE_CMD); + } + + _applySettings() { + ALWAYS_VISIBLE = this._settings.get_boolean('always-visible'); + USE_BUILDIN_ICONS = this._settings.get_boolean('use-buildin-icons'); + SHOW_COUNT = this._settings.get_boolean('show-count'); + BOOT_WAIT = this._settings.get_int('boot-wait'); + CHECK_INTERVAL = 60 * this._settings.get_int('check-interval'); + NOTIFY = this._settings.get_boolean('notify'); + HOWMUCH = this._settings.get_int('howmuch'); + TRANSIENT = this._settings.get_boolean('transient'); + UPDATE_CMD = this._settings.get_string('update-cmd'); + CHECK_CMD = this._settings.get_string('check-cmd'); + MANAGER_CMD = this._settings.get_string('package-manager'); + PACMAN_DIR = this._settings.get_string('pacman-dir'); + STRIP_VERSIONS = this._settings.get_boolean('strip-versions'); + AUTO_EXPAND_LIST = this._settings.get_int('auto-expand-list'); + this.managerMenuItem.actor.visible = ( MANAGER_CMD != "" ); + this._checkShowHide(); + this._updateStatus(); + let that = this; + if (this._TimeoutId) GLib.source_remove(this._TimeoutId); + this._TimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, CHECK_INTERVAL, function () { + that._checkUpdates(); + return true; + }); + } + + destroy() { + this._settings.disconnect( this._settingsChangedId ); + if (this._notifSource) { + // Delete the notification source, which lay still have a notification shown + this._notifSource.destroy(); + this._notifSource = null; + }; + if (this.monitor) { + // Stop spying on pacman local dir + this.monitor.cancel(); + this.monitor = null; + } + if (this._updateProcess_sourceId) { + // We leave the checkupdate process end by itself but undef handles to avoid zombies + GLib.source_remove(this._updateProcess_sourceId); + this._updateProcess_sourceId = null; + this._updateProcess_stream = null; + } + if (this._FirstTimeoutId) { + GLib.source_remove(this._FirstTimeoutId); + this._FirstTimeoutId = null; + } + if (this._TimeoutId) { + GLib.source_remove(this._TimeoutId); + this._TimeoutId = null; + } + super.destroy(); + } + + _checkShowHide() { + if ( UPDATES_PENDING == -3 ) { + // Do not apply visibility change while checking for updates + return; + } + if (!ALWAYS_VISIBLE && UPDATES_PENDING < 1) { + this.visible = false; + } else { + this.visible = true; + } + this.label.visible = SHOW_COUNT && UPDATES_PENDING > 0; + } + + _onMenuOpened() { + // This event is fired when menu is shown or hidden + // Only open the submenu if the menu is being opened and there is something to show + this._checkAutoExpandList(); + } + + _checkAutoExpandList() { + if (this.menu.isOpen && UPDATES_PENDING > 0 && UPDATES_PENDING <= AUTO_EXPAND_LIST) { + this.menuExpander.setSubmenuShown(true); + } else { + this.menuExpander.setSubmenuShown(false); + } + } + + _startFolderMonitor() { + if (PACMAN_DIR) { + this.pacman_dir = Gio.file_new_for_path(PACMAN_DIR); + this.monitor = this.pacman_dir.monitor_directory(0, null); + this.monitor.connect('changed', this._onFolderChanged.bind(this)); + } + } + + _onFolderChanged() { + // Folder have changed ! Let's schedule a check in a few seconds + let that = this; + if (this._FirstTimeoutId) GLib.source_remove(this._FirstTimeoutId); + this._FirstTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 5, function () { + that._checkUpdates(); + that._FirstTimeoutId = null; + return false; + }); + } + + _showChecking(isChecking) { + if (isChecking == true) { + this.updateIcon.set_gicon( this._getCustIcon('arch-unknown-symbolic') ); + this.checkNowMenuContainer.actor.visible = false; + this.checkingMenuItem.actor.visible = true;; + } else { + this.checkNowMenuContainer.actor.visible = true;; + this.checkingMenuItem.actor.visible = false;; + } + } + + _updateStatus(updatesCount) { + updatesCount = typeof updatesCount === 'number' ? updatesCount : UPDATES_PENDING; + if (updatesCount > 0) { + // Updates pending + this.updateIcon.set_gicon( this._getCustIcon('arch-updates-symbolic') ); + this._updateMenuExpander( true, Gettext.ngettext( "%d update pending", "%d updates pending", updatesCount ).format(updatesCount) ); + this.label.set_text(updatesCount.toString()); + if (NOTIFY && UPDATES_PENDING < updatesCount) { + if (HOWMUCH > 0) { + let updateList = []; + if (HOWMUCH > 1) { + updateList = this._updateList; + } else { + // Keep only packets that was not in the previous notification + updateList = this._updateList.filter(function(pkg) { return UPDATES_LIST.indexOf(pkg) < 0 }); + } + if (updateList.length > 0) { + // Show notification only if there's new updates + this._showNotification( + Gettext.ngettext( "New Arch Linux Update", "New Arch Linux Updates", updateList.length ), + updateList.join(', ') + ); + } + } else { + this._showNotification( + Gettext.ngettext( "New Arch Linux Update", "New Arch Linux Updates", updatesCount ), + Gettext.ngettext( "There is %d update pending", "There are %d updates pending", updatesCount ).format(updatesCount) + ); + } + } + // Store the new list + UPDATES_LIST = this._updateList; + } else { + this.label.set_text(''); + if (updatesCount == -1) { + // Unknown + this.updateIcon.set_gicon( this._getCustIcon('arch-unknown-symbolic') ); + this._updateMenuExpander( false, '' ); + } else if (updatesCount == -2) { + // Error + this.updateIcon.set_gicon( this._getCustIcon('arch-error-symbolic') ); + if ( this.lastUnknowErrorString.indexOf("/usr/bin/checkupdates") > 0 ) { + // We do a special change here due to checkupdates moved to pacman-contrib + this._updateMenuExpander( false, _("Note : you have to install pacman-contrib to use the 'checkupdates' script.") ); + } else { + this._updateMenuExpander( false, _('Error') + "\n" + this.lastUnknowErrorString ); + } + } else { + // Up to date + this.updateIcon.set_gicon( this._getCustIcon('arch-uptodate-symbolic') ); + this._updateMenuExpander( false, _('Up to date :)') ); + UPDATES_LIST = []; // Reset stored list + } + } + UPDATES_PENDING = updatesCount; + this._checkAutoExpandList(); + this._checkShowHide(); + } + + _updateMenuExpander(enabled, label) { + this.menuExpander.menu.box.destroy_all_children(); + if (label == "") { + // No text, hide the menuitem + this.menuExpander.actor.visible = false; + } else { + // We make our expander look like a regular menu label if disabled + this.menuExpander.actor.reactive = enabled; + this.menuExpander._triangle.visible = enabled; + this.menuExpander.label.set_text(label); + this.menuExpander.actor.visible = true; + if (enabled && this._updateList.length > 0) { + this._updateList.forEach( item => { + this.menuExpander.menu.box.add( new St.Label({ text: item }) ); + } ); + } + } + + // 'Update now' visibility is linked so let's save a few lines and set it here + this.updateNowMenuItem.actor.reactive = enabled; + } + + _checkUpdates() { + if(this._updateProcess_sourceId) { + // A check is already running ! Maybe we should kill it and run another one ? + return; + } + // Run asynchronously, to avoid shell freeze - even for a 1s check + this._showChecking(true); + try { + // Parse check command line + let [parseok, argvp] = GLib.shell_parse_argv( CHECK_CMD ); + if (!parseok) { throw 'Parse error' }; + let [res, pid, in_fd, out_fd, err_fd] = GLib.spawn_async_with_pipes(null, argvp, null, GLib.SpawnFlags.DO_NOT_REAP_CHILD, null); + // Let's buffer the command's output - that's a input for us ! + this._updateProcess_stream = new Gio.DataInputStream({ + base_stream: new Gio.UnixInputStream({fd: out_fd}) + }); + // We will process the output at once when it's done + this._updateProcess_sourceId = GLib.child_watch_add(0, pid, () => {this._checkUpdatesRead()} ); + this._updateProcess_pid = pid; + } catch (err) { + this._showChecking(false); + this.lastUnknowErrorString = err.message.toString(); + this._updateStatus(-2); + } + } + + _cancelCheck() { + if (this._updateProcess_pid == null) { return; }; + Util.spawnCommandLine( "kill " + this._updateProcess_pid ); + this._updateProcess_pid = null; // Prevent double kill + this._checkUpdatesEnd(); + } + + _checkUpdatesRead() { + // Read the buffered output + let updateList = []; + let out, size; + do { + [out, size] = this._updateProcess_stream.read_line_utf8(null); + if (out) updateList.push(out); + } while (out); + // If version numbers should be stripped, do it + if (STRIP_VERSIONS == true) { + updateList = updateList.map(function(p) { + // Try to keep only what's before the first space + var chunks = p.split(" ",2); + return chunks[0]; + }); + } + this._updateList = updateList; + this._checkUpdatesEnd(); + } + + _checkUpdatesEnd() { + // Free resources + this._updateProcess_stream.close(null); + this._updateProcess_stream = null; + GLib.source_remove(this._updateProcess_sourceId); + this._updateProcess_sourceId = null; + this._updateProcess_pid = null; + // Update indicator + this._showChecking(false); + this._updateStatus(this._updateList.length); + } + + _showNotification(title, message) { + if (this._notifSource == null) { + // We have to prepare this only once + this._notifSource = new MessageTray.SystemNotificationSource(); + this._notifSource.createIcon = function() { + let gicon = Gio.icon_new_for_string( Me.dir.get_child('icons').get_path() + "/arch-lit-symbolic.svg" ); + return new St.Icon({ gicon: gicon }); + }; + // Take care of note leaving unneeded sources + this._notifSource.connect('destroy', ()=>{this._notifSource = null;}); + Main.messageTray.add(this._notifSource); + } + let notification = null; + // We do not want to have multiple notifications stacked + // instead we will update previous + if (this._notifSource.notifications.length == 0) { + notification = new MessageTray.Notification(this._notifSource, title, message); + notification.addAction( _('Update now') , ()=>{this._updateNow();} ); + } else { + notification = this._notifSource.notifications[0]; + notification.update( title, message, { clear: true }); + } + notification.setTransient(TRANSIENT); + this._notifSource.showNotification(notification); + } + +}); + +let archupdateindicator; + +function enable() { + archupdateindicator = new ArchUpdateIndicator(); + Main.panel.addToStatusArea('ArchUpdateIndicator', archupdateindicator); + archupdateindicator._positionChanged(); +} + +function disable() { + archupdateindicator.destroy(); +} diff --git a/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/icons/arch-error-symbolic.svg b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/icons/arch-error-symbolic.svg new file mode 100644 index 0000000..fa32162 --- /dev/null +++ b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/icons/arch-error-symbolic.svg @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/icons/arch-lit-symbolic.svg b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/icons/arch-lit-symbolic.svg new file mode 100644 index 0000000..92a9704 --- /dev/null +++ b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/icons/arch-lit-symbolic.svg @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/icons/arch-unknown-symbolic.svg b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/icons/arch-unknown-symbolic.svg new file mode 100644 index 0000000..9831e3e --- /dev/null +++ b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/icons/arch-unknown-symbolic.svg @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/icons/arch-updates-symbolic.svg b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/icons/arch-updates-symbolic.svg new file mode 100644 index 0000000..9c40fd4 --- /dev/null +++ b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/icons/arch-updates-symbolic.svg @@ -0,0 +1,316 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/icons/arch-uptodate-symbolic.svg b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/icons/arch-uptodate-symbolic.svg new file mode 100644 index 0000000..e8dad98 --- /dev/null +++ b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/icons/arch-uptodate-symbolic.svg @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/ar/LC_MESSAGES/arch-update.mo b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/ar/LC_MESSAGES/arch-update.mo new file mode 100644 index 0000000000000000000000000000000000000000..3c3fb38b16344a4540e54182043ab136ccb717e5 GIT binary patch literal 3811 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/cs_CZ/LC_MESSAGES/arch-update.mo b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/cs_CZ/LC_MESSAGES/arch-update.mo new file mode 100644 index 0000000000000000000000000000000000000000..9371edeac321f50bed96a593c62c2b0adb34388b GIT binary patch literal 2500 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/es/LC_MESSAGES/arch-update.mo b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/es/LC_MESSAGES/arch-update.mo new file mode 100755 index 0000000000000000000000000000000000000000..dbb28d2ac05a6d1742514dad59d8bf5cb5f1b1a5 GIT binary patch literal 3655 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 TBu0w;YNg-Zcz17@!HU#ReH5}- literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/fi_FI/LC_MESSAGES/arch-update.mo b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/fi_FI/LC_MESSAGES/arch-update.mo new file mode 100644 index 0000000000000000000000000000000000000000..6782a18a4eadc97d33d150c2ccb8f951e50efa2c GIT binary patch literal 3170 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/he_IL/LC_MESSAGES/arch-update.mo b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/he_IL/LC_MESSAGES/arch-update.mo new file mode 100644 index 0000000000000000000000000000000000000000..d9c91503b51a40947a4ea3ddb19be73090d31568 GIT binary patch literal 3259 zcmbW1OKjX!6oySHPy!Sv@3z1#EhvGyoh0-@sz{`vDM4u)+N7`oIWspI(;0hYd-9Mf zB-8~P7I{pXMj57Q6_F-%%Yrovq)IH1kSrm=62XcEY*@i}Y>#J#lnqWipYOfrp7Y;x zj{Wnl?TZH2KJLBT2ktS9!|>oXez<;x2jFk;ak%r|m+y&o-eaUzMKFIhc+yQUH zC*fC6{M>=B!r$R;xO=-{w810rKKLdy;Q$mr0c?jK!-H@gijE)P!|)d<{{Mid;SQX1 z!C@%zufP}KXHeqZhR5Kyumk=LAAueB$2yKd(RUI)0gLcua`Fj0!g&1w!`K6Vg0lZF z_$b^@uxZ#0Mb~Zk4E!4MZ+y?sbMQy_3j7nEgnKyT0r&xwb6tUwk45+rT!n--zJa3q z4wQ3l!>AL!1I7L_Kl|aQ@FM&Y9);T;iuJq!MgIVlJX}u3OYkh?@8HvLFXH7q-EbK8 zL(yA-q#??+oI`v_T*>WI+|mnjPVwa-x2a zo|J1>j3}qHPLwODk6e$%i0UYh&vHx7>KCm+5p$|s%2|O{CGF&FXQDCVr&E2oNz2LV zobq)LVB;Hog+j8=SB_QGGBah(_-fMj?XiM3`pSW8>gkf@)C2gcVEcix$J32jx9l*% zg7$qCl9{;Q=aE~ z#@p_cDweZ(WjjPITH-^5_<0t1blkumx3kf_Zw!)up4m6(1lpUl3TjLTQyM*SBtJPp zd(n2vf%ZF$vwBMPc?1|@W?GF#mAw0`A5GUC#Wg$I6yG>^mxCL_R+b#7f}2H6&h~T` zeKX`I$SYHgP#zsHr>63#bWoFI)>oFNIkq2Ig@Vo*7icbJwH&yyr!GpiSd*4rkb<)% z>9q@zt-4LzGvv-rvvncx?2>ZJftu8wPm1dujmZ+f5Oh0C(9GQIpQ)se9kb;?0fH(gzo86&KO^O)bld{c$Ba1%ER zkf9N7#19H@RfXkJILD@HY+Fq|T^BE3g!78s5@$iix5Qe7vlw6FuJOApA!g-AmNyvJ z!ivnGd8HnzlDdrqD)d|nSH%uv?i=A+cuPFYg|qTmM2?7(7}X95oF2zuVS(xOEoYu% zb|G9hkhqMy#c*Sb$r^FRh`1790mF4sUe9fky%w`I&cBS$D3M5(q&26kJ<7%!sanSO zDvp;?+~T6{{(8$!qmiDuSdP?Y9yhaL#ULrtXvx7Q8k?4v2t~T8P2bwadQ!5;22s?t)h=zQfAx|KaVyaz z-)T6hXM75dDd|Oc)Ic?9i7icOFe@dX?`l!~$c$vAs*-QFDc-MQQ6Usd;(c9Q#a1oV zYDf|lQG2Iiq+e%oOe4l`UHp2b!neL#sgFqhe|LM8NKr+Sid85bQIftF&GXdohLrvP zz3)lgzi$XYvnu^~@MXH?>ELyaBsx~$-bZ-25M zpDR4le-$>#$AOS7of-FG;tuDlm{+k&8ex_qaC%8sRH8=QTC?o>`-x4HR^t8x5PBgQ literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/he_IL/LC_MESSAGES/he_IL.mo b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/he_IL/LC_MESSAGES/he_IL.mo new file mode 100644 index 0000000000000000000000000000000000000000..e4237322957f0a0245b71e7ac9177b08b68f2df7 GIT binary patch literal 3301 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 WYUaBjIyIK&2oL>(+u~XHSMgs3f|v^c literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/nb_NO/LC_MESSAGES/arch-update.mo b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/nb_NO/LC_MESSAGES/arch-update.mo new file mode 100644 index 0000000000000000000000000000000000000000..f4ed344f8345a0e543f69686e52439a16887defc GIT binary patch literal 2374 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 bG)+~+NiJ(?ko@Z!1sV_pauQ`6g~e2yTNxr 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/uk_UA/LC_MESSAGES/arch-update.mo b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/uk_UA/LC_MESSAGES/arch-update.mo new file mode 100644 index 0000000000000000000000000000000000000000..6d8a62309fae72e92cf40467c123242d621336e6 GIT binary patch literal 4661 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 J_8Fl>{TBu%U0MJD literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/zh_CN/LC_MESSAGES/arch-update.mo b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/locale/zh_CN/LC_MESSAGES/arch-update.mo new file mode 100644 index 0000000000000000000000000000000000000000..5a6bfecd384d2fc284705e5c388ead2305974602 GIT binary patch literal 3141 zcmZvcOK=oL7{^=1SA2`_7me}=kBOUz5QA7k1WidG!lInO$!;e*usgFgJ)02quuufE zh8U3qNLa;4Oj0086bJ}xI9Ob~df}o6-I>`vSr%2^tn&ZO%x;23*UtW?yTAV4J-li1{h-J@BVse%~C%?!x>T@Lq5} zcn`P?d<yVld9p~I zH^@)gBRT5Lpe6C<>v|p}n=}^TA-hoP)%{732Gy)I$Zlyoffw?d#^ZRXr)a!{hy13T zB6P2&6es0wJ|5~7>U-)X8XV6;JWu1H{-NB{pdRza!YEHAqpBr%Qs_}lZ)N2bSzcNj zZBzA#h;k_`3o??ejmLv?lIvj8$wEM#Z(=CnChuSvoLvOh+77aYEp&5C2XB4waAPI^+LCV$?9VwvYYEhyZ=T3 zcuRQUErwaL#p;bjf_#C-hK&fH!Q71i+JmjHJ}W5$GHZIuuYBg zW?{7p_~ToWK?IcvO;1@uE@2x*i^VpHcD~kxm5o^5!?$@=z4fZ}mP^|F`fW1M19iN^ zNb#83CdfZcmzIh`BTdMF5;1hk)S780B_R+Ke6g3yK+$3@Bc_(Lsu*;Y17e$RkxAC% zspN4Zg7Bi6DIy5C1D329S9lm@7Z%oddkkUgh%6|TwiV6H)tfRqqpK7+sZ!fiElv%FBSCL(rLvS{{2oP-v~pRtA`3}wq%7VhOo>{S zI##l9Optq$moHKI4*6STvb;BQ!NOX?%Xqu0S*SL;MDP~P#C7qTwNeUXKZ;UsOH%&5 z3tLAs?~U4j8%olg(&CnaRA`Wv-jZdiy3}x2Zpww^l`1YMU9N;Cq4on9_O8r~QPyM{ zyHEqAA*#IZ2U^D~wuq!*T1umA)uPI}RI60N29}#PD_aEJeM48?!q=pZ*Mw@SmC!1s zCd8{()>W@q7OD+}DmJRpQo`g@oLan&e-ulorsS_z*REK(@|D+CE%O)NM2DHON=G7+ z@s6ri&5EU(t0G3?4XDstQ@D;wSf~k@F3m~T&iYLijSY?KXB@7s3RTpjaghNf+>wN1 zme^yxoQ$iQz8cwuabd057T%!L&d8B}Ey7gR>k)&#iFLfTS+gpd;we*&D;sc!WgXX( z-c+usUd{d2HKElN+m*M4E=)hTCPPF~&{yCCeOB`qPdUfWIY%a)Gbi0M8GE|ZIoy-a zWm)m`G56AFVSL)njWZ{ccCr~?(Eg&w9i3$MXt#U0%Q-gf4qsyS$Uf)mY@n!rGN1cA zf9Z4Q%&^lxX{WE)T?2(rPuZW1*+2B#SJHu+uA_yavA?c#g*XT$?#N!Z4+|M)pSa{? zzYk33FPQ})45--tW7-#(PfUmivT zhhZ8U?)Z@V#Q}R@th@&w?B0oDIzzVob7eJUA(p?~iD=y8>P?(rt@SvC;bWePj zzxbJ-277qWo;;MlI^^ud_VH=wn@8!8T7y2XpRbl@i@>a~I3nP2&@l4@l=l?#Ff2Q{_ z-{)K8qxAiQ3Oh@I+PVJ1g$a9lKfZpjM-@em&v(PGa()Wmp7t$~+efFUQhwn1Tn{yl z-FueVxx@bp4e5qL$lJY#is`+9OF_Yd^QENv(FLiuGkwK@VK>u7PWE14&`Y1L*-ebH z+}y__N1X46{7l)Io?y>&b#^6wbdF6p*)(F~J9h3UwVRiI=I7f!(20nGvieKT;64Q5 bq(}V-pwI7fXQ~TsdhO(Os&BM7%M. + + Copyright 2016 Raphaël Rochet +*/ + +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; +const Gio = imports.gi.Gio; +const Lang = imports.lang; +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); + +const Gettext = imports.gettext.domain('arch-update'); +const _ = Gettext.gettext; + +let settings; + +function init() { + settings = ExtensionUtils.getSettings('org.gnome.shell.extensions.arch-update'); + ExtensionUtils.initTranslations("arch-update"); +} + +function buildPrefsWidget(){ + + // Prepare labels and controls + let buildable = new Gtk.Builder(); + buildable.add_from_file( Me.dir.get_path() + '/prefs.xml' ); + let box = buildable.get_object('prefs_widget'); + + let version_label = buildable.get_object('version_info'); + version_label.set_text('[Arch-update v' + Me.metadata.version.toString() + ']'); + + // Bind fields to settings + settings.bind('boot-wait' , buildable.get_object('field_wait') , 'value' , Gio.SettingsBindFlags.DEFAULT); + settings.bind('check-interval' , buildable.get_object('field_interval') , 'value' , Gio.SettingsBindFlags.DEFAULT); + settings.bind('always-visible' , buildable.get_object('field_visible') , 'active' , Gio.SettingsBindFlags.DEFAULT); + settings.bind('use-buildin-icons' , buildable.get_object('field_buildinicons') , 'active' , Gio.SettingsBindFlags.DEFAULT); + settings.bind('show-count' , buildable.get_object('field_count') , 'active', Gio.SettingsBindFlags.DEFAULT); + settings.bind('notify' , buildable.get_object('field_notify') , 'active' , Gio.SettingsBindFlags.DEFAULT); + settings.bind('howmuch', buildable.get_object('field_howmuch'), 'active', Gio.SettingsBindFlags.DEFAULT); + settings.bind('transient', buildable.get_object('field_transient'), 'active', Gio.SettingsBindFlags.DEFAULT); + settings.bind('strip-versions' , buildable.get_object('field_stripversions') , 'active' , Gio.SettingsBindFlags.DEFAULT); + settings.bind('check-cmd' , buildable.get_object('field_checkcmd') , 'text' , Gio.SettingsBindFlags.DEFAULT); + settings.bind('update-cmd' , buildable.get_object('field_updatecmd') , 'text' , Gio.SettingsBindFlags.DEFAULT); + settings.bind('pacman-dir' , buildable.get_object('field_pacmandir') , 'text' , Gio.SettingsBindFlags.DEFAULT); + settings.bind('auto-expand-list', buildable.get_object('field_autoexpandlist'), 'value', Gio.SettingsBindFlags.DEFAULT); + settings.bind('package-manager' , buildable.get_object('field_packagemanager') , 'text' , Gio.SettingsBindFlags.DEFAULT); + settings.bind('position' , buildable.get_object('field_position') , 'active' , Gio.SettingsBindFlags.DEFAULT); + settings.bind('position-number' , buildable.get_object('field_positionnumber') , 'value' , Gio.SettingsBindFlags.DEFAULT); + + return box; +}; + diff --git a/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/prefs.xml b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/prefs.xml new file mode 100644 index 0000000..38b3284 --- /dev/null +++ b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/prefs.xml @@ -0,0 +1,459 @@ + + + + + + 5 + 5000 + 1 + + + 30 + 2000 + 30 + + + 0 + 99 + 1 + + + 0 + 99 + 1 + + + + + + + 18 + 18 + 18 + 18 + 18 + false + vertical + + + + vertical + 6 + + + + horizontal + 12 + + + Checking for updates + true + 1 + + + + + + + + Version info placeholder + false + 1 + + + + + + + + 12 + 12 + + + Time to wait before first check (seconds) + true + 1 + + + + + Adjust_1 + + + + + + + + 12 + 12 + + + Interval between updates check (minutes) + true + 1 + + + + + Adjust_2 + + + + + + + + 12 + 12 + + + Strip out versions numbers + true + 1 + + + + + true + + + + + + + + + + + vertical + 6 + + + + Indicator + true + 1 + + + + + + + + + 12 + 12 + + + Always visible + true + 1 + + + + + true + + + + + + + + 12 + 12 + + + Use built-in icons + true + 1 + + + + + true + + + + + + + + 12 + 12 + + + Show updates count on indicator + true + 1 + + + + + true + + + + + + + + 12 + 12 + true + + + Auto-expand updates list if updates count is less than this number (0 to disable) + true + 0 + 1 + + + + + Adjust_3 + + + + + + + 12 + 12 + true + + + Position in Panel + true + 0 + 1 + + + + + 12 + 12 + false + + + + Left + Center + Right + + + + + + Adjust_4 + + + + + + + + + + + + + vertical + 6 + + + + Notification + true + 1 + + + + + + + + + 12 + 12 + + + Send a notification when new updates are available + true + 1 + + + + + true + + + + + + + + 12 + 12 + + + Use transient notifications (auto dismiss) + true + 1 + + + + + true + + + + + + + + 12 + 12 + + + How much information to show on notifications + true + 1 + + + + + + Count only + New updates names + All updates names + + + + + + + + + + + + + + + Basic settings + + + + + + 18 + 18 + 18 + 18 + 18 + false + vertical + + + + vertical + 6 + + + Command to check for package updates + true + 1 + + + + + + + + + + + + vertical + 6 + + + Command to update packages + true + 1 + + + + + + + + + + + + vertical + 6 + + + Pacman local directory path - To detect when new packages are installed + true + 1 + + + + + + + + + + + + vertical + 6 + + + Command to open package manager (optional) + true + 1 + + + + + + + + + + + + + + + Advanced settings + + + + + + diff --git a/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/schemas/gschemas.compiled b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/schemas/gschemas.compiled new file mode 100644 index 0000000000000000000000000000000000000000..991df55a9fd92191b4248f7b41322d7064c93cf9 GIT binary patch literal 1285 zcmYjRO=#3W7>(7ftv~%y3iY6j6?B8l#(O<@u>L@i;{UD&Uhnz+5A||c^C+Tc$HclG4WbyGpT%WBB~u?F`VEjk;1%H2#Y^j1 zBkBr#ZeTZX{!wj#@zg``_kpK?GpmOC=u@wQKLb7i+`9kfBYo--_#yZzuyXgxQTo(e z=MMN8uvIKQr%z4)HTW%Xf9!okpPF^}0R9GSxw-KNeQNG&0EOKM9G;rFPM@0jjDsz( z{pr2k^r`7z2Hyj^Jwfd_)6N6%Ct!H__fN)Ck0O2=kC&Ar}2%-7uMJ9nPyGGk{inDN_ycWm%pyv + + + + 15 +

Time to wait before first check (seconds) + A first check is made this number of seconds after startup + + + + + 60 + Interval between updates check (minutes) + Time to wait between two automatic checks + + + + + true + Indicator is always visble + + If true, the indicator is always visible, even when non updates are pending + + + + + false + Use build-in icons + + If true, the build-in status icons are used instead of theme icons + + + + + true + Show updates count on indicator + + If true, the indicator will display the number of updates pending + + + + + false + Send a notification when new updates are available + Send a notification when new updates are available + + + + 0 + How much information to show on notifications + 0:count, 1:list + + + + true + Use transient notifications (auto dismiss) + + + + + "/usr/bin/checkupdates" + Command to run to check for updated packages. + Command to run to check for updated packages. + + + + "gnome-terminal -e 'sh -c \"sudo pacman -Syu ; echo Done - Press enter to exit; read _\" '" + Command to run to update packages. + Command to run to update packages. + + + + "/var/lib/pacman/local" + Pacman directory to monitor + + + + + true + Remove version numbers from checkupdates output + + + + + 0 + Auto-open list submenu when updates count is lower than this number + + + + + + "" + Command to run to open package manager. + + + + + 2 + Position in the panel + Position where the arch update will be displayed (left/center/right) + + + + -0 + Position of the arch update inside the box + + + + + diff --git a/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/stylesheet.css b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/stylesheet.css new file mode 100644 index 0000000..29a9f51 --- /dev/null +++ b/.local/share/gnome-shell/extensions/arch-update@RaphaelRochet/stylesheet.css @@ -0,0 +1,36 @@ +/* + This file is part of Arch Linux Updates Indicator + + Arch Linux Updates Indicator is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Arch Linux Updates Indicator is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Arch Linux Updates Indicator. If not, see . + + Copyright 2016 Raphaël Rochet +*/ + +.arch-updates-list { + margin: 10px; + padding-left: 20px; +} + +.arch-updates-menubutton { + /* Meant to be used as an override to display a small system-menu-action */ + border-radius: 10px; + padding: 0px 4px; +} +.arch-updates-menubutton:hover, .arch-updates-menubutton:focus { + /* 1px borders disapears, need to compensate */ + padding: 1px 5px; +} +.arch-updates-menubutton > StIcon { + icon-size: 16px; +} diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/COPYING b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/COPYING new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/README.md b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/README.md new file mode 100644 index 0000000..1c76c69 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/README.md @@ -0,0 +1,45 @@ +# Dash to Dock +![screenshot](https://github.com/micheleg/dash-to-dock/raw/master/media/screenshot.jpg) + +## A dock for the GNOME Shell +This extension enhances the dash moving it out of the overview and transforming it in a dock for an easier launching of applications and a faster switching between windows and desktops without having to leave the desktop view. + +[](https://extensions.gnome.org/extension/307/dash-to-dock) + +For additional installation instructions and more information visit [https://micheleg.github.io/dash-to-dock/](https://micheleg.github.io/dash-to-dock/). + +## Installation from source + +The extension can be installed directly from source, either for the convenience of using git or to test the latest development version. Clone the desired branch with git + +### Build Dependencies + +To compile the stylesheet you'll need an implementation of SASS. Dash to Dock supports `dart-sass` (`sass`), `sassc`, and `ruby-sass`. Every distro should have at least one of these implementations, we recommend using `dart-sass` (`sass`) or `sassc` over `ruby-sass` as `ruby-sass` is deprecated. + +By default, Dash to Dock will attempt to build with `dart-sass`. To change this behavior set the `SASS` environment variable to either `sassc` or `ruby`. + +```bash +export SASS=sassc +# or... +export SASS=ruby +``` + +### Building + +Clone the repository or download the branch from github. A simple Makefile is included. + +Next use `make` to install the extension into your home directory. A Shell reload is required `Alt+F2 r Enter` under Xorg or under Wayland you may have to logout and login. The extension has to be enabled with *gnome-extensions-app* (GNOME Extensions) or with *dconf*. + +```bash +git clone https://github.com/micheleg/dash-to-dock.git +make +make install +``` + +## Bug Reporting + +Bugs should be reported to the Github bug tracker [https://github.com/micheleg/dash-to-dock/issues](https://github.com/micheleg/dash-to-dock/issues). + +## License +Dash to Dock Gnome Shell extension is distributed under the terms of the GNU General Public License, +version 2 or later. See the COPYING file for details. diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/Settings.ui b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/Settings.ui new file mode 100644 index 0000000..d129661 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/Settings.ui @@ -0,0 +1,2799 @@ + + + + + 1 + 0.050000000000000003 + 0.25 + + + 0 + 12 + 12 + 12 + 12 + vertical + + + 0 + + + 0 + 1 + 1 + none + + + 100 + 80 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + When set to minimize, double clicking minimizes all the windows of the application. + 1 + 0 + 40 + + + + 0 + 1 + + + + + + 0 + 1 + Shift+Click action + 0 + + + 0 + 0 + + + + + + 0 + center + + Raise window + Minimize window + Launch new instance + Cycle through windows + Minimize or overview + Show window previews + Minimize or show previews + Focus or show previews + Focus, minimize or show previews + Quit + + + 1 + 0 + 2 + + + + + + + + + + 100 + 80 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + Behavior for Middle-Click. + 1 + 0 + 40 + + + + 0 + 1 + + + + + + 0 + 1 + Middle-Click action + 0 + + + 0 + 0 + + + + + + 0 + center + + Raise window + Minimize window + Launch new instance + Cycle through windows + Minimize or overview + Show window previews + Minimize or show previews + Focus or show previews + Focus, minimize or show previews + Quit + + + 1 + 0 + 2 + + + + + + + + + + 100 + 80 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + Behavior for Shift+Middle-Click. + 1 + 0 + 40 + + + + 0 + 1 + + + + + + 0 + 1 + Shift+Middle-Click action + 0 + + + 0 + 0 + + + + + + 0 + center + + Raise window + Minimize window + Launch new instance + Cycle through windows + Minimize or overview + Show window previews + Minimize or show previews + Focus or show previews + Focus, minimize or show previews + Quit + + + 1 + 0 + 2 + + + + + + + + + + + + + + + + + 1 + 0.01 + 0.10 + + + 0.33 + 1 + 0.01 + 0.10 + + + 10 + 1 + 5 + + + 1 + 1 + 0 + 12 + 12 + 12 + 12 + vertical + + + 0 + + + + 0 + none + + + 100 + 80 + + + 0 + 12 + 12 + 12 + 12 + vertical + 12 + + + 0 + 32 + + + 1 + 0 + 0 + center + Enable Unity7 like glossy backlit items + + + + + + center + + + + + + + 0 + + + 1 + 0 + 0 + start + Use dominant color + + + + + + + + + + 0 + 32 + + + + 1 + 0 + + + + + + 0 + 1 + 0 + Customize indicator style + fill + + + 0 + 0 + + + + + + + + 0 + 1 + vertical + 12 + + + 0 + 32 + + + 1 + 0 + 0 + Color + + + + + + 1 + + + + + + + 0 + 32 + + + 1 + 0 + 0 + Border color + + + + + + 1 + + + + + + + 0 + 32 + + + 1 + 0 + 0 + Border width + + + + + + dot_border_width_adjustment + + + + + + + + + + + + + + + + + + + + 1 + 0.050000000000000003 + 0.25 + + + 16 + 128 + 1 + 10 + + + 0.0 + 1 + 0.01 + 0.1 + + + 6 + 6 + 6 + 6 + + + + + 0 + 24 + 24 + 24 + 24 + vertical + 24 + + + 0 + + + + 0 + none + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + start + Show the dock on + + 0 + 0 + + + + + + 0 + 400 + center + + + 1 + 0 + + + + + + Show on all monitors. + 12 + + + 0 + 2 + 2 + + + + + + + + + + + + + + + + 100 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + 0 + Position on screen + + + + + + 0 + 32 + + + Left + end + center + + + + + + + Bottom + center + + position_left_button + + + + + + Top + center + + + position_left_button + + + + + + Right + center + + position_left_button + + + + + + + + + + + + + + + + + + + 0 + + + + 0 + none + + + 100 + 80 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + start + Hide the dock when it obstructs a window of the current application. More refined settings are available. + 1 + 0 + + + 0 + 1 + + + + + + 0 + 1 + start + Intelligent autohide + + + 0 + 0 + + + + + + 0 + 6 + + + 1 + center + center + + + + 0 + emblem-system-symbolic + + + + + + + + end + center + + + + 1 + 0 + 2 + + + + + + + + + + + + + + + + + 0 + + + + 0 + none + + + 100 + 80 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + Dock size limit + + + 0 + 0 + + + + + + 1 + baseline + 1 + dock_size_adjustment + 0 + 2 + right + + + + + + Panel mode: extend to the screen edge + 12 + + + 0 + 1 + 2 + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + Icon size limit + + + 0 + 0 + + + + + + 1 + baseline + 1 + icon_size_adjustment + 1 + 0 + right + + + 1 + 0 + + + + + + Fixed icon size: scroll to reveal other icons + 12 + + + 0 + 1 + 2 + + + + + + + + + + True + + + False + 12 + 12 + 12 + 12 + 32 + + + False + Preview size scale + 0 + + 0 + 0 + + + + + + 1 + 1 + preview_size_adjustment + 0 + 0 + 0 + 2 + right + + + + + + 0 + 1 + + + + + + + + + + + + + + + + + 0 + Position and size + + + + + + + 1 + + + 0 + 24 + 24 + 24 + 24 + vertical + 24 + + + 0 + + + + 0 + none + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + end + center + + 1 + 0 + + + + + + 0 + 1 + start + Show favorite applications + + + 0 + 0 + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + end + center + + 1 + 0 + + + + + + 0 + 1 + start + Show running applications + + + 0 + 0 + + + + + + Isolate workspaces. + start + 12 + + + 0 + 2 + 2 + + + + + + Isolate monitors. + start + 12 + + + 0 + 3 + 2 + + + + + + 3 + Show open windows previews. + + 0 + 1 + 2 + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + end + center + + 1 + 0 + + + + + + 0 + 1 + start + Keep the focused application always visible in the dash + + + 0 + 0 + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + end + center + + 1 + 0 + 2 + + + + + + 0 + 1 + If disabled, these settings are accessible from gnome-tweak-tool or the extension website. + 1 + 0 + + + 0 + 1 + + + + + + 0 + 1 + start + Show <i>Applications</i> icon + 1 + + + 0 + 0 + + + + + + Move the applications button at the beginning of the dock. + start + 12 + + + 0 + 2 + 2 + + + + + + + 3 + + + 0 + start + Animate <i>Show Applications</i>. + 1 + + + + 0 + 3 + 2 + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + end + center + + 1 + 0 + + + + + + 0 + 1 + start + Show trash can + + + 0 + 0 + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + end + center + + 1 + 0 + + + + + + 0 + 1 + start + Show mounted volumes and devices + + + 0 + 0 + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + end + center + + 1 + 0 + + + + + + 0 + 1 + start + Isolate volumes, devices and trash windows from file manager + + + 0 + 0 + + + + + + + + + + + + + + + + + + + 0 + Launchers + + + + + + + 2 + + + 0 + 24 + 24 + 24 + 24 + vertical + 24 + + + 0 + + + + 0 + none + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + start + 1 + Enable Super+(0-9) as shortcuts to activate apps. It can also be used together with Shift and Ctrl. + 1 + 1 + 0 + + + + 0 + 1 + + + + + + 0 + 1 + start + Use keyboard shortcuts to activate apps + + + 0 + 0 + + + + + + 0 + 6 + + + 1 + center + center + + + + 0 + emblem-system-symbolic + + + + + + + + end + center + + + + 1 + 0 + 2 + + + + + + + + + + + + + + + + + 0 + + + + 0 + none + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + start + Behaviour when clicking on the icon of a running application. + 1 + 0 + + + + 0 + 1 + + + + + + 0 + 1 + start + Click action + 0 + + + 0 + 0 + + + + + + 0 + 6 + + + 1 + center + + + 0 + emblem-system-symbolic + + + + + + + + 0 + center + + Raise window + Minimize + Launch new instance + Cycle through windows + Minimize or overview + Show window previews + Minimize or show previews + Focus or show previews + Focus, minimize or show previews + + + + + 1 + 0 + 2 + + + + + + + + + + + + + + + + + 0 + + + + 0 + none + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + start + Behaviour when scrolling on the icon of an application. + 1 + 0 + + + + 0 + 1 + + + + + + 0 + 1 + start + Scroll action + + + 0 + 0 + + + + + + 0 + 6 + + + 0 + center + + Do nothing + Cycle through windows + Switch workspace + + + + + 1 + 0 + 2 + + + + + + + + + + + + + + + + + + + 0 + start + Behavior + + + + + + + 3 + + + 0 + 24 + 24 + 24 + 24 + vertical + 24 + + + 0 + + + + none + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + start + Few customizations meant to integrate the dock with the default GNOME theme. Alternatively, specific options can be enabled below. + 1 + 0 + + + + 0 + 1 + + + + + + 0 + 1 + start + Use built-in theme + + + 0 + 0 + + + + + + end + center + + 1 + 0 + 2 + + + + + + + + + + + + + + + + + 0 + + + + 0 + none + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + end + center + + 1 + 0 + 2 + + + + + + 0 + 1 + start + Save space reducing padding and border radius. + + + + 0 + 1 + + + + + + 0 + 1 + start + Shrink the dash + + + 0 + 0 + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + start + Customize windows counter indicators + + + 0 + 0 + + + + + + 0 + 6 + + + 1 + center + center + + + + 0 + emblem-system-symbolic + + + + + + + + 0 + + Default + Dots + Squares + Dashes + Segmented + Solid + Ciliora + Metro + + + + + 1 + 0 + 2 + + + + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + start + Set the background color for the dash. + 1 + 0 + + + + 0 + 1 + + + + + + 0 + 1 + start + Customize the dash color + + + 0 + 0 + + + + + + 0 + 6 + + + 1 + center + center + + + + + + end + center + + + + 1 + 0 + 2 + + + + + + + + + + + + 0 + vertical + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + start + Tune the dash background opacity. + + + + 0 + 1 + + + + + + 0 + 1 + start + Customize opacity + + + 0 + 0 + + + + + + 0 + 6 + + + 1 + center + center + + + 0 + emblem-system-symbolic + + + + + + + + 0 + center + + Default + Fixed + Dynamic + + + + + 1 + 0 + 2 + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + Opacity + + + + + 1 + 1 + custom_opacity_adjustement + + 0 + 0 + 0 + 2 + right + + + + + + + + + + + + 100 + 80 + + + 0 + 32 + 12 + 12 + 12 + 12 + + + 1 + 0 + center + start + Force straight corner + + + + + + center + 3 + + + + + + + + + + + + + + + + + + 0 + Appearance + + + + + + + 4 + + + 0 + 0 + 24 + 24 + 1 + 1 + vertical + 5 + + + + + + 0 + <b>Dash to Dock</b> + 1 + + + + + 0 + center + + + 0 + end + version: + + + + + 0 + start + ... + + + + + + + 0 + Moves the dash out of the overview transforming it in a dock + center + 1 + 0 + + + + + 0 + center + 5 + + + 0 + Created by + + + + + Michele (<a href="mailto:micxgx@gmail.com">micxgx@gmail.com</a>) + 1 + + + + + + + Webpage + 1 + center + + https://micheleg.github.io/dash-to-dock/ + + + + + 1 + end + <span size="small">This program comes with ABSOLUTELY NO WARRANTY. +See the <a href="https://www.gnu.org/licenses/old-licenses/gpl-2.0.html">GNU General Public License, version 2 or later</a> for details.</span> + 1 + center + 1 + 0 + + + + + + + 0 + About + + + + + + + 1 + 0.01 + 0.10000000000000001 + + + 1 + 0.01 + 0.10000000000000001 + + + 1 + 1 + 0 + 12 + 12 + 12 + 12 + vertical + + + 0 + + + + 0 + none + + + 100 + 80 + + + 0 + 12 + 12 + 12 + 12 + vertical + 12 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + Customize minimum and maximum opacity values + fill + 0 + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + Minimum opacity + + + + + 1 + 1 + min_opacity_adjustement + + 0 + 0 + 0 + 2 + right + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + Maximum opacity + + + + + 1 + 1 + max_opacity_adjustement + + 0 + 0 + 0 + 2 + right + + + + + + + + + + + + + + + + + + + 1000 + 50 + 250 + + + 10 + 0.25 + 1 + + + 1 + 1 + 12 + 12 + 12 + 12 + vertical + + + + + none + + + 100 + 80 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + end + center + + 1 + 0 + 2 + + + + + + 0 + 1 + Number overlay + + + 0 + 0 + + + + + + 0 + Temporarily show the application numbers over the icons, corresponding to the shortcut. + 1 + 0 + 40 + + + + 0 + 1 + + + + + + + + + + 100 + 80 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + end + center + + 1 + 0 + 2 + + + + + + 0 + 1 + start + Show the dock if it is hidden + + + 0 + 0 + + + + + + 0 + If using autohide, the dock will appear for a short time when triggering the shortcut. + 1 + 0 + 40 + + + + 0 + 1 + + + + + + + + + + 100 + 80 + + + 12 + 12 + 12 + 12 + 32 + + + center + 12 + + 1 + 0 + + + + + + 0 + 1 + Shortcut for the options above + + + 0 + 0 + + + + + + 0 + Syntax: <Shift>, <Ctrl>, <Alt>, <Super> + 1 + 0 + 40 + + + + 0 + 1 + + + + + + + + + + + + + + + 12 + 12 + 12 + 12 + 1 + 6 + 32 + + + end + shortcut_time_adjustment + 3 + + 1 + 0 + + + + + + 0 + 1 + Hide timeout (s) + + + 0 + 0 + + + + + + + + + + + + + + + + + 1 + 0.050000000000000003 + 0.25 + + + 1 + 1 + 0 + 12 + 12 + 12 + 12 + vertical + + + 0 + + + + 0 + none + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + start + Show the dock by mouse hover on the screen edge. + 1 + 0 + + + + 0 + 1 + + + + + + 0 + 1 + Autohide + + + 0 + 0 + + + + + + end + center + + 1 + 0 + 2 + + + + + + Push to show: require pressure to show the dock + + + 0 + 3 + 2 + + + + + + Enable in fullscreen mode + 12 + + + 0 + 2 + 2 + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + 1 + start + Show the dock when it doesn't obstruct application windows. + 1 + 0 + + + + 0 + 1 + + + + + + 0 + 1 + Dodge windows + + + 0 + 0 + + + + + + end + center + + 1 + 0 + 2 + + + + + + 0 + vertical + + + All windows + 12 + + + + + + + Only focused application's windows + all_windows_radio_button + + + + + + Only maximized windows + all_windows_radio_button + + + + + 0 + 2 + 2 + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 1 + 6 + 32 + + + end + animation_time_adjustment + 3 + + 1 + 0 + + + + + + 0 + 1 + Animation duration (s) + + + 0 + 0 + + + + + + end + hide_timeout_adjustment + 3 + + 1 + 1 + + + + + + end + show_timeout_adjustment + 3 + + 1 + 2 + + + + + + 0.000 + pressure_threshold_adjustment + + 1 + 3 + + + + + + 0 + 1 + Hide timeout (s) + + + 0 + 1 + + + + + + 0 + 1 + Show timeout (s) + + + 0 + 2 + + + + + + 0 + 1 + Pressure threshold + + + 0 + 3 + + + + + + + + + + + + + + + + diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/appIconIndicators.js b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/appIconIndicators.js new file mode 100644 index 0000000..c1839bf --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/appIconIndicators.js @@ -0,0 +1,1031 @@ +const Cairo = imports.cairo; +const Clutter = imports.gi.Clutter; +const GdkPixbuf = imports.gi.GdkPixbuf +const Gio = imports.gi.Gio; +const Graphene = imports.gi.Graphene; +const Gtk = imports.gi.Gtk; +const Main = imports.ui.main; +const Pango = imports.gi.Pango; +const Shell = imports.gi.Shell; +const St = imports.gi.St; + +const Util = imports.misc.util; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Docking = Me.imports.docking; +const Utils = Me.imports.utils; + +let tracker = Shell.WindowTracker.get_default(); + +const RunningIndicatorStyle = { + DEFAULT: 0, + DOTS: 1, + SQUARES: 2, + DASHES: 3, + SEGMENTED: 4, + SOLID: 5, + CILIORA: 6, + METRO: 7 +}; + +const MAX_WINDOWS_CLASSES = 4; + + +/* + * This is the main indicator class to be used. The desired bahviour is + * obtained by composing the desired classes below based on the settings. + * + */ +var AppIconIndicator = class DashToDock_AppIconIndicator { + + constructor(source) { + this._indicators = []; + + // Unity indicators always enabled for now + let unityIndicator = new UnityIndicator(source); + this._indicators.push(unityIndicator); + + // Choose the style for the running indicators + let runningIndicator = null; + let runningIndicatorStyle; + + let settings = Docking.DockManager.settings; + if (settings.get_boolean('apply-custom-theme' )) { + runningIndicatorStyle = RunningIndicatorStyle.DOTS; + } else { + runningIndicatorStyle = settings.get_enum('running-indicator-style'); + } + + switch (runningIndicatorStyle) { + case RunningIndicatorStyle.DEFAULT: + runningIndicator = new RunningIndicatorDefault(source); + break; + + case RunningIndicatorStyle.DOTS: + runningIndicator = new RunningIndicatorDots(source); + break; + + case RunningIndicatorStyle.SQUARES: + runningIndicator = new RunningIndicatorSquares(source); + break; + + case RunningIndicatorStyle.DASHES: + runningIndicator = new RunningIndicatorDashes(source); + break; + + case RunningIndicatorStyle.SEGMENTED: + runningIndicator = new RunningIndicatorSegmented(source); + break; + + case RunningIndicatorStyle.SOLID: + runningIndicator = new RunningIndicatorSolid(source); + break; + + case RunningIndicatorStyle.CILIORA: + runningIndicator = new RunningIndicatorCiliora(source); + break; + + case RunningIndicatorStyle.METRO: + runningIndicator = new RunningIndicatorMetro(source); + break; + + default: + runningIndicator = new RunningIndicatorBase(source); + } + + this._indicators.push(runningIndicator); + } + + update() { + for (let i=0; i this.update()); + this._signalsHandler.add(this._source, 'notify::focused', () => this.update()); + this._signalsHandler.add(this._source, 'notify::windows-count', () => this._updateCounterClass()); + this.update(); + } + + get _number() { + return Math.min(this._source.windowsCount, MAX_WINDOWS_CLASSES); + } + + update() { + this._updateCounterClass(); + this._updateDefaultDot(); + } + + _updateCounterClass() { + for (let i = 1; i <= MAX_WINDOWS_CLASSES; i++) { + let className = 'running' + i; + if (i != this._number) + this._source.remove_style_class_name(className); + else + this._source.add_style_class_name(className); + } + } + + _updateDefaultDot() { + if (this._source.running) + this._source._dot.show(); + else + this._source._dot.hide(); + } + + _hideDefaultDot() { + // I use opacity to hide the default dot because the show/hide function + // are used by the parent class. + this._source._dot.opacity = 0; + } + + _restoreDefaultDot() { + this._source._dot.opacity = 255; + } + + _enableBacklight() { + + let colorPalette = this._dominantColorExtractor._getColorPalette(); + + // Fallback + if (colorPalette === null) { + this._source._iconContainer.set_style( + 'border-radius: 5px;' + + 'background-gradient-direction: vertical;' + + 'background-gradient-start: #e0e0e0;' + + 'background-gradient-end: darkgray;' + ); + + return; + } + + this._source._iconContainer.set_style( + 'border-radius: 5px;' + + 'background-gradient-direction: vertical;' + + 'background-gradient-start: ' + colorPalette.original + ';' + + 'background-gradient-end: ' + colorPalette.darker + ';' + ); + + } + + _disableBacklight() { + this._source._iconContainer.set_style(null); + } + + destroy() { + this._disableBacklight(); + // Remove glossy background if the children still exists + if (this._source._iconContainer.get_children().length > 1) + this._source._iconContainer.get_children()[1].set_style(null); + this._restoreDefaultDot(); + + super.destroy(); + } +}; + +// We add a css class so third parties themes can limit their indicaor customization +// to the case we do nothing +var RunningIndicatorDefault = class DashToDock_RunningIndicatorDefault extends RunningIndicatorBase { + + constructor(source) { + super(source); + this._source.add_style_class_name('default'); + } + + destroy() { + this._source.remove_style_class_name('default'); + super.destroy(); + } +}; + +var RunningIndicatorDots = class DashToDock_RunningIndicatorDots extends RunningIndicatorBase { + + constructor(source) { + super(source) + + this._hideDefaultDot(); + + this._area = new St.DrawingArea({x_expand: true, y_expand: true}); + + // We draw for the bottom case and rotate the canvas for other placements + //set center of rotatoins to the center + this._area.set_pivot_point(0.5, 0.5); + // prepare transformation matrix + let m = new Graphene.Matrix(); + m.init_identity(); + let v = new Graphene.Vec3(); + v.init(0, 0, 1); + + switch (this._side) { + case St.Side.TOP: + m.xx = -1; + m.rotate(180, v); + break + + case St.Side.BOTTOM: + // nothing + break; + + case St.Side.LEFT: + m.yy = -1; + m.rotate(90, v); + break; + + case St.Side.RIGHT: + m.rotate(-90, v); + break + } + + this._area.set_transform(m); + + this._area.connect('repaint', this._updateIndicator.bind(this)); + this._source._iconContainer.add_child(this._area); + + let keys = ['custom-theme-running-dots-color', + 'custom-theme-running-dots-border-color', + 'custom-theme-running-dots-border-width', + 'custom-theme-customize-running-dots', + 'unity-backlit-items', + 'running-indicator-dominant-color']; + + keys.forEach(function(key) { + this._signalsHandler.add( + Docking.DockManager.settings, + 'changed::' + key, + this.update.bind(this) + ); + }, this); + + // Apply glossy background + // TODO: move to enable/disableBacklit to apply itonly to the running apps? + // TODO: move to css class for theming support + this._glossyBackgroundStyle = 'background-image: url(\'' + Me.path + '/media/glossy.svg\');' + + 'background-size: contain;'; + } + + update() { + super.update(); + + // Enable / Disable the backlight of running apps + if (!Docking.DockManager.settings.get_boolean('apply-custom-theme') && + Docking.DockManager.settings.get_boolean('unity-backlit-items')) { + this._source._iconContainer.get_children()[1].set_style(this._glossyBackgroundStyle); + if (this._source.running) + this._enableBacklight(); + else + this._disableBacklight(); + } else { + this._disableBacklight(); + this._source._iconContainer.get_children()[1].set_style(null); + } + + if (this._area) + this._area.queue_repaint(); + } + + _computeStyle() { + + let [width, height] = this._area.get_surface_size(); + this._width = height; + this._height = width; + + // By defaut re-use the style - background color, and border width and color - + // of the default dot + let themeNode = this._source._dot.get_theme_node(); + this._borderColor = themeNode.get_border_color(this._side); + this._borderWidth = themeNode.get_border_width(this._side); + this._bodyColor = themeNode.get_background_color(); + + let settings = Docking.DockManager.settings; + if (!settings.get_boolean('apply-custom-theme')) { + // Adjust for the backlit case + if (settings.get_boolean('unity-backlit-items')) { + // Use dominant color for dots too if the backlit is enables + let colorPalette = this._dominantColorExtractor._getColorPalette(); + + // Slightly adjust the styling + this._borderWidth = 2; + + if (colorPalette !== null) { + this._borderColor = Clutter.color_from_string(colorPalette.lighter)[1] ; + this._bodyColor = Clutter.color_from_string(colorPalette.darker)[1]; + } else { + // Fallback + this._borderColor = Clutter.color_from_string('white')[1]; + this._bodyColor = Clutter.color_from_string('gray')[1]; + } + } + + // Apply dominant color if requested + if (settings.get_boolean('running-indicator-dominant-color')) { + let colorPalette = this._dominantColorExtractor._getColorPalette(); + if (colorPalette !== null) { + this._bodyColor = Clutter.color_from_string(colorPalette.original)[1]; + } + } + + // Finally, use customize style if requested + if (settings.get_boolean('custom-theme-customize-running-dots')) { + this._borderColor = Clutter.color_from_string(settings.get_string('custom-theme-running-dots-border-color'))[1]; + this._borderWidth = settings.get_int('custom-theme-running-dots-border-width'); + this._bodyColor = Clutter.color_from_string(settings.get_string('custom-theme-running-dots-color'))[1]; + } + } + + // Define the radius as an arbitrary size, but keep large enough to account + // for the drawing of the border. + this._radius = Math.max(this._width/22, this._borderWidth/2); + this._padding = 0; // distance from the margin + this._spacing = this._radius + this._borderWidth; // separation between the dots + } + + _updateIndicator() { + + let area = this._area; + let cr = this._area.get_context(); + + this._computeStyle(); + this._drawIndicator(cr); + cr.$dispose(); + } + + _drawIndicator(cr) { + // Draw the required numbers of dots + let n = this._number; + + cr.setLineWidth(this._borderWidth); + Clutter.cairo_set_source_color(cr, this._borderColor); + + // draw for the bottom case: + cr.translate((this._width - (2*n)*this._radius - (n-1)*this._spacing)/2, this._height - this._padding); + for (let i = 0; i < n; i++) { + cr.newSubPath(); + cr.arc((2*i+1)*this._radius + i*this._spacing, -this._radius - this._borderWidth/2, this._radius, 0, 2*Math.PI); + } + + cr.strokePreserve(); + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.fill(); + } + + destroy() { + this._area.destroy(); + super.destroy(); + } +}; + +// Adapted from dash-to-panel by Jason DeRose +// https://github.com/jderose9/dash-to-panel +var RunningIndicatorCiliora = class DashToDock_RunningIndicatorCiliora extends RunningIndicatorDots { + + _drawIndicator(cr) { + if (this._source.running) { + let size = Math.max(this._width/20, this._borderWidth); + let spacing = size; // separation between the dots + let lineLength = this._width - (size*(this._number-1)) - (spacing*(this._number-1)); + let padding = this._borderWidth; + // For the backlit case here we don't want the outer border visible + if (Docking.DockManager.settings.get_boolean('unity-backlit-items') && + !Docking.DockManager.settings.get_boolean('custom-theme-customize-running-dots')) + padding = 0; + let yOffset = this._height - padding - size; + + cr.setLineWidth(this._borderWidth); + Clutter.cairo_set_source_color(cr, this._borderColor); + + cr.translate(0, yOffset); + cr.newSubPath(); + cr.rectangle(0, 0, lineLength, size); + for (let i = 1; i < this._number; i++) { + cr.newSubPath(); + cr.rectangle(lineLength + (i*spacing) + ((i-1)*size), 0, size, size); + } + + cr.strokePreserve(); + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.fill(); + } + } +}; + +// Adapted from dash-to-panel by Jason DeRose +// https://github.com/jderose9/dash-to-panel +var RunningIndicatorSegmented = class DashToDock_RunningIndicatorSegmented extends RunningIndicatorDots { + + _drawIndicator(cr) { + if (this._source.running) { + let size = Math.max(this._width/20, this._borderWidth); + let spacing = Math.ceil(this._width/18); // separation between the dots + let dashLength = Math.ceil((this._width - ((this._number-1)*spacing))/this._number); + let lineLength = this._width - (size*(this._number-1)) - (spacing*(this._number-1)); + let padding = this._borderWidth; + // For the backlit case here we don't want the outer border visible + if (Docking.DockManager.settings.get_boolean('unity-backlit-items') && + !Docking.DockManager.settings.get_boolean('custom-theme-customize-running-dots')) + padding = 0; + let yOffset = this._height - padding - size; + + cr.setLineWidth(this._borderWidth); + Clutter.cairo_set_source_color(cr, this._borderColor); + + cr.translate(0, yOffset); + for (let i = 0; i < this._number; i++) { + cr.newSubPath(); + cr.rectangle(i*dashLength + i*spacing, 0, dashLength, size); + } + + cr.strokePreserve(); + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.fill() + } + } +}; + +// Adapted from dash-to-panel by Jason DeRose +// https://github.com/jderose9/dash-to-panel +var RunningIndicatorSolid = class DashToDock_RunningIndicatorSolid extends RunningIndicatorDots { + + _drawIndicator(cr) { + if (this._source.running) { + let size = Math.max(this._width/20, this._borderWidth); + let padding = this._borderWidth; + // For the backlit case here we don't want the outer border visible + if (Docking.DockManager.settings.get_boolean('unity-backlit-items') && + !Docking.DockManager.settings.get_boolean('custom-theme-customize-running-dots')) + padding = 0; + let yOffset = this._height - padding - size; + + cr.setLineWidth(this._borderWidth); + Clutter.cairo_set_source_color(cr, this._borderColor); + + cr.translate(0, yOffset); + cr.newSubPath(); + cr.rectangle(0, 0, this._width, size); + + cr.strokePreserve(); + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.fill(); + } + } +}; + +// Adapted from dash-to-panel by Jason DeRose +// https://github.com/jderose9/dash-to-panel +var RunningIndicatorSquares = class DashToDock_RunningIndicatorSquares extends RunningIndicatorDots { + + _drawIndicator(cr) { + if (this._source.running) { + let size = Math.max(this._width/11, this._borderWidth); + let padding = this._borderWidth; + let spacing = Math.ceil(this._width/18); // separation between the dots + let yOffset = this._height - padding - size; + + cr.setLineWidth(this._borderWidth); + Clutter.cairo_set_source_color(cr, this._borderColor); + + cr.translate(Math.floor((this._width - this._number*size - (this._number-1)*spacing)/2), yOffset); + for (let i = 0; i < this._number; i++) { + cr.newSubPath(); + cr.rectangle(i*size + i*spacing, 0, size, size); + } + cr.strokePreserve(); + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.fill(); + } + } +} + +// Adapted from dash-to-panel by Jason DeRose +// https://github.com/jderose9/dash-to-panel +var RunningIndicatorDashes = class DashToDock_RunningIndicatorDashes extends RunningIndicatorDots { + + _drawIndicator(cr) { + if (this._source.running) { + let size = Math.max(this._width/20, this._borderWidth); + let padding = this._borderWidth; + let spacing = Math.ceil(this._width/18); // separation between the dots + let dashLength = Math.floor(this._width/4) - spacing; + let yOffset = this._height - padding - size; + + cr.setLineWidth(this._borderWidth); + Clutter.cairo_set_source_color(cr, this._borderColor); + + cr.translate(Math.floor((this._width - this._number*dashLength - (this._number-1)*spacing)/2), yOffset); + for (let i = 0; i < this._number; i++) { + cr.newSubPath(); + cr.rectangle(i*dashLength + i*spacing, 0, dashLength, size); + } + + cr.strokePreserve(); + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.fill(); + } + } +} + +// Adapted from dash-to-panel by Jason DeRose +// https://github.com/jderose9/dash-to-panel +var RunningIndicatorMetro = class DashToDock_RunningIndicatorMetro extends RunningIndicatorDots { + + constructor(source) { + super(source); + this._source.add_style_class_name('metro'); + } + + destroy() { + this._source.remove_style_class_name('metro'); + super.destroy(); + } + + _drawIndicator(cr) { + if (this._source.running) { + let size = Math.max(this._width/20, this._borderWidth); + let padding = 0; + // For the backlit case here we don't want the outer border visible + if (Docking.DockManager.settings.get_boolean('unity-backlit-items') && + !Docking.DockManager.settings.get_boolean('custom-theme-customize-running-dots')) + padding = 0; + let yOffset = this._height - padding - size; + + let n = this._number; + if(n <= 1) { + cr.translate(0, yOffset); + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.newSubPath(); + cr.rectangle(0, 0, this._width, size); + cr.fill(); + } else { + let blackenedLength = (1/48)*this._width; // need to scale with the SVG for the stacked highlight + let darkenedLength = this._source.focused ? (2/48)*this._width : (10/48)*this._width; + let blackenedColor = this._bodyColor.shade(.3); + let darkenedColor = this._bodyColor.shade(.7); + + cr.translate(0, yOffset); + + Clutter.cairo_set_source_color(cr, this._bodyColor); + cr.newSubPath(); + cr.rectangle(0, 0, this._width - darkenedLength - blackenedLength, size); + cr.fill(); + Clutter.cairo_set_source_color(cr, blackenedColor); + cr.newSubPath(); + cr.rectangle(this._width - darkenedLength - blackenedLength, 0, 1, size); + cr.fill(); + Clutter.cairo_set_source_color(cr, darkenedColor); + cr.newSubPath(); + cr.rectangle(this._width - darkenedLength, 0, darkenedLength, size); + cr.fill(); + } + } + } +} + +/* + * Unity like notification and progress indicators + */ +var UnityIndicator = class DashToDock_UnityIndicator extends IndicatorBase { + + constructor(source) { + + super(source); + + this._notificationBadgeLabel = new St.Label(); + this._notificationBadgeBin = new St.Bin({ + child: this._notificationBadgeLabel, + x_align: Clutter.ActorAlign.END, + y_align: Clutter.ActorAlign.START, + x_expand: true, y_expand: true + }); + this._notificationBadgeLabel.add_style_class_name('notification-badge'); + this._notificationBadgeLabel.clutter_text.ellipsize = Pango.EllipsizeMode.MIDDLE; + this._notificationBadgeBin.hide(); + + this._source._iconContainer.add_child(this._notificationBadgeBin); + this.updateNotificationBadgeStyle(); + + const { remoteModel } = Docking.DockManager.getDefault(); + const remoteEntry = remoteModel.lookupById(this._source.app.id); + this._signalsHandler.add([ + remoteEntry, + ['count-changed', 'count-visible-changed'], + (sender, { count, count_visible }) => + this.setNotificationCount(count_visible ? count : 0) + ], [ + remoteEntry, + ['progress-changed', 'progress-visible-changed'], + (sender, { progress, progress_visible }) => + this.setProgress(progress_visible ? progress : -1) + ], [ + remoteEntry, + 'urgent-changed', + (sender, { urgent }) => this.setUrgent(urgent) + ], [ + St.ThemeContext.get_for_stage(global.stage), + 'changed', + this.updateNotificationBadgeStyle.bind(this) + ], [ + this._source._iconContainer, + 'notify::size', + this.updateNotificationBadgeStyle.bind(this) + ]); + } + + updateNotificationBadgeStyle() { + let themeContext = St.ThemeContext.get_for_stage(global.stage); + let fontDesc = themeContext.get_font(); + let defaultFontSize = fontDesc.get_size() / 1024; + let fontSize = defaultFontSize * 0.9; + let iconSize = Main.overview.dash.iconSize; + let defaultIconSize = Docking.DockManager.settings.get_default_value( + 'dash-max-icon-size').unpack(); + + if (!fontDesc.get_size_is_absolute()) { + // fontSize was exprimed in points, so convert to pixel + fontSize /= 0.75; + } + + fontSize = Math.round((iconSize / defaultIconSize) * fontSize); + let leftMargin = Math.round((iconSize / defaultIconSize) * 3); + + this._notificationBadgeLabel.set_style( + 'font-size: ' + fontSize + 'px;' + + 'margin-left: ' + leftMargin + 'px' + ); + } + + _notificationBadgeCountToText(count) { + if (count <= 9999) { + return count.toString(); + } else if (count < 1e5) { + let thousands = count / 1e3; + return thousands.toFixed(1).toString() + "k"; + } else if (count < 1e6) { + let thousands = count / 1e3; + return thousands.toFixed(0).toString() + "k"; + } else if (count < 1e8) { + let millions = count / 1e6; + return millions.toFixed(1).toString() + "M"; + } else if (count < 1e9) { + let millions = count / 1e6; + return millions.toFixed(0).toString() + "M"; + } else { + let billions = count / 1e9; + return billions.toFixed(1).toString() + "B"; + } + } + + setNotificationCount(count) { + if (count > 0) { + let text = this._notificationBadgeCountToText(count); + this._notificationBadgeLabel.set_text(text); + this._notificationBadgeBin.show(); + } else { + this._notificationBadgeBin.hide(); + } + } + + _showProgressOverlay() { + if (this._progressOverlayArea) { + this._updateProgressOverlay(); + return; + } + + this._progressOverlayArea = new St.DrawingArea({x_expand: true, y_expand: true}); + this._progressOverlayArea.add_style_class_name('progress-bar'); + this._progressOverlayArea.connect('repaint', () => { + this._drawProgressOverlay(this._progressOverlayArea); + }); + + this._source._iconContainer.add_child(this._progressOverlayArea); + let node = this._progressOverlayArea.get_theme_node(); + + let [hasColor, color] = node.lookup_color('-progress-bar-background', false); + if (hasColor) + this._progressbar_background = color + else + this._progressbar_background = new Clutter.Color({red: 204, green: 204, blue: 204, alpha: 255}); + + [hasColor, color] = node.lookup_color('-progress-bar-border', false); + if (hasColor) + this._progressbar_border = color; + else + this._progressbar_border = new Clutter.Color({red: 230, green: 230, blue: 230, alpha: 255}); + + this._updateProgressOverlay(); + } + + _hideProgressOverlay() { + if (this._progressOverlayArea) + this._progressOverlayArea.destroy(); + this._progressOverlayArea = null; + this._progressbar_background = null; + this._progressbar_border = null; + } + + _updateProgressOverlay() { + if (this._progressOverlayArea) + this._progressOverlayArea.queue_repaint(); + } + + _drawProgressOverlay(area) { + let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; + let [surfaceWidth, surfaceHeight] = area.get_surface_size(); + let cr = area.get_context(); + + let iconSize = this._source.icon.iconSize * scaleFactor; + + let x = Math.floor((surfaceWidth - iconSize) / 2); + let y = Math.floor((surfaceHeight - iconSize) / 2); + + let lineWidth = Math.floor(1.0 * scaleFactor); + let padding = Math.floor(iconSize * 0.05); + let width = iconSize - 2.0*padding; + let height = Math.floor(Math.min(18.0*scaleFactor, 0.20*iconSize)); + x += padding; + y += iconSize - height - padding; + + cr.setLineWidth(lineWidth); + + // Draw the outer stroke + let stroke = new Cairo.LinearGradient(0, y, 0, y + height); + let fill = null; + stroke.addColorStopRGBA(0.5, 0.5, 0.5, 0.5, 0.1); + stroke.addColorStopRGBA(0.9, 0.8, 0.8, 0.8, 0.4); + Utils.drawRoundedLine(cr, x + lineWidth/2.0, y + lineWidth/2.0, width, height, true, true, stroke, fill); + + // Draw the background + x += lineWidth; + y += lineWidth; + width -= 2.0*lineWidth; + height -= 2.0*lineWidth; + + stroke = Cairo.SolidPattern.createRGBA(0.20, 0.20, 0.20, 0.9); + fill = new Cairo.LinearGradient(0, y, 0, y + height); + fill.addColorStopRGBA(0.4, 0.25, 0.25, 0.25, 1.0); + fill.addColorStopRGBA(0.9, 0.35, 0.35, 0.35, 1.0); + Utils.drawRoundedLine(cr, x + lineWidth/2.0, y + lineWidth/2.0, width, height, true, true, stroke, fill); + + // Draw the finished bar + x += lineWidth; + y += lineWidth; + width -= 2.0*lineWidth; + height -= 2.0*lineWidth; + + let finishedWidth = Math.ceil(this._progress * width); + + let bg = this._progressbar_background; + let bd = this._progressbar_border; + + stroke = Cairo.SolidPattern.createRGBA(bd.red/255, bd.green/255, bd.blue/255, bd.alpha/255); + fill = Cairo.SolidPattern.createRGBA(bg.red/255, bg.green/255, bg.blue/255, bg.alpha/255); + + if (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL) + Utils.drawRoundedLine(cr, x + lineWidth/2.0 + width - finishedWidth, y + lineWidth/2.0, finishedWidth, height, true, true, stroke, fill); + else + Utils.drawRoundedLine(cr, x + lineWidth/2.0, y + lineWidth/2.0, finishedWidth, height, true, true, stroke, fill); + + cr.$dispose(); + } + + setProgress(progress) { + if (progress < 0) { + this._hideProgressOverlay(); + } else { + this._progress = Math.min(progress, 1.0); + this._showProgressOverlay(); + } + } + + setUrgent(urgent) { + if (urgent || this._isUrgent !== undefined) + this._source.urgent = urgent; + + if (urgent) + this._isUrgent = urgent; + else + delete this._isUrgent; + } +} + + +// Global icon cache. Used for Unity7 styling. +let iconCacheMap = new Map(); +// Max number of items to store +// We don't expect to ever reach this number, but let's put an hard limit to avoid +// even the remote possibility of the cached items to grow indefinitely. +const MAX_CACHED_ITEMS = 1000; +// When the size exceed it, the oldest 'n' ones are deleted +const BATCH_SIZE_TO_DELETE = 50; +// The icon size used to extract the dominant color +const DOMINANT_COLOR_ICON_SIZE = 64; + +// Compute dominant color frim the app icon. +// The color is cached for efficiency. +var DominantColorExtractor = class DashToDock_DominantColorExtractor { + + constructor(app) { + this._app = app; + } + + /** + * Try to get the pixel buffer for the current icon, if not fail gracefully + */ + _getIconPixBuf() { + let iconTexture = this._app.create_icon_texture(16); + const themeLoader = Docking.DockManager.iconTheme; + + // Unable to load the icon texture, use fallback + if (iconTexture instanceof St.Icon === false) { + return null; + } + + iconTexture = iconTexture.get_gicon(); + + // Unable to load the icon texture, use fallback + if (iconTexture === null) { + return null; + } + + if (iconTexture instanceof Gio.FileIcon) { + // Use GdkPixBuf to load the pixel buffer from the provided file path + return GdkPixbuf.Pixbuf.new_from_file(iconTexture.get_file().get_path()); + } + + // Get the pixel buffer from the icon theme + let icon_info = themeLoader.lookup_icon(iconTexture.get_names()[0], DOMINANT_COLOR_ICON_SIZE, 0); + if (icon_info !== null) + return icon_info.load_icon(); + else + return null; + } + + /** + * The backlight color choosing algorithm was mostly ported to javascript from the + * Unity7 C++ source of Canonicals: + * https://bazaar.launchpad.net/~unity-team/unity/trunk/view/head:/launcher/LauncherIcon.cpp + * so it more or less works the same way. + */ + _getColorPalette() { + if (iconCacheMap.get(this._app.get_id())) { + // We already know the answer + return iconCacheMap.get(this._app.get_id()); + } + + let pixBuf = this._getIconPixBuf(); + if (pixBuf == null) + return null; + + let pixels = pixBuf.get_pixels(), + offset = 0; + + let total = 0, + rTotal = 0, + gTotal = 0, + bTotal = 0; + + let resample_y = 1, + resample_x = 1; + + // Resampling of large icons + // We resample icons larger than twice the desired size, as the resampling + // to a size s + // DOMINANT_COLOR_ICON_SIZE < s < 2*DOMINANT_COLOR_ICON_SIZE, + // most of the case exactly DOMINANT_COLOR_ICON_SIZE as the icon size is tipycally + // a multiple of it. + let width = pixBuf.get_width(); + let height = pixBuf.get_height(); + + // Resample + if (height >= 2* DOMINANT_COLOR_ICON_SIZE) + resample_y = Math.floor(height/DOMINANT_COLOR_ICON_SIZE); + + if (width >= 2* DOMINANT_COLOR_ICON_SIZE) + resample_x = Math.floor(width/DOMINANT_COLOR_ICON_SIZE); + + if (resample_x !==1 || resample_y !== 1) + pixels = this._resamplePixels(pixels, resample_x, resample_y); + + // computing the limit outside the for (where it would be repeated at each iteration) + // for performance reasons + let limit = pixels.length; + for (let offset = 0; offset < limit; offset+=4) { + let r = pixels[offset], + g = pixels[offset + 1], + b = pixels[offset + 2], + a = pixels[offset + 3]; + + let saturation = (Math.max(r,g, b) - Math.min(r,g, b)); + let relevance = 0.1 * 255 * 255 + 0.9 * a * saturation; + + rTotal += r * relevance; + gTotal += g * relevance; + bTotal += b * relevance; + + total += relevance; + } + + total = total * 255; + + let r = rTotal / total, + g = gTotal / total, + b = bTotal / total; + + let hsv = Utils.ColorUtils.RGBtoHSV(r * 255, g * 255, b * 255); + + if (hsv.s > 0.15) + hsv.s = 0.65; + hsv.v = 0.90; + + let rgb = Utils.ColorUtils.HSVtoRGB(hsv.h, hsv.s, hsv.v); + + // Cache the result. + let backgroundColor = { + lighter: Utils.ColorUtils.ColorLuminance(rgb.r, rgb.g, rgb.b, 0.2), + original: Utils.ColorUtils.ColorLuminance(rgb.r, rgb.g, rgb.b, 0), + darker: Utils.ColorUtils.ColorLuminance(rgb.r, rgb.g, rgb.b, -0.5) + }; + + if (iconCacheMap.size >= MAX_CACHED_ITEMS) { + //delete oldest cached values (which are in order of insertions) + let ctr=0; + for (let key of iconCacheMap.keys()) { + if (++ctr > BATCH_SIZE_TO_DELETE) + break; + iconCacheMap.delete(key); + } + } + + iconCacheMap.set(this._app.get_id(), backgroundColor); + + return backgroundColor; + } + + /** + * Downsample large icons before scanning for the backlight color to + * improve performance. + * + * @param pixBuf + * @param pixels + * @param resampleX + * @param resampleY + * + * @return []; + */ + _resamplePixels (pixels, resampleX, resampleY) { + let resampledPixels = []; + // computing the limit outside the for (where it would be repeated at each iteration) + // for performance reasons + let limit = pixels.length / (resampleX * resampleY) / 4; + for (let i = 0; i < limit; i++) { + let pixel = i * resampleX * resampleY; + + resampledPixels.push(pixels[pixel * 4]); + resampledPixels.push(pixels[pixel * 4 + 1]); + resampledPixels.push(pixels[pixel * 4 + 2]); + resampledPixels.push(pixels[pixel * 4 + 3]); + } + + return resampledPixels; + } +}; diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/appIcons.js b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/appIcons.js new file mode 100644 index 0000000..cca3641 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/appIcons.js @@ -0,0 +1,1435 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Clutter = imports.gi.Clutter; +const GdkPixbuf = imports.gi.GdkPixbuf +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Signals = imports.signals; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; +const St = imports.gi.St; + +// Use __ () and N__() for the extension gettext domain, and reuse +// the shell domain with the default _() and N_() +const Gettext = imports.gettext.domain('dashtodock'); +const __ = Gettext.gettext; +const N__ = function(e) { return e }; + +const AppDisplay = imports.ui.appDisplay; +const AppFavorites = imports.ui.appFavorites; +const BoxPointer = imports.ui.boxpointer; +const Dash = imports.ui.dash; +const DND = imports.ui.dnd; +const IconGrid = imports.ui.iconGrid; +const Main = imports.ui.main; +const ParentalControlsManager = imports.misc.parentalControlsManager; +const PopupMenu = imports.ui.popupMenu; +const Util = imports.misc.util; +const Workspace = imports.ui.workspace; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Docking = Me.imports.docking; +const Utils = Me.imports.utils; +const WindowPreview = Me.imports.windowPreview; +const AppIconIndicators = Me.imports.appIconIndicators; +const DbusmenuUtils = Me.imports.dbusmenuUtils; + +const tracker = Shell.WindowTracker.get_default(); + +const clickAction = { + SKIP: 0, + MINIMIZE: 1, + LAUNCH: 2, + CYCLE_WINDOWS: 3, + MINIMIZE_OR_OVERVIEW: 4, + PREVIEWS: 5, + MINIMIZE_OR_PREVIEWS: 6, + FOCUS_OR_PREVIEWS: 7, + FOCUS_MINIMIZE_OR_PREVIEWS: 8, + QUIT: 9 +}; + +const scrollAction = { + DO_NOTHING: 0, + CYCLE_WINDOWS: 1, + SWITCH_WORKSPACE: 2 +}; + +let recentlyClickedAppLoopId = 0; +let recentlyClickedApp = null; +let recentlyClickedAppWindows = null; +let recentlyClickedAppIndex = 0; +let recentlyClickedAppMonitor = -1; + +/** + * Extend AppIcon + * + * - Apply a css class based on the number of windows of each application (#N); + * - Customized indicators for running applications in place of the default "dot" style which is hidden (#N); + * a class of the form "running#N" is applied to the AppWellIcon actor. + * like the original .running one. + * - Add a .focused style to the focused app + * - Customize click actions. + * - Update minimization animation target + * - Update menu if open on windows change + */ +var DockAbstractAppIcon = GObject.registerClass({ + GTypeFlags: GObject.TypeFlags.ABSTRACT, + Properties: { + 'focused': GObject.ParamSpec.boolean( + 'focused', 'focused', 'focused', + GObject.ParamFlags.READWRITE, + false), + 'running': GObject.ParamSpec.boolean( + 'running', 'running', 'running', + GObject.ParamFlags.READWRITE, + false), + 'urgent': GObject.ParamSpec.boolean( + 'urgent', 'urgent', 'urgent', + GObject.ParamFlags.READWRITE, + false), + 'windows-count': GObject.ParamSpec.uint( + 'windows-count', 'windows-count', 'windows-count', + GObject.ParamFlags.READWRITE, + 0, GLib.MAXUINT32, 0), + } +}, class DockAbstractAppIcon extends Dash.DashIcon { + // settings are required inside. + _init(app, monitorIndex, iconAnimator) { + super._init(app); + + // a prefix is required to avoid conflicting with the parent class variable + this.monitorIndex = monitorIndex; + this._signalsHandler = new Utils.GlobalSignalsHandler(this); + this.iconAnimator = iconAnimator; + this._indicator = new AppIconIndicators.AppIconIndicator(this); + + // Monitor windows-changes instead of app state. + // Keep using the same Id and function callback (that is extended) + if (this._stateChangedId > 0) { + this.app.disconnect(this._stateChangedId); + this._stateChangedId = 0; + } + + this._signalsHandler.add(this.app, 'windows-changed', () => this._updateWindows()); + this._signalsHandler.add(this.app, 'notify::state', () => this._updateRunningState()); + this._signalsHandler.add(global.display, 'window-demands-attention', (_dpy, window) => + this._onWindowDemandsAttention(window)); + this._signalsHandler.add(global.display, 'window-marked-urgent', (_dpy, window) => + this._onWindowDemandsAttention(window)); + + // In Wayland sessions, this signal is needed to track the state of windows dragged + // from one monitor to another. As this is triggered quite often (whenever a new winow + // of any application opened or moved to a different desktop), + // we restrict this signal to the case when 'isolate-monitors' is true, + // and if there are at least 2 monitors. + if (Docking.DockManager.settings.get_boolean('isolate-monitors') && + Main.layoutManager.monitors.length > 1) { + this._signalsHandler.removeWithLabel('isolate-monitors'); + this._signalsHandler.addWithLabel('isolate-monitors', + global.display, + 'window-entered-monitor', + this._onWindowEntered.bind(this)); + } + + this.connect('notify::running', () => { + if (this.running) + this.add_style_class_name('running'); + else + this.remove_style_class_name('running'); + }); + + this.connect('notify::focused', () => { + if (this.focused) + this.add_style_class_name('focused'); + else + this.remove_style_class_name('focused'); + }) + + this.connect('notify::urgent', () => { + const icon = this.icon._iconBin; + this._signalsHandler.removeWithLabel('urgent-windows') + if (this.urgent) { + icon.set_pivot_point(0.5, 0.5); + this.iconAnimator.addAnimation(icon, 'dance'); + if (!this._urgentWindows.size) { + const urgentWindows = this.getInterestingWindows(); + urgentWindows.forEach(w => (w._manualUrgency = true)); + this._updateUrgentWindows(urgentWindows); + } + } else { + this.iconAnimator.removeAnimation(icon, 'dance'); + icon.rotation_angle_z = 0; + this._urgentWindows.forEach(w => (delete w._manualUrgency)); + this._updateUrgentWindows(); + } + }); + + this._urgentWindows = new Set(); + this._progressOverlayArea = null; + this._progress = 0; + + let keys = ['apply-custom-theme', + 'running-indicator-style', + ]; + + keys.forEach(key => { + this._signalsHandler.add( + Docking.DockManager.settings, + 'changed::' + key, () => { + this._indicator.destroy(); + this._indicator = new AppIconIndicators.AppIconIndicator(this); + } + ); + }); + + this._updateState(); + this._numberOverlay(); + this.updateIconGeometry(); + + this._previewMenuManager = null; + this._previewMenu = null; + } + + _onDestroy() { + super._onDestroy(); + + // This is necessary due to an upstream bug + // https://bugzilla.gnome.org/show_bug.cgi?id=757556 + // It can be safely removed once it get solved upstrea. + if (this._menu) + this._menu.close(false); + } + + ownsWindow(window) { + return this.app === tracker.get_window_app(window); + } + + _onWindowEntered(metaScreen, monitorIndex, metaWin) { + if (this.ownsWindow(metaWin)) + this._updateWindows(); + } + + vfunc_scroll_event(scrollEvent) { + let settings = Docking.DockManager.settings; + let isEnabled = settings.get_enum('scroll-action') === scrollAction.CYCLE_WINDOWS; + if (!isEnabled) + return Clutter.EVENT_PROPAGATE; + + // We only activate windows of running applications, i.e. we never open new windows + // We check if the app is running, and that the # of windows is > 0 in + // case we use workspace isolation, + if (!this.running) + return Clutter.EVENT_PROPAGATE; + + if (this._optionalScrollCycleWindowsDeadTimeId) + return Clutter.EVENT_PROPAGATE; + else + this._optionalScrollCycleWindowsDeadTimeId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, 250, () => { + this._optionalScrollCycleWindowsDeadTimeId = 0; + }); + + let direction = null; + + switch (scrollEvent.direction) { + case Clutter.ScrollDirection.UP: + direction = Meta.MotionDirection.UP; + break; + case Clutter.ScrollDirection.DOWN: + direction = Meta.MotionDirection.DOWN; + break; + case Clutter.ScrollDirection.SMOOTH: + let [, dy] = Clutter.get_current_event().get_scroll_delta(); + if (dy < 0) + direction = Meta.MotionDirection.UP; + else if (dy > 0) + direction = Meta.MotionDirection.DOWN; + break; + } + + if (!Main.overview.visible) { + let reversed = direction === Meta.MotionDirection.UP; + if (this.focused && !this._urgentWindows.size) + this._cycleThroughWindows(reversed); + else { + // Activate the first window + let windows = this.getInterestingWindows(); + if (windows.length > 0) { + let w = windows[0]; + Main.activateWindow(w); + } + } + } + else + this.app.activate(); + return Clutter.EVENT_STOP; + } + + _updateWindows() { + if (this._menu && this._menu.isOpen) + this._menu.update(); + + this._updateState(); + this.updateIconGeometry(); + } + + _updateState() { + this._urgentWindows.clear(); + const interestingWindows = this.getInterestingWindows(); + this.windowsCount = interestingWindows.length; + this._updateRunningState(); + this._updateFocusState(); + this._updateUrgentWindows(interestingWindows); + } + + _updateRunningState() { + this.running = (this.app.state === Shell.AppState.RUNNING) && this.windowsCount; + } + + _updateFocusState() { + this.focused = (tracker.focus_app === this.app && this.running); + } + + _updateUrgentWindows(interestingWindows) { + this._signalsHandler.removeWithLabel('urgent-windows') + this._urgentWindows.clear(); + if (interestingWindows === undefined) + interestingWindows = this.getInterestingWindows(); + interestingWindows.forEach(win => { + if (win.urgent || win.demandsAttention || win._manualUrgency) + this._addUrgentWindow(win); + }); + this.urgent = !!this._urgentWindows.size; + } + + _onWindowDemandsAttention(window) { + if (this.ownsWindow(window)) + this._addUrgentWindow(window); + } + + _addUrgentWindow(window) { + if (this._urgentWindows.has(window)) + return; + + if (window._manualUrgency && window.has_focus()) { + delete window._manualUrgency; + return; + } + + this._urgentWindows.add(window); + this.urgent = true; + + const onDemandsAttentionChanged = () => { + if (!window.demandsAttention && !window.urgent && !window._manualUrgency) + this._updateUrgentWindows(); + }; + + if (window.demandsAttention) + this._signalsHandler.addWithLabel('urgent-windows', window, + 'notify::demands-attention', () => onDemandsAttentionChanged()); + if (window.urgent) + this._signalsHandler.addWithLabel('urgent-windows', window, + 'notify::urgent', () => onDemandsAttentionChanged()); + if (window._manualUrgency) { + this._signalsHandler.addWithLabel('urgent-windows', window, + 'focus', () => { + delete window._manualUrgency; + onDemandsAttentionChanged() + }); + } + } + + /** + * Update taraget for minimization animation + */ + updateIconGeometry() { + // If (for unknown reason) the actor is not on the stage the reported size + // and position are random values, which might exceeds the integer range + // resulting in an error when assigned to the a rect. This is a more like + // a workaround to prevent flooding the system with errors. + if (this.get_stage() == null) + return; + + let rect = new Meta.Rectangle(); + + [rect.x, rect.y] = this.get_transformed_position(); + [rect.width, rect.height] = this.get_transformed_size(); + + let windows = this.getWindows(); + if (Docking.DockManager.settings.get_boolean('multi-monitor')) { + let monitorIndex = this.monitorIndex; + windows = windows.filter(function(w) { + return w.get_monitor() == monitorIndex; + }); + } + windows.forEach(function(w) { + w.set_icon_geometry(rect); + }); + } + + _updateRunningStyle() { + // The logic originally in this function has been moved to + // AppIconIndicatorBase._updateDefaultDot(). However it cannot be removed as + // it called by the parent constructor. + } + + popupMenu() { + this._removeMenuTimeout(); + this.fake_release(); + this._draggable.fakeRelease(); + + if (!this._menu) { + this._menu = new DockAppIconMenu(this); + this._menu.connect('activate-window', (menu, window) => { + Main.activateWindow(window); + }); + this._menu.connect('open-state-changed', (menu, isPoppedUp) => { + if (!isPoppedUp) + this._onMenuPoppedDown(); + else { + // Setting the max-height is s useful if part of the menu is + // scrollable so the minimum height is smaller than the natural height. + let monitor_index = Main.layoutManager.findIndexForActor(this); + let workArea = Main.layoutManager.getWorkAreaForMonitor(monitor_index); + let position = Utils.getPosition(); + this._isHorizontal = ( position == St.Side.TOP || + position == St.Side.BOTTOM); + // If horizontal also remove the height of the dash + const { dockFixed: fixedDock } = Docking.DockManager.settings; + let additional_margin = this._isHorizontal && !fixedDock ? Main.overview.dash.height : 0; + let verticalMargins = this._menu.actor.margin_top + this._menu.actor.margin_bottom; + // Also set a max width to the menu, so long labels (long windows title) get truncated + this._menu.actor.style = ('max-height: ' + Math.round(workArea.height - additional_margin - verticalMargins) + 'px;' + + 'max-width: 400px'); + } + }); + let id = Main.overview.connect('hiding', () => { + this._menu.close(); + }); + this._menu.actor.connect('destroy', function() { + Main.overview.disconnect(id); + }); + + this._menuManager.addMenu(this._menu); + } + + this.emit('menu-state-changed', true); + + this.set_hover(true); + this._menu.popup(); + this._menuManager.ignoreRelease(); + this.emit('sync-tooltip'); + + return false; + } + + activate(button) { + let event = Clutter.get_current_event(); + let modifiers = event ? event.get_state() : 0; + + // Only consider SHIFT and CONTROL as modifiers (exclude SUPER, CAPS-LOCK, etc.) + modifiers = modifiers & (Clutter.ModifierType.SHIFT_MASK | Clutter.ModifierType.CONTROL_MASK); + + // We don't change the CTRL-click behaviour: in such case we just chain + // up the parent method and return. + if (modifiers & Clutter.ModifierType.CONTROL_MASK) { + // Keep default behaviour: launch new window + // By calling the parent method I make it compatible + // with other extensions tweaking ctrl + click + super.activate(button); + return; + } + + // We check what type of click we have and if the modifier SHIFT is + // being used. We then define what buttonAction should be for this + // event. + let buttonAction = 0; + let settings = Docking.DockManager.settings; + if (button && button == 2 ) { + if (modifiers & Clutter.ModifierType.SHIFT_MASK) + buttonAction = settings.get_enum('shift-middle-click-action'); + else + buttonAction = settings.get_enum('middle-click-action'); + } + else if (button && button == 1) { + if (modifiers & Clutter.ModifierType.SHIFT_MASK) + buttonAction = settings.get_enum('shift-click-action'); + else + buttonAction = settings.get_enum('click-action'); + } + + // We check if the app is running, and that the # of windows is > 0 in + // case we use workspace isolation. + let windows = this.getInterestingWindows(); + + // Some action modes (e.g. MINIMIZE_OR_OVERVIEW) require overview to remain open + // This variable keeps track of this + let shouldHideOverview = true; + + // We customize the action only when the application is already running + if (this.running) { + const hasUrgentWindows = !!this._urgentWindows.size; + const singleOrUrgentWindows = windows.length === 1 || hasUrgentWindows; + switch (buttonAction) { + case clickAction.MINIMIZE: + // In overview just activate the app, unless the acion is explicitely + // requested with a keyboard modifier + if (!Main.overview.visible || modifiers){ + // If we have button=2 or a modifier, allow minimization even if + // the app is not focused + if ((this.focused && !hasUrgentWindows) || button === 2 || modifiers & Clutter.ModifierType.SHIFT_MASK) { + // minimize all windows on double click and always in the case of primary click without + // additional modifiers + let click_count = 0; + if (Clutter.EventType.CLUTTER_BUTTON_PRESS) + click_count = event.get_click_count(); + let all_windows = (button == 1 && ! modifiers) || click_count > 1; + this._minimizeWindow(all_windows); + } + else + this._activateAllWindows(); + } + else { + let w = windows[0]; + Main.activateWindow(w); + } + break; + + case clickAction.MINIMIZE_OR_OVERVIEW: + // When a single window is present, toggle minimization + // If only one windows is present toggle minimization, but only when trigggered with the + // simple click action (no modifiers, no middle click). + if (singleOrUrgentWindows && !modifiers && button == 1) { + let w = windows[0]; + if (this.focused) { + // Window is raised, minimize it + this._minimizeWindow(w); + } else { + // Window is minimized, raise it + Main.activateWindow(w); + } + // Launch overview when multiple windows are present + // TODO: only show current app windows when gnome shell API will allow it + } else { + shouldHideOverview = false; + Main.overview.toggle(); + } + break; + + case clickAction.CYCLE_WINDOWS: + if (!Main.overview.visible) { + if (this.focused && !hasUrgentWindows) + this._cycleThroughWindows(); + else { + // Activate the first window + let w = windows[0]; + Main.activateWindow(w); + } + } + else + this.app.activate(); + break; + + case clickAction.FOCUS_OR_PREVIEWS: + if (this.focused && !hasUrgentWindows && + (windows.length > 1 || modifiers || button != 1)) { + this._windowPreviews(); + } else { + // Activate the first window + let w = windows[0]; + Main.activateWindow(w); + } + break; + + case clickAction.FOCUS_MINIMIZE_OR_PREVIEWS: + if (this.focused && !hasUrgentWindows) { + if (windows.length > 1 || modifiers || button != 1) + this._windowPreviews(); + else if (!Main.overview.visible) + this._minimizeWindow(); + } else { + // Activate the first window + let w = windows[0]; + Main.activateWindow(w); + } + break; + + case clickAction.LAUNCH: + this.launchNewWindow(); + break; + + case clickAction.PREVIEWS: + if (!Main.overview.visible) { + // If only one windows is present just switch to it, but only when trigggered with the + // simple click action (no modifiers, no middle click). + if (singleOrUrgentWindows && !modifiers && button == 1) { + let w = windows[0]; + Main.activateWindow(w); + } else + this._windowPreviews(); + } + else { + this.app.activate(); + } + break; + + case clickAction.MINIMIZE_OR_PREVIEWS: + // When a single window is present, toggle minimization + // If only one windows is present toggle minimization, but only when trigggered with the + // simple click action (no modifiers, no middle click). + if (!Main.overview.visible) { + if (singleOrUrgentWindows && !modifiers && button == 1) { + let w = windows[0]; + if (this.focused) { + // Window is raised, minimize it + this._minimizeWindow(w); + } else { + // Window is minimized, raise it + Main.activateWindow(w); + } + } else { + // Launch previews when multiple windows are present + this._windowPreviews(); + } + } else { + this.app.activate(); + } + break; + + case clickAction.QUIT: + this.closeAllWindows(); + break; + + case clickAction.SKIP: + let w = windows[0]; + Main.activateWindow(w); + break; + } + } + else { + this.launchNewWindow(); + } + + // Hide overview except when action mode requires it + if(shouldHideOverview) { + Main.overview.hide(); + } + } + + shouldShowTooltip() { + return this.hover && (!this._menu || !this._menu.isOpen) && + (!this._previewMenu || !this._previewMenu.isOpen); + } + + _windowPreviews() { + if (!this._previewMenu) { + this._previewMenuManager = new PopupMenu.PopupMenuManager(this); + + this._previewMenu = new WindowPreview.WindowPreviewMenu(this); + + this._previewMenuManager.addMenu(this._previewMenu); + + this._previewMenu.connect('open-state-changed', (menu, isPoppedUp) => { + if (!isPoppedUp) + this._onMenuPoppedDown(); + }); + let id = Main.overview.connect('hiding', () => { + this._previewMenu.close(); + }); + this._previewMenu.actor.connect('destroy', function() { + Main.overview.disconnect(id); + }); + + } + + if (this._previewMenu.isOpen) + this._previewMenu.close(); + else + this._previewMenu.popup(); + + return false; + } + + // Try to do the right thing when attempting to launch a new window of an app. In + // particular, if the application doens't allow to launch a new window, activate + // the existing window instead. + launchNewWindow(p) { + let appInfo = this.app.get_app_info(); + let actions = appInfo.list_actions(); + if (this.app.can_open_new_window()) { + this.animateLaunch(); + // This is used as a workaround for a bug resulting in no new windows being opened + // for certain running applications when calling open_new_window(). + // + // https://bugzilla.gnome.org/show_bug.cgi?id=756844 + // + // Similar to what done when generating the popupMenu entries, if the application provides + // a "New Window" action, use it instead of directly requesting a new window with + // open_new_window(), which fails for certain application, notably Nautilus. + if (actions.indexOf('new-window') == -1) { + this.app.open_new_window(-1); + } + else { + let i = actions.indexOf('new-window'); + if (i !== -1) + this.app.launch_action(actions[i], global.get_current_time(), -1); + } + } + else { + // Try to manually activate the first window. Otherwise, when the app is activated by + // switching to a different workspace, a launch spinning icon is shown and disappers only + // after a timeout. + let windows = this.getWindows(); + if (windows.length > 0) + Main.activateWindow(windows[0]) + else + this.app.activate(); + } + } + + _numberOverlay() { + // Add label for a Hot-Key visual aid + this._numberOverlayLabel = new St.Label(); + this._numberOverlayBin = new St.Bin({ + child: this._numberOverlayLabel, + x_align: Clutter.ActorAlign.START, + y_align: Clutter.ActorAlign.START, + x_expand: true, y_expand: true + }); + this._numberOverlayLabel.add_style_class_name('number-overlay'); + this._numberOverlayOrder = -1; + this._numberOverlayBin.hide(); + + this._iconContainer.add_child(this._numberOverlayBin); + + } + + updateNumberOverlay() { + // We apply an overall scale factor that might come from a HiDPI monitor. + // Clutter dimensions are in physical pixels, but CSS measures are in logical + // pixels, so make sure to consider the scale. + let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; + // Set the font size to something smaller than the whole icon so it is + // still visible. The border radius is large to make the shape circular + let [minWidth, natWidth] = this._iconContainer.get_preferred_width(-1); + let font_size = Math.round(Math.max(12, 0.3*natWidth) / scaleFactor); + let size = Math.round(font_size*1.2); + this._numberOverlayLabel.set_style( + 'font-size: ' + font_size + 'px;' + + 'border-radius: ' + this.icon.iconSize + 'px;' + + 'width: ' + size + 'px; height: ' + size +'px;' + ); + } + + setNumberOverlay(number) { + this._numberOverlayOrder = number; + this._numberOverlayLabel.set_text(number.toString()); + } + + toggleNumberOverlay(activate) { + if (activate && this._numberOverlayOrder > -1) { + this.updateNumberOverlay(); + this._numberOverlayBin.show(); + } + else + this._numberOverlayBin.hide(); + } + + _minimizeWindow(param) { + // Param true make all app windows minimize + let windows = this.getInterestingWindows(); + let current_workspace = global.workspace_manager.get_active_workspace(); + for (let i = 0; i < windows.length; i++) { + let w = windows[i]; + if (w.get_workspace() == current_workspace && w.showing_on_its_workspace()) { + w.minimize(); + // Just minimize one window. By specification it should be the + // focused window on the current workspace. + if(!param) + break; + } + } + } + + // By default only non minimized windows are activated. + // This activates all windows in the current workspace. + _activateAllWindows() { + // First activate first window so workspace is switched if needed. + // We don't do this if isolation is on! + if (!Docking.DockManager.settings.get_boolean('isolate-workspaces') && + !Docking.DockManager.settings.get_boolean('isolate-monitors')) + this.app.activate(); + + // then activate all other app windows in the current workspace + let windows = this.getInterestingWindows(); + let activeWorkspace = global.workspace_manager.get_active_workspace_index(); + + if (windows.length <= 0) + return; + + let activatedWindows = 0; + + for (let i = windows.length - 1; i >= 0; i--) { + if (windows[i].get_workspace().index() == activeWorkspace) { + Main.activateWindow(windows[i]); + activatedWindows++; + } + } + } + + //This closes all windows of the app. + closeAllWindows() { + let windows = this.getInterestingWindows(); + const time = global.get_current_time(); + windows.forEach(w => w.delete(time)); + } + + _cycleThroughWindows(reversed) { + // Store for a little amount of time last clicked app and its windows + // since the order changes upon window interaction + let MEMORY_TIME=3000; + + let app_windows = this.getInterestingWindows(); + + if (app_windows.length <1) + return + + if (recentlyClickedAppLoopId > 0) + GLib.source_remove(recentlyClickedAppLoopId); + recentlyClickedAppLoopId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, MEMORY_TIME, this._resetRecentlyClickedApp); + + // If there isn't already a list of windows for the current app, + // or the stored list is outdated, use the current windows list. + let monitorIsolation = Docking.DockManager.settings.get_boolean('isolate-monitors'); + if (!recentlyClickedApp || + recentlyClickedApp.get_id() != this.app.get_id() || + recentlyClickedAppWindows.length != app_windows.length || + (recentlyClickedAppMonitor != this.monitorIndex && monitorIsolation)) { + recentlyClickedApp = this.app; + recentlyClickedAppWindows = app_windows; + recentlyClickedAppMonitor = this.monitorIndex; + recentlyClickedAppIndex = 0; + } + + if (reversed) { + recentlyClickedAppIndex--; + if (recentlyClickedAppIndex < 0) recentlyClickedAppIndex = recentlyClickedAppWindows.length - 1; + } else { + recentlyClickedAppIndex++; + } + let index = recentlyClickedAppIndex % recentlyClickedAppWindows.length; + let window = recentlyClickedAppWindows[index]; + + Main.activateWindow(window); + } + + _resetRecentlyClickedApp() { + if (recentlyClickedAppLoopId > 0) + GLib.source_remove(recentlyClickedAppLoopId); + recentlyClickedAppLoopId=0; + recentlyClickedApp =null; + recentlyClickedAppWindows = null; + recentlyClickedAppIndex = 0; + recentlyClickedAppMonitor = -1; + + return false; + } + + getWindows() { + return this.app.get_windows(); + } + + // Filter out unnecessary windows, for instance + // nautilus desktop window. + getInterestingWindows() { + const interestingWindows = getInterestingWindows(this.getWindows(), + this.monitorIndex); + + if (!this._urgentWindows.size) + return interestingWindows; + + return [...new Set([...this._urgentWindows, ...interestingWindows])]; + } +}); + +var DockAppIcon = GObject.registerClass({ +}, class DockAppIcon extends DockAbstractAppIcon { + _init(app, monitorIndex, iconAnimator) { + super._init(app, monitorIndex, iconAnimator); + + this._signalsHandler.add(tracker, 'notify::focus-app', () => this._updateFocusState()); + } +}); + +var DockLocationAppIcon = GObject.registerClass({ +}, class DockLocationAppIcon extends DockAbstractAppIcon { + _init(app, monitorIndex, iconAnimator) { + if (!app.location) + throw new Error('Provided application %s has no location'.format(app)); + + super._init(app, monitorIndex, iconAnimator); + + if (Docking.DockManager.settings.isolateLocations) { + this._signalsHandler.add(tracker, 'notify::focus-app', () => this._updateFocusState()); + } else { + this._signalsHandler.add(global.display, 'notify::focus-window', + () => this._updateFocusState()); + } + } + + get location() { + return this.app.location; + } + + _updateFocusState() { + if (Docking.DockManager.settings.isolateLocations) + return super._updateFocusState(); + + this.focused = (this.app.isFocused && this.running); + } +}); + +function makeAppIcon(app, monitorIndex, iconAnimator) { + if (app.location) + return new DockLocationAppIcon(app, monitorIndex, iconAnimator); + + + return new DockAppIcon(app, monitorIndex, iconAnimator); +} + +let discreteGpuAvailable = AppDisplay.discreteGpuAvailable; + +/** + * DockAppIconMenu + * + * - set popup arrow side based on dash orientation + * - Add close windows option based on quitfromdash extension + * (https://github.com/deuill/shell-extension-quitfromdash) + * - Add open windows thumbnails instead of list + * - update menu when application windows change + */ +const DockAppIconMenu = class DockAppIconMenu extends PopupMenu.PopupMenu { + + constructor(source) { + super(source, 0.5, Utils.getPosition()); + + this._signalsHandler = new Utils.GlobalSignalsHandler(this); + + // We want to keep the item hovered while the menu is up + this.blockSourceEvents = true; + + this._source = source; + this._parentalControlsManager = ParentalControlsManager.getDefault(); + + this.actor.add_style_class_name('app-menu'); + this.actor.add_style_class_name('app-well-menu'); + this.actor.add_style_class_name('dock-app-menu'); + + // Chain our visibility and lifecycle to that of the source + this._signalsHandler.add(source, 'notify::mapped', () => { + if (!source.mapped) + this.close(); + }); + source.connect('destroy', () => this.destroy()); + + Main.uiGroup.add_actor(this.actor); + + const { remoteModel } = Docking.DockManager.getDefault(); + const remoteModelApp = remoteModel?.lookupById(this._source?.app?.id); + if (remoteModelApp && DbusmenuUtils.haveDBusMenu()) { + const [onQuicklist, onDynamicSection] = Utils.splitHandler((sender, { quicklist }, dynamicSection) => { + dynamicSection.removeAll(); + if (quicklist) { + quicklist.get_children().forEach(remoteItem => + dynamicSection.addMenuItem(DbusmenuUtils.makePopupMenuItem(remoteItem, false))); + } + }); + + this._signalsHandler.add([ + remoteModelApp, + 'quicklist-changed', + onQuicklist + ], [ + this, + 'dynamic-section-changed', + onDynamicSection + ]); + } + + if (discreteGpuAvailable === undefined) { + const updateDiscreteGpuAvailable = () => { + const switcherooProxy = global.get_switcheroo_control(); + if (switcherooProxy) { + const prop = switcherooProxy.get_cached_property('HasDualGpu'); + discreteGpuAvailable = prop?.unpack() ?? false; + } else { + discreteGpuAvailable = false; + } + } + updateDiscreteGpuAvailable(); + global.connect('notify::switcheroo-control', + () => updateDiscreteGpuAvailable()); + } + } + + _appendSeparator() { + this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + } + + _appendMenuItem(labelText) { + const item = new PopupMenu.PopupMenuItem(labelText); + this.addMenuItem(item); + return item; + } + + popup(_activatingButton) { + this._rebuildMenu(); + this.open(BoxPointer.PopupAnimation.FULL); + } + + _rebuildMenu() { + this.removeAll(); + + if (Docking.DockManager.settings.get_boolean('show-windows-preview')) { + // Display the app windows menu items and the separator between windows + // of the current desktop and other windows. + + this._allWindowsMenuItem = new PopupMenu.PopupSubMenuMenuItem(__('All Windows'), false); + this._allWindowsMenuItem.hide(); + this.addMenuItem(this._allWindowsMenuItem); + } else { + const windows = this._source.getInterestingWindows().filter( + w => !w.skip_taskbar); + + if (windows.length > 0) { + this.addMenuItem( + /* Translators: This is the heading of a list of open windows */ + new PopupMenu.PopupSeparatorMenuItem(_('Open Windows'))); + } + + windows.forEach(window => { + let title = window.title + ? window.title : this._source.app.get_name(); + let item = this._appendMenuItem(title); + item.connect('activate', () => { + this.emit('activate-window', window); + }); + }); + } + + if (!this._source.app.is_window_backed()) { + this._appendSeparator(); + + let appInfo = this._source.app.get_app_info(); + let actions = appInfo.list_actions(); + if (this._source.app.can_open_new_window() && + actions.indexOf('new-window') == -1) { + this._newWindowMenuItem = this._appendMenuItem(_('New Window')); + this._newWindowMenuItem.connect('activate', () => { + if (this._source.app.state == Shell.AppState.STOPPED) + this._source.animateLaunch(); + + this._source.app.open_new_window(-1); + this.emit('activate-window', null); + }); + this._appendSeparator(); + } + + if (discreteGpuAvailable && + this._source.app.state == Shell.AppState.STOPPED) { + const appPrefersNonDefaultGPU = appInfo.get_boolean('PrefersNonDefaultGPU'); + const gpuPref = appPrefersNonDefaultGPU + ? Shell.AppLaunchGpu.DEFAULT + : Shell.AppLaunchGpu.DISCRETE; + this._onGpuMenuItem = this._appendMenuItem(appPrefersNonDefaultGPU + ? _('Launch using Integrated Graphics Card') + : _('Launch using Discrete Graphics Card')); + this._onGpuMenuItem.connect('activate', () => { + this._source.animateLaunch(); + this._source.app.launch(0, -1, gpuPref); + this.emit('activate-window', null); + }); + } + + for (let i = 0; i < actions.length; i++) { + let action = actions[i]; + let item = this._appendMenuItem(appInfo.get_action_name(action)); + item.connect('activate', (emitter, event) => { + this._source.app.launch_action(action, event.get_time(), -1); + this.emit('activate-window', null); + }); + } + + let canFavorite = global.settings.is_writable('favorite-apps') && + (this._source instanceof DockAppIcon) && + this._parentalControlsManager.shouldShowApp(this._source.app.app_info); + + if (canFavorite) { + this._appendSeparator(); + + let isFavorite = AppFavorites.getAppFavorites().isFavorite(this._source.app.get_id()); + + if (isFavorite) { + let item = this._appendMenuItem(_('Remove from Favorites')); + item.connect('activate', () => { + let favs = AppFavorites.getAppFavorites(); + favs.removeFavorite(this._source.app.get_id()); + }); + } else { + let item = this._appendMenuItem(_('Add to Favorites')); + item.connect('activate', () => { + let favs = AppFavorites.getAppFavorites(); + favs.addFavorite(this._source.app.get_id()); + }); + } + } + + if (Shell.AppSystem.get_default().lookup_app('org.gnome.Software.desktop') && + (this._source instanceof DockAppIcon)) { + this._appendSeparator(); + let item = this._appendMenuItem(_('Show Details')); + item.connect('activate', () => { + let id = this._source.app.get_id(); + let args = GLib.Variant.new('(ss)', [id, '']); + Gio.DBus.get(Gio.BusType.SESSION, null, + function(o, res) { + let bus = Gio.DBus.get_finish(res); + bus.call('org.gnome.Software', + '/org/gnome/Software', + 'org.gtk.Actions', 'Activate', + GLib.Variant.new('(sava{sv})', + ['details', [args], null]), + null, 0, -1, null, null); + Main.overview.hide(); + }); + }); + } + } + + // dynamic menu + const items = this._getMenuItems(); + let i = items.length; + if (Shell.AppSystem.get_default().lookup_app('org.gnome.Software.desktop')) { + i -= 2; + } + if (global.settings.is_writable('favorite-apps')) { + i -= 2; + } + if (i < 0) { + i = 0; + } + const dynamicSection = new PopupMenu.PopupMenuSection(); + this.addMenuItem(dynamicSection, i); + this.emit('dynamic-section-changed', dynamicSection); + + // quit menu + this._appendSeparator(); + this._quitfromDashMenuItem = this._appendMenuItem(_('Quit')); + this._quitfromDashMenuItem.connect('activate', () => { + this._source.closeAllWindows(); + }); + + this.update(); + } + + // update menu content when application windows change. This is desirable as actions + // acting on windows (closing) are performed while the menu is shown. + update() { + // update, show or hide the quit menu + if (this._source.windowsCount > 0) { + let quitFromDashMenuText = ""; + if (this._source.windowsCount == 1) + this._quitfromDashMenuItem.label.set_text(_('Quit')); + else + this._quitfromDashMenuItem.label.set_text(__('Quit %d Windows').format(this._source.windowsCount)); + + this._quitfromDashMenuItem.actor.show(); + + } else { + this._quitfromDashMenuItem.actor.hide(); + } + + if (Docking.DockManager.settings.get_boolean('show-windows-preview')){ + const windows = this._source.getInterestingWindows(); + + // update, show, or hide the allWindows menu + // Check if there are new windows not already displayed. In such case, repopulate the allWindows + // menu. Windows removal is already handled by each preview being connected to the destroy signal + let old_windows = this._allWindowsMenuItem.menu._getMenuItems().map(function(item){ + return item._window; + }); + + let new_windows = windows.filter(function(w) {return old_windows.indexOf(w) < 0;}); + if (new_windows.length > 0) { + this._populateAllWindowMenu(windows); + + // Try to set the width to that of the submenu. + // TODO: can't get the actual size, getting a bit less. + // Temporary workaround: add 15px to compensate + this._allWindowsMenuItem.width = this._allWindowsMenuItem.menu.actor.width + 15; + + } + + // The menu is created hidden and never hidded after being shown. Instead, a singlal + // connected to its items destroy will set is insensitive if no more windows preview are shown. + if (windows.length > 0){ + this._allWindowsMenuItem.show(); + this._allWindowsMenuItem.setSensitive(true); + } + } + + // Update separators + this._getMenuItems().forEach(item => { + if ('label' in item) { + this._updateSeparatorVisibility(item); + } + }); + } + + _populateAllWindowMenu(windows) { + + this._allWindowsMenuItem.menu.removeAll(); + + if (windows.length > 0) { + + let activeWorkspace = global.workspace_manager.get_active_workspace(); + let separatorShown = windows[0].get_workspace() != activeWorkspace; + + for (let i = 0; i < windows.length; i++) { + let window = windows[i]; + if (!separatorShown && window.get_workspace() != activeWorkspace) { + this._allWindowsMenuItem.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + separatorShown = true; + } + + let item = new WindowPreview.WindowPreviewMenuItem(window); + this._allWindowsMenuItem.menu.addMenuItem(item); + item.connect('activate', () => { + this.emit('activate-window', window); + }); + + // This is to achieve a more gracefull transition when the last windows is closed. + item.connect('destroy', () => { + if(this._allWindowsMenuItem.menu._getMenuItems().length == 1) // It's still counting the item just going to be destroyed + this._allWindowsMenuItem.setSensitive(false); + }); + } + } + } +}; + +// Filter out unnecessary windows, for instance +// nautilus desktop window. +function getInterestingWindows(windows, monitorIndex) { + let settings = Docking.DockManager.settings; + + // When using workspace isolation, we filter out windows + // that are not in the current workspace + if (settings.get_boolean('isolate-workspaces')) + windows = windows.filter(function(w) { + return w.get_workspace().index() == global.workspace_manager.get_active_workspace_index(); + }); + + if (settings.get_boolean('isolate-monitors')) + windows = windows.filter(function(w) { + return w.get_monitor() == monitorIndex; + }); + + return windows; +} + +/** + * A ShowAppsIcon improved class. + * + * - set label position based on dash orientation (Note, I am reusing most machinery of the appIcon class) + * - implement a popupMenu based on the AppIcon code (Note, I am reusing most machinery of the appIcon class) + * + */ + +var DockShowAppsIcon = GObject.registerClass({ + Signals: { + 'menu-state-changed': { param_types: [GObject.TYPE_BOOLEAN] }, + 'sync-tooltip': {} + } +} +, class DockShowAppsIcon extends Dash.ShowAppsIcon { + _init() { + super._init(); + + // Re-use appIcon methods + let appIconPrototype = AppDisplay.AppIcon.prototype; + this.toggleButton.y_expand = false; + this.toggleButton.connect('popup-menu', + appIconPrototype._onKeyboardPopupMenu.bind(this)); + this.toggleButton.connect('clicked', + this._removeMenuTimeout.bind(this)); + + this.reactive = true; + this.toggleButton.popupMenu = () => this.popupMenu.call(this); + this.toggleButton._removeMenuTimeout = () => this._removeMenuTimeout.call(this); + + this._menu = null; + this._menuManager = new PopupMenu.PopupMenuManager(this); + this._menuTimeoutId = 0; + } + + vfunc_leave_event(leaveEvent) + { + return AppDisplay.AppIcon.prototype.vfunc_leave_event.call( + this.toggleButton, leaveEvent); + } + + vfunc_button_press_event(buttonPressEvent) + { + return AppDisplay.AppIcon.prototype.vfunc_button_press_event.call( + this.toggleButton, buttonPressEvent); + } + + vfunc_touch_event(touchEvent) + { + return AppDisplay.AppIcon.prototype.vfunc_touch_event.call( + this.toggleButton, touchEvent); + } + + showLabel() { + itemShowLabel.call(this); + } + + _onMenuPoppedDown() { + AppDisplay.AppIcon.prototype._onMenuPoppedDown.apply(this, arguments); + } + + _setPopupTimeout() { + AppDisplay.AppIcon.prototype._onMenuPoppedDown.apply(this, arguments); + } + + _removeMenuTimeout() { + AppDisplay.AppIcon.prototype._removeMenuTimeout.apply(this, arguments); + } + + popupMenu() { + this._removeMenuTimeout(); + this.toggleButton.fake_release(); + + if (!this._menu) { + this._menu = new DockShowAppsIconMenu(this); + this._menu.connect('open-state-changed', (menu, isPoppedUp) => { + if (!isPoppedUp) + this._onMenuPoppedDown(); + }); + let id = Main.overview.connect('hiding', () => { + this._menu.close(); + }); + this._menu.actor.connect('destroy', function() { + Main.overview.disconnect(id); + }); + this._menuManager.addMenu(this._menu); + } + + this.emit('menu-state-changed', true); + + this.toggleButton.set_hover(true); + this._menu.popup(); + this._menuManager.ignoreRelease(); + this.emit('sync-tooltip'); + + return false; + } +}); + + +/** + * A menu for the showAppsIcon + */ +class DockShowAppsIconMenu extends DockAppIconMenu { + _rebuildMenu() { + this.removeAll(); + + /* Translators: %s is "Settings", which is automatically translated. You + can also translate the full message if this fits better your language. */ + let name = __('Dash to Dock %s').format(_('Settings')) + let item = this._appendMenuItem(name); + + item.connect('activate', function () { + ExtensionUtils.openPrefs(); + }); + } +}; + +/** + * This function is used for both extendShowAppsIcon and extendDashItemContainer + */ +function itemShowLabel() { + // Check if the label is still present at all. When switching workpaces, the + // item might have been destroyed in between. + if (!this._labelText || this.label.get_stage() == null) + return; + + this.label.set_text(this._labelText); + this.label.opacity = 0; + this.label.show(); + + let [stageX, stageY] = this.get_transformed_position(); + let node = this.label.get_theme_node(); + + let itemWidth = this.allocation.x2 - this.allocation.x1; + let itemHeight = this.allocation.y2 - this.allocation.y1; + + let labelWidth = this.label.get_width(); + let labelHeight = this.label.get_height(); + + let x, y, xOffset, yOffset; + + let position = Utils.getPosition(); + this._isHorizontal = ((position == St.Side.TOP) || (position == St.Side.BOTTOM)); + let labelOffset = node.get_length('-x-offset'); + + switch (position) { + case St.Side.LEFT: + yOffset = Math.floor((itemHeight - labelHeight) / 2); + y = stageY + yOffset; + xOffset = labelOffset; + x = stageX + this.get_width() + xOffset; + break; + case St.Side.RIGHT: + yOffset = Math.floor((itemHeight - labelHeight) / 2); + y = stageY + yOffset; + xOffset = labelOffset; + x = Math.round(stageX) - labelWidth - xOffset; + break; + case St.Side.TOP: + y = stageY + labelOffset + itemHeight; + xOffset = Math.floor((itemWidth - labelWidth) / 2); + x = stageX + xOffset; + break; + case St.Side.BOTTOM: + yOffset = labelOffset; + y = stageY - labelHeight - yOffset; + xOffset = Math.floor((itemWidth - labelWidth) / 2); + x = stageX + xOffset; + break; + } + + // keep the label inside the screen border + // Only needed fot the x coordinate. + + // Leave a few pixel gap + let gap = 5; + let monitor = Main.layoutManager.findMonitorForActor(this); + if (x - monitor.x < gap) + x += monitor.x - x + labelOffset; + else if (x + labelWidth > monitor.x + monitor.width - gap) + x -= x + labelWidth - (monitor.x + monitor.width) + gap; + + this.label.remove_all_transitions(); + this.label.set_position(x, y); + this.label.ease({ + opacity: 255, + duration: Dash.DASH_ITEM_LABEL_SHOW_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD + }); +} diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/dash.js b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/dash.js new file mode 100644 index 0000000..a7561e8 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/dash.js @@ -0,0 +1,1113 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Clutter = imports.gi.Clutter; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; +const St = imports.gi.St; + +const AppDisplay = imports.ui.appDisplay; +const AppFavorites = imports.ui.appFavorites; +const Dash = imports.ui.dash; +const DND = imports.ui.dnd; +const IconGrid = imports.ui.iconGrid; +const Main = imports.ui.main; +const PopupMenu = imports.ui.popupMenu; +const Util = imports.misc.util; +const Workspace = imports.ui.workspace; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Docking = Me.imports.docking; +const Utils = Me.imports.utils; +const AppIcons = Me.imports.appIcons; +const Locations = Me.imports.locations; + +const DASH_ANIMATION_TIME = Dash.DASH_ANIMATION_TIME; +const DASH_ITEM_LABEL_HIDE_TIME = Dash.DASH_ITEM_LABEL_HIDE_TIME; +const DASH_ITEM_HOVER_TIMEOUT = Dash.DASH_ITEM_HOVER_TIMEOUT; +const DASH_VISIBILITY_TIMEOUT = 3; + +/** + * Extend DashItemContainer + * + * - set label position based on dash orientation + * + */ +var DockDashItemContainer = GObject.registerClass( +class DockDashItemContainer extends Dash.DashItemContainer { + + showLabel() { + return AppIcons.itemShowLabel.call(this); + } +}); + +const DockDashIconsVerticalLayout = GObject.registerClass( + class DockDashIconsVerticalLayout extends Clutter.BoxLayout { + _init() { + super._init({ + orientation: Clutter.Orientation.VERTICAL, + }); + } + + vfunc_get_preferred_height(container, forWidth) { + const [natHeight] = super.vfunc_get_preferred_height(container, forWidth); + return [natHeight, 0]; + } +}); + + +const baseIconSizes = [16, 22, 24, 32, 48, 64, 96, 128]; + +/** + * This class is a fork of the upstream dash class (ui.dash.js) + * + * Summary of changes: + * - disconnect global signals adding a destroy method; + * - play animations even when not in overview mode + * - set a maximum icon size + * - show running and/or favorite applications + * - hide showApps label when the custom menu is shown. + * - add scrollview + * ensure actor is visible on keyfocus inseid the scrollview + * - add 128px icon size, might be useful for hidpi display + * - sync minimization application target position. + * - keep running apps ordered. + */ +var DockDash = GObject.registerClass({ + Properties: { + 'requires-visibility': GObject.ParamSpec.boolean( + 'requires-visibility', 'requires-visibility', 'requires-visibility', + GObject.ParamFlags.READWRITE, + false), + }, + Signals: { + 'menu-closed': {}, + 'icon-size-changed': {}, + } +}, class DockDash extends St.Widget { + + _init(monitorIndex) { + // Initialize icon variables and size + this._maxWidth = -1; + this._maxHeight = -1; + this.iconSize = Docking.DockManager.settings.get_int('dash-max-icon-size'); + this._availableIconSizes = baseIconSizes; + this._shownInitially = false; + this._initializeIconSize(this.iconSize); + + this._separator = null; + + this._monitorIndex = monitorIndex; + this._position = Utils.getPosition(); + this._isHorizontal = ((this._position == St.Side.TOP) || + (this._position == St.Side.BOTTOM)); + + this._dragPlaceholder = null; + this._dragPlaceholderPos = -1; + this._animatingPlaceholdersCount = 0; + this._showLabelTimeoutId = 0; + this._resetHoverTimeoutId = 0; + this._labelShowing = false; + + super._init({ + name: 'dash', + offscreen_redirect: Clutter.OffscreenRedirect.ALWAYS, + layout_manager: new Clutter.BinLayout() + }); + + this._dashContainer = new St.BoxLayout({ + name: "dashtodockDashContainer", + x_align: Clutter.ActorAlign.CENTER, + y_align: Clutter.ActorAlign.CENTER, + vertical: !this._isHorizontal, + y_expand: this._isHorizontal, + x_expand: !this._isHorizontal, + }); + + this._scrollView = new St.ScrollView({ + name: 'dashtodockDashScrollview', + hscrollbar_policy: this._isHorizontal ? St.PolicyType.EXTERNAL : St.PolicyType.NEVER, + vscrollbar_policy: this._isHorizontal ? St.PolicyType.NEVER : St.PolicyType.EXTERNAL, + x_expand: this._isHorizontal, + y_expand: !this._isHorizontal, + enable_mouse_scrolling: false + }); + + if (Docking.DockManager.settings.dockExtended) { + if (!this._isHorizontal) { + this._scrollView.y_align = Clutter.ActorAlign.START; + } else { + this._scrollView.x_align = Clutter.ActorAlign.START; + } + } + + this._scrollView.connect('scroll-event', this._onScrollEvent.bind(this)); + + let rtl = Clutter.get_default_text_direction() == Clutter.TextDirection.RTL; + this._box = new St.BoxLayout({ + vertical: !this._isHorizontal, + clip_to_allocation: false, + ...(!this._isHorizontal ? { layout_manager: new DockDashIconsVerticalLayout() } : {}), + x_align: rtl ? Clutter.ActorAlign.END : Clutter.ActorAlign.START, + y_align: this._isHorizontal ? Clutter.ActorAlign.CENTER: Clutter.ActorAlign.START, + y_expand: !this._isHorizontal, + x_expand: this._isHorizontal + }); + this._box._delegate = this; + this._dashContainer.add_actor(this._scrollView); + this._scrollView.add_actor(this._box); + + this._showAppsIcon = new AppIcons.DockShowAppsIcon(); + this._showAppsIcon.show(false); + this._showAppsIcon.icon.setIconSize(this.iconSize); + this._showAppsIcon.x_expand = false; + this._showAppsIcon.y_expand = false; + if (!this._isHorizontal) + this._showAppsIcon.y_align = Clutter.ActorAlign.START; + this._hookUpLabel(this._showAppsIcon); + this._showAppsIcon.connect('menu-state-changed', (_icon, opened) => { + this._itemMenuStateChanged(this._showAppsIcon, opened); + }); + + if (Docking.DockManager.settings.get_boolean('show-apps-at-top')) { + this._dashContainer.insert_child_below(this._showAppsIcon, null); + } else { + this._dashContainer.insert_child_above(this._showAppsIcon, null); + } + + this._background = new St.Widget({ + style_class: 'dash-background', + y_expand: this._isHorizontal, + x_expand: !this._isHorizontal, + }); + + const sizerBox = new Clutter.Actor(); + sizerBox.add_constraint(new Clutter.BindConstraint({ + source: this._isHorizontal ? this._showAppsIcon.icon : this._dashContainer, + coordinate: Clutter.BindCoordinate.HEIGHT, + })); + sizerBox.add_constraint(new Clutter.BindConstraint({ + source: this._isHorizontal ? this._dashContainer : this._showAppsIcon.icon, + coordinate: Clutter.BindCoordinate.WIDTH, + })); + this._background.add_child(sizerBox); + + this.add_child(this._background); + this.add_child(this._dashContainer); + + this._workId = Main.initializeDeferredWork(this._box, this._redisplay.bind(this)); + + this._shellSettings = new Gio.Settings({ + schema_id: 'org.gnome.shell' + }); + + this._appSystem = Shell.AppSystem.get_default(); + + this.iconAnimator = new Docking.IconAnimator(this); + + this._signalsHandler = new Utils.GlobalSignalsHandler(this); + this._signalsHandler.add([ + this._appSystem, + 'installed-changed', + () => { + AppFavorites.getAppFavorites().reload(); + this._queueRedisplay(); + } + ], [ + AppFavorites.getAppFavorites(), + 'changed', + this._queueRedisplay.bind(this) + ], [ + this._appSystem, + 'app-state-changed', + this._queueRedisplay.bind(this) + ], [ + Main.overview, + 'item-drag-begin', + this._onItemDragBegin.bind(this) + ], [ + Main.overview, + 'item-drag-end', + this._onItemDragEnd.bind(this) + ], [ + Main.overview, + 'item-drag-cancelled', + this._onItemDragCancelled.bind(this) + ], [ + Main.overview, + 'window-drag-begin', + this._onWindowDragBegin.bind(this) + ], [ + Main.overview, + 'window-drag-cancelled', + this._onWindowDragEnd.bind(this) + ], [ + Main.overview, + 'window-drag-end', + this._onWindowDragEnd.bind(this) + ]); + + this.connect('destroy', this._onDestroy.bind(this)); + } + + vfunc_get_preferred_height(forWidth) { + let [minHeight, natHeight] = super.vfunc_get_preferred_height.call(this, forWidth); + if (!this._isHorizontal && this._maxHeight !== -1 && natHeight > this._maxHeight) + return [minHeight, this._maxHeight] + else + return [minHeight, natHeight] + } + + vfunc_get_preferred_width(forHeight) { + let [minWidth, natWidth] = super.vfunc_get_preferred_width.call(this, forHeight); + if (this._isHorizontal && this._maxWidth !== -1 && natWidth > this._maxWidth) + return [minWidth, this._maxWidth] + else + return [minWidth, natWidth] + } + + get _container() { + return this._dashContainer; + } + + _onDestroy() { + this.iconAnimator.destroy(); + + if (this._requiresVisibilityTimeout) + GLib.source_remove(this._requiresVisibilityTimeout); + } + + + _onItemDragBegin() { + return Dash.Dash.prototype._onItemDragBegin.call(this, ...arguments); + } + + _onItemDragCancelled() { + return Dash.Dash.prototype._onItemDragCancelled.call(this, ...arguments); + } + + _onItemDragEnd() { + return Dash.Dash.prototype._onItemDragEnd.call(this, ...arguments); + } + + _endItemDrag() { + return Dash.Dash.prototype._endItemDrag.call(this, ...arguments); + } + + _onItemDragMotion() { + return Dash.Dash.prototype._onItemDragMotion.call(this, ...arguments); + } + + _appIdListToHash() { + return Dash.Dash.prototype._appIdListToHash.call(this, ...arguments); + } + + _queueRedisplay() { + return Dash.Dash.prototype._queueRedisplay.call(this, ...arguments); + } + + _hookUpLabel() { + return Dash.Dash.prototype._hookUpLabel.call(this, ...arguments); + } + + _syncLabel() { + return Dash.Dash.prototype._syncLabel.call(this, ...arguments); + } + + _clearDragPlaceholder() { + return Dash.Dash.prototype._clearDragPlaceholder.call(this, ...arguments); + } + + _clearEmptyDropTarget() { + return Dash.Dash.prototype._clearEmptyDropTarget.call(this, ...arguments); + } + + handleDragOver(source, actor, x, y, time) { + let ret; + if (this._isHorizontal) { + ret = Dash.Dash.prototype.handleDragOver.call(this, source, actor, x, y, time); + + if (ret == DND.DragMotionResult.CONTINUE) + return ret; + } else { + const propertyInjections = new Utils.PropertyInjectionsHandler(); + propertyInjections.add(this._box, 'width', { + get: () => this._box.get_children().reduce((a, c) => a + c.height, 0), + }); + + if (this._dragPlaceholder) { + propertyInjections.add(this._dragPlaceholder, 'width', { + get: () => this._dragPlaceholder.height, + }); + } + + ret = Dash.Dash.prototype.handleDragOver.call(this, source, actor, y, x, time); + propertyInjections.destroy(); + + if (ret == DND.DragMotionResult.CONTINUE) + return ret; + + if (this._dragPlaceholder) { + this._dragPlaceholder.child.set_width(this.iconSize / 2); + this._dragPlaceholder.child.set_height(this.iconSize); + + let pos = this._dragPlaceholderPos; + if (this._isHorizontal && (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL)) + pos = this._box.get_children() - 1 - pos; + + if (pos != this._dragPlaceholderPos) { + this._dragPlaceholderPos = pos; + this._box.set_child_at_index(this._dragPlaceholder, + this._dragPlaceholderPos) + } + } + } + + if (this._dragPlaceholder) { + // Ensure the next and previous icon are visible when moving the placeholder + // (I assume there's room for both of them) + const children = this._box.get_children(); + if (this._dragPlaceholderPos > 0) + ensureActorVisibleInScrollView(this._scrollView, + children[this._dragPlaceholderPos - 1]); + + if (this._dragPlaceholderPos >= -1 && + this._dragPlaceholderPos < children.length - 1) + ensureActorVisibleInScrollView(this._scrollView, + children[this._dragPlaceholderPos + 1]); + } + + return ret; + } + + acceptDrop() { + return Dash.Dash.prototype.acceptDrop.call(this, ...arguments); + } + + _onWindowDragBegin() { + return Dash.Dash.prototype._onWindowDragBegin.call(this, ...arguments); + } + + _onWindowDragEnd() { + return Dash.Dash.prototype._onWindowDragEnd.call(this, ...arguments); + } + + _onScrollEvent(actor, event) { + // If scroll is not used because the icon is resized, let the scroll event propagate. + if (!Docking.DockManager.settings.get_boolean('icon-size-fixed')) + return Clutter.EVENT_PROPAGATE; + + // reset timeout to avid conflicts with the mousehover event + if (this._ensureAppIconVisibilityTimeoutId > 0) { + GLib.source_remove(this._ensureAppIconVisibilityTimeoutId); + this._ensureAppIconVisibilityTimeoutId = 0; + } + + // Skip to avoid double events mouse + // TODO: Horizontal events are emulated, potentially due to a conflict + // with the workspace switching gesture. + if (!this._isHorizontal && event.is_pointer_emulated()) { + return Clutter.EVENT_STOP; + } + + let adjustment, delta = 0; + + if (this._isHorizontal) + adjustment = this._scrollView.get_hscroll_bar().get_adjustment(); + else + adjustment = this._scrollView.get_vscroll_bar().get_adjustment(); + + let increment = adjustment.step_increment; + + if (this._isHorizontal) { + switch (event.get_scroll_direction()) { + case Clutter.ScrollDirection.LEFT: + delta = -increment; + break; + case Clutter.ScrollDirection.RIGHT: + delta = +increment; + break; + case Clutter.ScrollDirection.SMOOTH: + let [dx, dy] = event.get_scroll_delta(); + // TODO: Handle y + //delta = dy * increment; + // Also consider horizontal component, for instance touchpad + delta = dx * increment; + break; + } + } else { + switch (event.get_scroll_direction()) { + case Clutter.ScrollDirection.UP: + delta = -increment; + break; + case Clutter.ScrollDirection.DOWN: + delta = +increment; + break; + case Clutter.ScrollDirection.SMOOTH: + let [, dy] = event.get_scroll_delta(); + delta = dy * increment; + break; + } + } + + const value = adjustment.get_value(); + + // TODO: Remove this if possible. + if (Number.isNaN(value)) { + adjustment.set_value(delta); + } else { + adjustment.set_value(value + delta); + } + + return Clutter.EVENT_STOP; + } + + _createAppItem(app) { + const appIcon = new AppIcons.makeAppIcon(app, this._monitorIndex, this.iconAnimator); + + if (appIcon._draggable) { + appIcon._draggable.connect('drag-begin', () => { + appIcon.opacity = 50; + }); + appIcon._draggable.connect('drag-end', () => { + appIcon.opacity = 255; + }); + } + + appIcon.connect('menu-state-changed', (appIcon, opened) => { + this._itemMenuStateChanged(item, opened); + }); + + const item = new DockDashItemContainer(); + item.setChild(appIcon); + + appIcon.connect('notify::hover', () => { + if (appIcon.hover) { + this._ensureAppIconVisibilityTimeoutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, 100, () => { + ensureActorVisibleInScrollView(this._scrollView, appIcon); + this._ensureAppIconVisibilityTimeoutId = 0; + return GLib.SOURCE_REMOVE; + }); + } + else { + if (this._ensureAppIconVisibilityTimeoutId > 0) { + GLib.source_remove(this._ensureAppIconVisibilityTimeoutId); + this._ensureAppIconVisibilityTimeoutId = 0; + } + } + }); + + appIcon.connect('clicked', (actor) => { + ensureActorVisibleInScrollView(this._scrollView, actor); + }); + + appIcon.connect('key-focus-in', (actor) => { + let [x_shift, y_shift] = ensureActorVisibleInScrollView(this._scrollView, actor); + + // This signal is triggered also by mouse click. The popup menu is opened at the original + // coordinates. Thus correct for the shift which is going to be applied to the scrollview. + if (appIcon._menu) { + appIcon._menu._boxPointer.xOffset = -x_shift; + appIcon._menu._boxPointer.yOffset = -y_shift; + } + }); + + appIcon.connect('notify::focused', () => { + const { settings } = Docking.DockManager; + if (appIcon.focused && settings.get_boolean('scroll-to-focused-application')) + ensureActorVisibleInScrollView(this._scrollView, item); + }); + + appIcon.connect('notify::urgent', () => { + if (appIcon.urgent) { + ensureActorVisibleInScrollView(this._scrollView, item); + this._requireVisibility(); + } + }); + + // Override default AppIcon label_actor, now the + // accessible_name is set at DashItemContainer.setLabelText + appIcon.label_actor = null; + item.setLabelText(app.get_name()); + + appIcon.icon.setIconSize(this.iconSize); + this._hookUpLabel(item, appIcon); + + return item; + } + + _requireVisibility() { + this.requiresVisibility = true; + + if (this._requiresVisibilityTimeout) + GLib.source_remove(this._requiresVisibilityTimeout); + + this._requiresVisibilityTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, + DASH_VISIBILITY_TIMEOUT, () => { + this._requiresVisibilityTimeout = 0; + this.requiresVisibility = false; + }); + } + + /** + * Return an array with the "proper" appIcons currently in the dash + */ + getAppIcons() { + // Only consider children which are "proper" + // icons (i.e. ignoring drag placeholders) and which are not + // animating out (which means they will be destroyed at the end of + // the animation) + let iconChildren = this._box.get_children().filter(function(actor) { + return actor.child && + !!actor.child.icon && + !actor.animatingOut; + }); + + let appIcons = iconChildren.map(function(actor) { + return actor.child; + }); + + return appIcons; + } + + _updateAppsIconGeometry() { + let appIcons = this.getAppIcons(); + appIcons.forEach(function(icon) { + icon.updateIconGeometry(); + }); + } + + _itemMenuStateChanged(item, opened) { + Dash.Dash.prototype._itemMenuStateChanged.call(this, item, opened); + + if (!opened) { + // I want to listen from outside when a menu is closed. I used to + // add a custom signal to the appIcon, since gnome 3.8 the signal + // calling this callback was added upstream. + this.emit('menu-closed'); + } + } + + _adjustIconSize() { + // For the icon size, we only consider children which are "proper" + // icons (i.e. ignoring drag placeholders) and which are not + // animating out (which means they will be destroyed at the end of + // the animation) + let iconChildren = this._box.get_children().filter(actor => { + return actor.child && + actor.child._delegate && + actor.child._delegate.icon && + !actor.animatingOut; + }); + + iconChildren.push(this._showAppsIcon); + + if (this._maxWidth === -1 && this._maxHeight === -1) + return; + + // Check if the container is present in the stage. This avoids critical + // errors when unlocking the screen + if (!this._container.get_stage()) + return; + + const themeNode = this._dashContainer.get_theme_node(); + const maxAllocation = new Clutter.ActorBox({ + x1: 0, + y1: 0, + x2: this._isHorizontal ? this._maxWidth : 42 /* whatever */, + y2: this._isHorizontal ? 42 : this._maxHeight + }); + let maxContent = themeNode.get_content_box(maxAllocation); + let availSpace; + if (this._isHorizontal) + availSpace = maxContent.get_width(); + else + availSpace = maxContent.get_height(); + + let spacing = themeNode.get_length('spacing'); + + const [{ child: firstButton }] = iconChildren; + const { child: firstIcon } = firstButton.icon; + + // if no icons there's nothing to adjust + if (!firstIcon) + return; + + // Enforce valid spacings during the size request + firstIcon.ensure_style(); + const [, , iconWidth, iconHeight] = firstIcon.get_preferred_size(); + const [, , buttonWidth, buttonHeight] = firstButton.get_preferred_size(); + + if (this._isHorizontal) { + // Subtract icon padding and box spacing from the available width + availSpace -= iconChildren.length * (buttonWidth - iconWidth) + + (iconChildren.length - 1) * spacing; + + if (this._separator) { + const [, , separatorWidth] = this._separator.get_preferred_size(); + availSpace -= separatorWidth + spacing; + } + } else { + // Subtract icon padding and box spacing from the available height + availSpace -= iconChildren.length * (buttonHeight - iconHeight) + + (iconChildren.length - 1) * spacing; + + if (this._separator) { + const [, , , separatorHeight] = this._separator.get_preferred_size(); + availSpace -= separatorHeight + spacing; + } + } + + const maxIconSize = availSpace / iconChildren.length; + let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; + let iconSizes = this._availableIconSizes.map(s => s * scaleFactor); + + let newIconSize = this._availableIconSizes[0]; + for (let i = 0; i < iconSizes.length; i++) { + if (iconSizes[i] <= maxIconSize) + newIconSize = this._availableIconSizes[i]; + } + + if (newIconSize == this.iconSize) + return; + + let oldIconSize = this.iconSize; + this.iconSize = newIconSize; + this.emit('icon-size-changed'); + + let scale = oldIconSize / newIconSize; + for (let i = 0; i < iconChildren.length; i++) { + let icon = iconChildren[i].child._delegate.icon; + + // Set the new size immediately, to keep the icons' sizes + // in sync with this.iconSize + icon.setIconSize(this.iconSize); + + // Don't animate the icon size change when the overview + // is transitioning, not visible or when initially filling + // the dash + if (!Main.overview.visible || Main.overview.animationInProgress || + !this._shownInitially) + continue; + + let [targetWidth, targetHeight] = icon.icon.get_size(); + + // Scale the icon's texture to the previous size and + // tween to the new size + icon.icon.set_size(icon.icon.width * scale, + icon.icon.height * scale); + + icon.icon.ease({ + width: targetWidth, + height: targetHeight, + duration: DASH_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } + + if (this._separator) { + const animateProperties = this._isHorizontal ? + { height: this.iconSize } : { width: this.iconSize }; + + this._separator.ease({ + ...animateProperties, + duration: DASH_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } + } + + _redisplay() { + let favorites = AppFavorites.getAppFavorites().getFavoriteMap(); + + let running = this._appSystem.get_running(); + const dockManager = Docking.DockManager.getDefault(); + const { settings } = dockManager; + + if (settings.get_boolean('isolate-workspaces') || + settings.get_boolean('isolate-monitors')) { + // When using isolation, we filter out apps that have no windows in + // the current workspace + let monitorIndex = this._monitorIndex; + running = running.filter(app => + AppIcons.getInterestingWindows(app.get_windows(), monitorIndex).length); + } + + let children = this._box.get_children().filter(actor => { + return actor.child && + actor.child._delegate && + actor.child._delegate.app; + }); + // Apps currently in the dash + let oldApps = children.map(actor => actor.child._delegate.app); + // Apps supposed to be in the dash + let newApps = []; + + const showFavorites = settings.get_boolean('show-favorites'); + if (showFavorites) { + for (let id in favorites) + newApps.push(favorites[id]); + } + + if (settings.get_boolean('show-running')) { + // We reorder the running apps so that they don't change position on the + // dash with every redisplay() call + + // First: add the apps from the oldApps list that are still running + oldApps.forEach(oldApp => { + const index = running.indexOf(oldApp); + if (index > -1) { + const [app] = running.splice(index, 1); + if (!showFavorites || !(app.get_id() in favorites)) + newApps.push(app); + } + }); + + // Second: add the new apps + running.forEach(app => { + if (!showFavorites || !(app.get_id() in favorites)) + newApps.push(app); + }); + } + + this._signalsHandler.removeWithLabel('show-mounts'); + if (dockManager.removables) { + this._signalsHandler.addWithLabel('show-mounts', + dockManager.removables, 'changed', this._queueRedisplay.bind(this)); + dockManager.removables.getApps().forEach(removable => { + if (!newApps.includes(removable)) + newApps.push(removable); + }); + } else { + oldApps = oldApps.filter(app => !app.location || app.isTrash) + } + + this._signalsHandler.removeWithLabel('show-trash'); + if (dockManager.trash) { + this._signalsHandler.addWithLabel('show-trash', + dockManager.trash, 'changed', this._queueRedisplay.bind(this)); + const trashApp = dockManager.trash.getApp(); + if (!newApps.includes(trashApp)) + newApps.push(trashApp); + } else { + oldApps = oldApps.filter(app => !app.isTrash) + } + + // Figure out the actual changes to the list of items; we iterate + // over both the list of items currently in the dash and the list + // of items expected there, and collect additions and removals. + // Moves are both an addition and a removal, where the order of + // the operations depends on whether we encounter the position + // where the item has been added first or the one from where it + // was removed. + // There is an assumption that only one item is moved at a given + // time; when moving several items at once, everything will still + // end up at the right position, but there might be additional + // additions/removals (e.g. it might remove all the launchers + // and add them back in the new order even if a smaller set of + // additions and removals is possible). + // If above assumptions turns out to be a problem, we might need + // to use a more sophisticated algorithm, e.g. Longest Common + // Subsequence as used by diff. + + let addedItems = []; + let removedActors = []; + + let newIndex = 0; + let oldIndex = 0; + while (newIndex < newApps.length || oldIndex < oldApps.length) { + let oldApp = oldApps.length > oldIndex ? oldApps[oldIndex] : null; + let newApp = newApps.length > newIndex ? newApps[newIndex] : null; + + // No change at oldIndex/newIndex + if (oldApp == newApp) { + oldIndex++; + newIndex++; + continue; + } + + // App removed at oldIndex + if (oldApp && !newApps.includes(oldApp)) { + removedActors.push(children[oldIndex]); + oldIndex++; + continue; + } + + // App added at newIndex + if (newApp && !oldApps.includes(newApp)) { + addedItems.push({ app: newApp, + item: this._createAppItem(newApp), + pos: newIndex }); + newIndex++; + continue; + } + + // App moved + let nextApp = newApps.length > newIndex + 1 + ? newApps[newIndex + 1] : null; + let insertHere = nextApp && nextApp == oldApp; + let alreadyRemoved = removedActors.reduce((result, actor) => { + let removedApp = actor.child._delegate.app; + return result || removedApp == newApp; + }, false); + + if (insertHere || alreadyRemoved) { + let newItem = this._createAppItem(newApp); + addedItems.push({ app: newApp, + item: newItem, + pos: newIndex + removedActors.length }); + newIndex++; + } else { + removedActors.push(children[oldIndex]); + oldIndex++; + } + } + + for (let i = 0; i < addedItems.length; i++) { + this._box.insert_child_at_index(addedItems[i].item, + addedItems[i].pos); + } + + for (let i = 0; i < removedActors.length; i++) { + let item = removedActors[i]; + + // Don't animate item removal when the overview is transitioning + // or hidden + if (!Main.overview.animationInProgress) + item.animateOutAndDestroy(); + else + item.destroy(); + } + + this._adjustIconSize(); + + // Skip animations on first run when adding the initial set + // of items, to avoid all items zooming in at once + + let animate = this._shownInitially && + !Main.overview.animationInProgress; + + if (!this._shownInitially) + this._shownInitially = true; + + for (let i = 0; i < addedItems.length; i++) + addedItems[i].item.show(animate); + + // Update separator + const nFavorites = Object.keys(favorites).length; + const nIcons = children.length + addedItems.length - removedActors.length; + if (nFavorites > 0 && nFavorites < nIcons) { + if (!this._separator) { + this._separator = new St.Widget({ + style_class: 'dash-separator', + x_align: this._isHorizontal ? + Clutter.ActorAlign.FILL : Clutter.ActorAlign.CENTER, + y_align: this._isHorizontal ? + Clutter.ActorAlign.CENTER : Clutter.ActorAlign.FILL, + width: this._isHorizontal ? -1 : this.iconSize, + height: this._isHorizontal ? this.iconSize : -1, + }); + this._box.add_child(this._separator); + } + let pos = nFavorites; + if (this._dragPlaceholder) + pos++; + this._box.set_child_at_index(this._separator, pos); + } else if (this._separator) { + this._separator.destroy(); + this._separator = null; + } + + // Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=692744 + // Without it, StBoxLayout may use a stale size cache + this._box.queue_relayout(); + // TODO + // This is required for icon reordering when the scrollview is used. + this._updateAppsIconGeometry(); + + // This will update the size, and the corresponding number for each icon + this._updateNumberOverlay(); + } + + _updateNumberOverlay() { + let appIcons = this.getAppIcons(); + let counter = 1; + appIcons.forEach(function(icon) { + if (counter < 10){ + icon.setNumberOverlay(counter); + counter++; + } else if (counter == 10) { + icon.setNumberOverlay(0); + counter++; + } else { + // No overlay after 10 + icon.setNumberOverlay(-1); + } + icon.updateNumberOverlay(); + }); + + } + + toggleNumberOverlay(activate) { + let appIcons = this.getAppIcons(); + appIcons.forEach(function(icon) { + icon.toggleNumberOverlay(activate); + }); + } + + _initializeIconSize(max_size) { + let max_allowed = baseIconSizes[baseIconSizes.length-1]; + max_size = Math.min(max_size, max_allowed); + + if (Docking.DockManager.settings.get_boolean('icon-size-fixed')) + this._availableIconSizes = [max_size]; + else { + this._availableIconSizes = baseIconSizes.filter(function(val) { + return (val notifiedProperties.push(pspec.name)); + + if (Docking.DockManager.settings.get_boolean('show-apps-at-top')) { + this._dashContainer.set_child_below_sibling(this._showAppsIcon, null); + } else { + this._dashContainer.set_child_above_sibling(this._showAppsIcon, null); + } + + this._signalsHandler.removeWithLabel('first-last-child-workaround'); + + // This is indeed ugly, but we need to ensure that the last and first + // visible widgets are re-computed by St, that is buggy because of a + // mutter issue that is being fixed: + // https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/2047 + if (!notifiedProperties.includes('first-child')) + this._dashContainer.notify('first-child'); + if (!notifiedProperties.includes('last-child')) + this._dashContainer.notify('last-child'); + } +}); + + +/** + * This is a copy of the same function in utils.js, but also adjust horizontal scrolling + * and perform few further checks on the current value to avoid changing the values when + * it would be clamp to the current one in any case. + * Return the amount of shift applied + */ +function ensureActorVisibleInScrollView(scrollView, actor) { + const { adjustment: vAdjustment } = scrollView.vscroll; + const { adjustment: hAdjustment } = scrollView.hscroll; + const { value: vValue0, pageSize: vPageSize, upper: vUpper } = vAdjustment; + const { value: hValue0, pageSize: hPageSize, upper: hUpper } = hAdjustment; + let [hValue, vValue] = [hValue0, vValue0]; + let vOffset = 0; + let hOffset = 0; + let fade = scrollView.get_effect('fade'); + if (fade) { + vOffset = fade.fade_margins.top; + hOffset = fade.fade_margins.left; + } + + let box = actor.get_allocation_box(); + let y1 = box.y1, y2 = box.y2, x1 = box.x1, x2 = box.x2; + + let parent = actor.get_parent(); + while (parent != scrollView) { + if (!parent) + throw new Error('Actor not in scroll view'); + + let box = parent.get_allocation_box(); + y1 += box.y1; + y2 += box.y1; + x1 += box.x1; + x2 += box.x1; + parent = parent.get_parent(); + } + + if (y1 < vValue + vOffset) + vValue = Math.max(0, y1 - vOffset); + else if (vValue < vUpper - vPageSize && y2 > vValue + vPageSize - vOffset) + vValue = Math.min(vUpper -vPageSize, y2 + vOffset - vPageSize); + + if (x1 < hValue + hOffset) + hValue = Math.max(0, x1 - hOffset); + else if (hValue < hUpper - hPageSize && x2 > hValue + hPageSize - hOffset) + hValue = Math.min(hUpper - hPageSize, x2 + hOffset - hPageSize); + + if (vValue !== vValue0) { + vAdjustment.ease(vValue, { + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + duration: Util.SCROLL_TIME + }); + } + + if (hValue !== hValue0) { + hAdjustment.ease(hValue, { + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + duration: Util.SCROLL_TIME + }); + } + + return [hValue - hValue0, vValue - vValue0]; +} diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/dbusmenuUtils.js b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/dbusmenuUtils.js new file mode 100644 index 0000000..3af8dc6 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/dbusmenuUtils.js @@ -0,0 +1,274 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Atk = imports.gi.Atk; +const Clutter = imports.gi.Clutter; +let Dbusmenu = null; /* Dynamically imported */ +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const St = imports.gi.St; + +const PopupMenu = imports.ui.popupMenu; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Utils = Me.imports.utils; + +// Dbusmenu features not (yet) supported: +// +// * The CHILD_DISPLAY property +// +// This seems to have only one possible value in the Dbusmenu API, so +// there's little point in depending on it--the code in libdbusmenu sets it +// if and only if an item has children, so for our purposes it's simpler +// and more intuitive to just check children.length. (This does ignore the +// possibility of a program not using libdbusmenu and setting CHILD_DISPLAY +// independently, perhaps to indicate that an childless menu item should +// nevertheless be displayed like a submenu.) +// +// * Children more than two levels deep +// +// PopupMenu doesn't seem to support submenus in submenus. +// +// * Shortcut keys +// +// If these keys are supposed to be installed as global shortcuts, we'd +// have to query these aggressively and not wait for the DBus menu to be +// mapped to a popup menu. A shortcut key that only works once the popup +// menu is open and has key focus is possibly of marginal value. + +function haveDBusMenu() { + if (Dbusmenu) + return Dbusmenu; + + try { + Dbusmenu = imports.gi.Dbusmenu; + return Dbusmenu; + } catch (e) { + log(`Failed to import DBusMenu, quicklists are not avaialble: ${e}`); + return null; + } +} + + +function makePopupMenuItem(dbusmenuItem, deep) { + // These are the only properties guaranteed to be available when the root + // item is first announced. Other properties might be loaded already, but + // be sure to connect to Dbusmenu.MENUITEM_SIGNAL_PROPERTY_CHANGED to get + // the most up-to-date values in case they aren't. + const itemType = dbusmenuItem.property_get(Dbusmenu.MENUITEM_PROP_TYPE); + const label = dbusmenuItem.property_get(Dbusmenu.MENUITEM_PROP_LABEL); + const visible = dbusmenuItem.property_get_bool(Dbusmenu.MENUITEM_PROP_VISIBLE); + const enabled = dbusmenuItem.property_get_bool(Dbusmenu.MENUITEM_PROP_ENABLED); + const accessibleDesc = dbusmenuItem.property_get(Dbusmenu.MENUITEM_PROP_ACCESSIBLE_DESC); + //const childDisplay = dbusmenuItem.property_get(Dbusmenu.MENUITEM_PROP_CHILD_DISPLAY); + + let item; + const signalsHandler = new Utils.GlobalSignalsHandler(); + const wantIcon = itemType === Dbusmenu.CLIENT_TYPES_IMAGE; + + // If the basic type of the menu item needs to change, call this. + const recreateItem = () => { + const newItem = makePopupMenuItem(dbusmenuItem, deep); + const parentMenu = item._parent; + parentMenu.addMenuItem(newItem); + // Reminder: Clutter thinks of later entries in the child list as + // "above" earlier ones, so "above" here means "below" in terms of the + // menu's vertical order. + parentMenu.actor.set_child_above_sibling(newItem.actor, item.actor); + if (newItem.menu) { + parentMenu.actor.set_child_above_sibling(newItem.menu.actor, newItem.actor); + } + parentMenu.actor.remove_child(item.actor); + item.destroy(); + item = null; + }; + + const updateDisposition = () => { + const disposition = dbusmenuItem.property_get(Dbusmenu.MENUITEM_PROP_DISPOSITION); + let icon_name = null; + switch (disposition) { + case Dbusmenu.MENUITEM_DISPOSITION_ALERT: + case Dbusmenu.MENUITEM_DISPOSITION_WARNING: + icon_name = 'dialog-warning-symbolic'; + break; + case Dbusmenu.MENUITEM_DISPOSITION_INFORMATIVE: + icon_name = 'dialog-information-symbolic'; + break; + } + if (icon_name) { + item._dispositionIcon = new St.Icon({ + icon_name, + style_class: 'popup-menu-icon', + y_align: Clutter.ActorAlign.CENTER, + y_expand: true, + }); + let expander; + for (let child = item.label.get_next_sibling();; child = child.get_next_sibling()) { + if (!child) { + expander = new St.Bin({ + style_class: 'popup-menu-item-expander', + x_expand: true, + }); + item.actor.add_child(expander); + break; + } else if (child instanceof St.Widget && child.has_style_class_name('popup-menu-item-expander')) { + expander = child; + break; + } + } + item.actor.insert_child_above(item._dispositionIcon, expander); + } else if (item._dispositionIcon) { + item.actor.remove_child(item._dispositionIcon); + item._dispositionIcon = null; + } + }; + + const updateIcon = () => { + if (!wantIcon) { + return; + } + const iconData = dbusmenuItem.property_get_byte_array(Dbusmenu.MENUITEM_PROP_ICON_DATA); + const iconName = dbusmenuItem.property_get(Dbusmenu.MENUITEM_PROP_ICON_NAME); + if (iconName) { + item.icon.icon_name = iconName; + } else if (iconData.length) { + item.icon.gicon = Gio.BytesIcon.new(iconData); + } + }; + + const updateOrnament = () => { + const toggleType = dbusmenuItem.property_get(Dbusmenu.MENUITEM_PROP_TOGGLE_TYPE); + switch (toggleType) { + case Dbusmenu.MENUITEM_TOGGLE_CHECK: + item.actor.accessible_role = Atk.Role.CHECK_MENU_ITEM; + break; + case Dbusmenu.MENUITEM_TOGGLE_RADIO: + item.actor.accessible_role = Atk.Role.RADIO_MENU_ITEM; + break; + default: + item.actor.accessible_role = Atk.Role.MENU_ITEM; + } + let ornament = PopupMenu.Ornament.NONE; + const state = dbusmenuItem.property_get_int(Dbusmenu.MENUITEM_PROP_TOGGLE_STATE); + if (state === Dbusmenu.MENUITEM_TOGGLE_STATE_UNKNOWN) { + // PopupMenu doesn't natively support an "unknown" ornament, but we + // can hack one in: + item.setOrnament(ornament); + item.actor.add_accessible_state(Atk.StateType.INDETERMINATE); + item._ornamentLabel.text = '\u2501'; + item.actor.remove_style_pseudo_class('checked'); + } else { + item.actor.remove_accessible_state(Atk.StateType.INDETERMINATE); + if (state === Dbusmenu.MENUITEM_TOGGLE_STATE_CHECKED) { + if (toggleType === Dbusmenu.MENUITEM_TOGGLE_CHECK) { + ornament = PopupMenu.Ornament.CHECK; + } else if (toggleType === Dbusmenu.MENUITEM_TOGGLE_RADIO) { + ornament = PopupMenu.Ornament.DOT; + } + item.actor.add_style_pseudo_class('checked'); + } else { + item.actor.remove_style_pseudo_class('checked'); + } + item.setOrnament(ornament); + } + }; + + const onPropertyChanged = (dbusmenuItem, name, value) => { + // `value` is null when a property is cleared, so handle those cases + // with sensible defaults. + switch (name) { + case Dbusmenu.MENUITEM_PROP_TYPE: + recreateItem(); + break; + case Dbusmenu.MENUITEM_PROP_ENABLED: + item.setSensitive(value ? value.unpack() : false); + break; + case Dbusmenu.MENUITEM_PROP_LABEL: + item.label.text = value ? value.unpack() : ''; + break; + case Dbusmenu.MENUITEM_PROP_VISIBLE: + item.actor.visible = value ? value.unpack() : false; + break; + case Dbusmenu.MENUITEM_PROP_DISPOSITION: + updateDisposition(); + break; + case Dbusmenu.MENUITEM_PROP_ACCESSIBLE_DESC: + item.actor.get_accessible().accessible_description = value && value.unpack() || ''; + break; + case Dbusmenu.MENUITEM_PROP_ICON_DATA: + case Dbusmenu.MENUITEM_PROP_ICON_NAME: + updateIcon(); + break; + case Dbusmenu.MENUITEM_PROP_TOGGLE_TYPE: + case Dbusmenu.MENUITEM_PROP_TOGGLE_STATE: + updateOrnament(); + break; + } + }; + + + // Start actually building the menu item. + const children = dbusmenuItem.get_children(); + if (children.length && !deep) { + // Make a submenu. + item = new PopupMenu.PopupSubMenuMenuItem(label, wantIcon); + const updateChildren = () => { + const children = dbusmenuItem.get_children(); + if (!children.length) { + return recreateItem(); + } + item.menu.removeAll(); + children.forEach(remoteChild => + item.menu.addMenuItem(makePopupMenuItem(remoteChild, true))); + }; + updateChildren(); + signalsHandler.add( + [dbusmenuItem, Dbusmenu.MENUITEM_SIGNAL_CHILD_ADDED, updateChildren], + [dbusmenuItem, Dbusmenu.MENUITEM_SIGNAL_CHILD_MOVED, updateChildren], + [dbusmenuItem, Dbusmenu.MENUITEM_SIGNAL_CHILD_REMOVED, updateChildren]); + + } else { + // Don't make a submenu. + if (!deep) { + // We only have the potential to get a submenu if we aren't deep. + signalsHandler.add( + [dbusmenuItem, Dbusmenu.MENUITEM_SIGNAL_CHILD_ADDED, recreateItem], + [dbusmenuItem, Dbusmenu.MENUITEM_SIGNAL_CHILD_MOVED, recreateItem], + [dbusmenuItem, Dbusmenu.MENUITEM_SIGNAL_CHILD_REMOVED, recreateItem]); + } + + if (itemType === Dbusmenu.CLIENT_TYPES_SEPARATOR) { + item = new PopupMenu.PopupSeparatorMenuItem(); + } else if (wantIcon) { + item = new PopupMenu.PopupImageMenuItem(label, null); + item.icon = item._icon; + } else { + item = new PopupMenu.PopupMenuItem(label); + } + } + + // Set common initial properties. + item.actor.visible = visible; + item.setSensitive(enabled); + if (accessibleDesc) { + item.actor.get_accessible().accessible_description = accessibleDesc; + } + updateDisposition(); + updateIcon(); + updateOrnament(); + + // Prevent an initial resize flicker. + if (wantIcon) { + item.icon.icon_size = 16; + } + + signalsHandler.add(dbusmenuItem, Dbusmenu.MENUITEM_SIGNAL_PROPERTY_CHANGED, onPropertyChanged); + + // Connections on item will be lost when item is disposed; there's no need + // to add them to signalsHandler. + item.connect('activate', () => { + dbusmenuItem.handle_event(Dbusmenu.MENUITEM_EVENT_ACTIVATED, new GLib.Variant('i', 0), Math.floor(Date.now()/1000)); + }); + item.connect('destroy', () => signalsHandler.destroy()); + + return item; +} diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/docking.js b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/docking.js new file mode 100644 index 0000000..5ba0c3b --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/docking.js @@ -0,0 +1,2340 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Clutter = imports.gi.Clutter; +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; +const GObject = imports.gi.GObject; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; +const St = imports.gi.St; +const Params = imports.misc.params; + +const Main = imports.ui.main; +const AppDisplay = imports.ui.appDisplay; +const Dash = imports.ui.dash; +const IconGrid = imports.ui.iconGrid; +const Overview = imports.ui.overview; +const OverviewControls = imports.ui.overviewControls; +const PointerWatcher = imports.ui.pointerWatcher; +const Signals = imports.signals; +const SearchController = imports.ui.searchController; +const WorkspaceSwitcherPopup= imports.ui.workspaceSwitcherPopup; +const Layout = imports.ui.layout; +const LayoutManager = imports.ui.main.layoutManager; +const Workspace = imports.ui.workspace; +const WorkspacesView = imports.ui.workspacesView; +const WorkspaceThumbnail = imports.ui.workspaceThumbnail; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Utils = Me.imports.utils; +const Intellihide = Me.imports.intellihide; +const Theming = Me.imports.theming; +const DockDash = Me.imports.dash; +const Locations = Me.imports.locations; +const LauncherAPI = Me.imports.launcherAPI; +const FileManager1API = Me.imports.fileManager1API; + +const DOCK_DWELL_CHECK_INTERVAL = 100; + +var State = { + HIDDEN: 0, + SHOWING: 1, + SHOWN: 2, + HIDING: 3 +}; + +const scrollAction = { + DO_NOTHING: 0, + CYCLE_WINDOWS: 1, + SWITCH_WORKSPACE: 2 +}; + +/** + * A simple St.Widget with one child whose allocation takes into account the + * slide out of its child via the slide-x property ([0:1]). + * + * Required since I want to track the input region of this container which is + * based on its allocation even if the child overlows the parent actor. By doing + * this the region of the dash that is slideout is not steling anymore the input + * regions making the extesion usable when the primary monitor is the right one. + * + * The slide-x parameter can be used to directly animate the sliding. The parent + * must have a WEST (SOUTH) anchor_point to achieve the sliding to the RIGHT (BOTTOM) + * side. +*/ +var DashSlideContainer = GObject.registerClass({ + Properties: { + 'monitor-index': GObject.ParamSpec.uint( + 'monitor-index', 'monitor-index', 'monitor-index', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, + 0, GLib.MAXUINT32, 0), + 'side': GObject.ParamSpec.enum( + 'side', 'side', 'side', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, + St.Side, St.Side.LEFT), + 'slide-x': GObject.ParamSpec.double( + 'slide-x', 'slide-x', 'slide-x', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT, + 0, 1, 1), + } +}, class DashSlideContainer extends St.Bin { + + _init(params = {}) { + super._init(params); + + this._slideoutSize = 0; // minimum size when slided out + this.connect('notify::slide-x', () => this.queue_relayout()); + + if (this.side == St.Side.TOP && DockManager.settings.dockFixed) { + this._signalsHandler = new Utils.GlobalSignalsHandler(this); + this._signalsHandler.add(Main.panel, 'notify::height', + () => this.queue_relayout()); + } + } + + vfunc_allocate(box) { + let contentBox = this.get_theme_node().get_content_box(box); + + this.set_allocation(box); + + if (this.child == null) + return; + + let availWidth = contentBox.x2 - contentBox.x1; + let availHeight = contentBox.y2 - contentBox.y1; + let [, , natChildWidth, natChildHeight] = + this.child.get_preferred_size(); + + let childWidth = natChildWidth; + let childHeight = natChildHeight; + + let childBox = new Clutter.ActorBox(); + + let slideoutSize = this._slideoutSize; + + if (this.side == St.Side.LEFT) { + childBox.x1 = (this.slideX -1) * (childWidth - slideoutSize); + childBox.x2 = slideoutSize + this.slideX * (childWidth - slideoutSize); + childBox.y1 = 0; + childBox.y2 = childBox.y1 + childHeight; + } + else if ((this.side == St.Side.RIGHT) || (this.side == St.Side.BOTTOM)) { + childBox.x1 = 0; + childBox.x2 = childWidth; + childBox.y1 = 0; + childBox.y2 = childBox.y1 + childHeight; + } + else if (this.side == St.Side.TOP) { + const monitor = Main.layoutManager.monitors[this.monitorIndex]; + let yOffset = 0; + if (Main.panel.x === monitor.x && Main.panel.y === monitor.y && + DockManager.settings.dockFixed) + yOffset = Main.panel.height; + childBox.x1 = 0; + childBox.x2 = childWidth; + childBox.y1 = (this.slideX - 1) * (childHeight - slideoutSize) + yOffset; + childBox.y2 = slideoutSize + this.slideX * (childHeight - slideoutSize) + yOffset; + availHeight += yOffset; + } + + this.child.allocate(childBox); + + this.child.set_clip(-childBox.x1, -childBox.y1, + -childBox.x1+availWidth, -childBox.y1 + availHeight); + } + + /** + * Just the child width but taking into account the slided out part + */ + vfunc_get_preferred_width(forHeight) { + let [minWidth, natWidth] = super.vfunc_get_preferred_width(forHeight); + if ((this.side == St.Side.LEFT) || (this.side == St.Side.RIGHT)) { + minWidth = (minWidth - this._slideoutSize) * this.slideX + this._slideoutSize; + natWidth = (natWidth - this._slideoutSize) * this.slideX + this._slideoutSize; + } + return [minWidth, natWidth]; + } + + /** + * Just the child height but taking into account the slided out part + */ + vfunc_get_preferred_height(forWidth) { + let [minHeight, natHeight] = super.vfunc_get_preferred_height(forWidth); + if ((this.side == St.Side.TOP) || (this.side == St.Side.BOTTOM)) { + minHeight = (minHeight - this._slideoutSize) * this.slideX + this._slideoutSize; + natHeight = (natHeight - this._slideoutSize) * this.slideX + this._slideoutSize; + + if (this.side == St.Side.TOP && DockManager.settings.dockFixed) { + const monitor = Main.layoutManager.monitors[this.monitorIndex]; + if (Main.panel.x === monitor.x && Main.panel.y === monitor.y) { + minHeight += Main.panel.height; + natHeight += Main.panel.height; + } + } + } + return [minHeight, natHeight]; + } +}); + +var DockedDash = GObject.registerClass({ + Signals: { + 'showing': {}, + 'hiding': {}, + } +}, class DashToDock extends St.Bin { + + _init(monitorIndex) { + this._rtl = (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL); + + // Load settings + let settings = DockManager.settings; + this._monitorIndex = monitorIndex; + this._position = Utils.getPosition(); + this._isHorizontal = ((this._position == St.Side.TOP) || (this._position == St.Side.BOTTOM)); + + // Temporary ignore hover events linked to autohide for whatever reason + this._ignoreHover = false; + this._oldignoreHover = null; + // This variables are linked to the settings regardles of autohide or intellihide + // being temporary disable. Get set by _updateVisibilityMode; + this._autohideIsEnabled = null; + this._intellihideIsEnabled = null; + + // Create intellihide object to monitor windows overlapping + this._intellihide = new Intellihide.Intellihide(this._monitorIndex); + + // initialize dock state + this._dockState = State.HIDDEN; + + // Put dock on the required monitor + this._monitor = Main.layoutManager.monitors[this._monitorIndex]; + + // this store size and the position where the dash is shown; + // used by intellihide module to check window overlap. + this.staticBox = new Clutter.ActorBox(); + + // Initialize pressure barrier variables + this._canUsePressure = false; + this._pressureBarrier = null; + this._barrier = null; + this._removeBarrierTimeoutId = 0; + + // Initialize dwelling system variables + this._dockDwelling = false; + this._dockWatch = null; + this._dockDwellUserTime = 0; + this._dockDwellTimeoutId = 0 + + // Create a new dash object + this.dash = new DockDash.DockDash(this._monitorIndex); + + if (Main.overview.isDummy || !settings.get_boolean('show-show-apps-button')) + this.dash.hideShowAppsButton(); + + // Create the main actor and the containers for sliding in and out and + // centering, turn on track hover + + let positionStyleClass = ['top', 'right', 'bottom', 'left']; + // This is the centering actor + super._init({ + name: 'dashtodockContainer', + reactive: false, + style_class: positionStyleClass[this._position], + }); + + // This is the sliding actor whose allocation is to be tracked for input regions + this._slider = new DashSlideContainer({ + monitor_index: this._monitor.index, + side: this._position, + slide_x: 0, + ...(this._isHorizontal ? { + x_align: Clutter.ActorAlign.CENTER, + } : { + y_align: Clutter.ActorAlign.CENTER, + }) + }); + + // This is the actor whose hover status us tracked for autohide + this._box = new St.BoxLayout({ + name: 'dashtodockBox', + reactive: true, + track_hover: true + }); + this._box.connect('notify::hover', this._hoverChanged.bind(this)); + + // Connect global signals + this._signalsHandler = new Utils.GlobalSignalsHandler(this); + this._bindSettingsChanges(); + this._signalsHandler.add([ + // update when workarea changes, for instance if other extensions modify the struts + //(like moving th panel at the bottom) + global.display, + 'workareas-changed', + this._resetPosition.bind(this) + ], [ + global.display, + 'in-fullscreen-changed', + this._updateBarrier.bind(this) + ], [ + // Monitor windows overlapping + this._intellihide, + 'status-changed', + this._updateDashVisibility.bind(this) + ], [ + // sync hover after a popupmenu is closed + this.dash, + 'menu-closed', + () => { this._box.sync_hover() } + ], [ + this.dash, + 'notify::requires-visibility', + () => this._updateDashVisibility(), + ]); + + if (!Main.overview.isDummy) { + this._signalsHandler.add([ + Main.overview, + 'item-drag-begin', + this._onDragStart.bind(this) + ], [ + Main.overview, + 'item-drag-end', + this._onDragEnd.bind(this) + ], [ + Main.overview, + 'item-drag-cancelled', + this._onDragEnd.bind(this) + ], [ + Main.overview, + 'showing', + this._onOverviewShowing.bind(this) + ], [ + Main.overview, + 'hiding', + this._onOverviewHiding.bind(this) + ], + [ + Main.overview, + 'hidden', + this._onOverviewHidden.bind(this) + ]); + } + + this._themeManager = new Theming.ThemeManager(this); + this._signalsHandler.add(this._themeManager, 'updated', + () => this.dash.resetAppIcons()); + + this._signalsHandler.add(DockManager.iconTheme, 'changed', + () => this.dash.resetAppIcons()); + + // Since the actor is not a topLevel child and its parent is now not added to the Chrome, + // the allocation change of the parent container (slide in and slideout) doesn't trigger + // anymore an update of the input regions. Force the update manually. + this.connect('notify::allocation', + Main.layoutManager._queueUpdateRegions.bind(Main.layoutManager)); + + + // Since Clutter has no longer ClutterAllocationFlags, + // "allocation-changed" signal has been removed. MR !1245 + this.dash._container.connect('notify::allocation', this._updateStaticBox.bind(this)); + this._slider.connect(this._isHorizontal ? 'notify::x' : 'notify::y', this._updateStaticBox.bind(this)); + + // Load optional features that need to be activated for one dock only + if (this._monitorIndex == settings.get_int('preferred-monitor')) + this._enableExtraFeatures(); + // Load optional features that need to be activated once per dock + this._optionalScrollWorkspaceSwitch(); + + // Delay operations that require the shell to be fully loaded and with + // user theme applied. + + this._signalsHandler.addWithLabel('initialize', global.stage, + 'after-paint', () => this._initialize()); + + // Add dash container actor and the container to the Chrome. + this.set_child(this._slider); + this._slider.set_child(this._box); + this._box.add_actor(this.dash); + + // Add aligning container without tracking it for input region + Main.uiGroup.add_child(this); + if (Main.uiGroup.contains(global.top_window_group)) + Main.uiGroup.set_child_below_sibling(this, global.top_window_group); + + if (settings.get_boolean('dock-fixed')) { + // Note: tracking the fullscreen directly on the slider actor causes some hiccups when fullscreening + // windows of certain applications + Main.layoutManager._trackActor(this, {affectsInputRegion: false, trackFullscreen: true}); + Main.layoutManager._trackActor(this._slider, {affectsStruts: true}); + } + else + Main.layoutManager._trackActor(this._slider); + + // Create and apply height/width constraint to the dash. + if (this._isHorizontal) { + this.connect('notify::width', () => { + this.dash.setMaxSize(this.width, this.height); + }); + } else { + this.connect('notify::height', () => { + this.dash.setMaxSize(this.width, this.height) + }); + } + + if (this._position == St.Side.RIGHT) + this.connect('notify::width', () => this.translation_x = -this.width); + else if (this._position == St.Side.BOTTOM) + this.connect('notify::height', () => this.translation_y = -this.height); + + // Set initial position + this._resetPosition(); + + this.connect('destroy', this._onDestroy.bind(this)); + } + + get monitorIndex() { + return this._monitorIndex; + } + + get position() { + return this._position; + } + + get isHorizontal() { + return this._isHorizontal; + } + + _untrackDock() { + Main.layoutManager._untrackActor(this); + Main.layoutManager._untrackActor(this._slider); + } + + _trackDock() { + if (DockManager.settings.dockFixed) { + if (Main.layoutManager._findActor(this) == -1) + Main.layoutManager._trackActor(this, { affectsInputRegion: false, trackFullscreen: true }); + if (Main.layoutManager._findActor(this._slider) == -1) + Main.layoutManager._trackActor(this._slider, { affectsStruts: true }); + } else { + if (Main.layoutManager._findActor(this._slider) == -1) + Main.layoutManager._trackActor(this._slider); + } + } + + _initialize() { + this._signalsHandler.removeWithLabel('initialize'); + + // Apply custome css class according to the settings + this._themeManager.updateCustomTheme(); + + this._updateVisibilityMode(); + + // In case we are already inside the overview when the extension is loaded, + // for instance on unlocking the screen if it was locked with the overview open. + if (Main.overview.visibleTarget) { + this._onOverviewShowing(); + } + + this._updateAutoHideBarriers(); + } + + _onDestroy() { + // The dash, intellihide and themeManager have global signals as well internally + this.dash.destroy(); + this._intellihide.destroy(); + this._themeManager.destroy(); + + if (this._marginLater) { + Meta.later_remove(this._marginLater); + delete this._marginLater; + } + + // Remove barrier timeout + if (this._removeBarrierTimeoutId > 0) + GLib.source_remove(this._removeBarrierTimeoutId); + + // Remove existing barrier + this._removeBarrier(); + + // Remove pointer watcher + if (this._dockWatch) { + PointerWatcher.getPointerWatcher()._removeWatch(this._dockWatch); + this._dockWatch = null; + } + } + + _updateAutoHideBarriers() { + // Remove pointer watcher + if (this._dockWatch) { + PointerWatcher.getPointerWatcher()._removeWatch(this._dockWatch); + this._dockWatch = null; + } + + // Setup pressure barrier (GS38+ only) + this._updatePressureBarrier(); + this._updateBarrier(); + + // setup dwelling system if pressure barriers are not available + this._setupDockDwellIfNeeded(); + } + + _bindSettingsChanges() { + let settings = DockManager.settings; + this._signalsHandler.add([ + settings, + 'changed::scroll-action', + () => { this._optionalScrollWorkspaceSwitch(); } + ], [ + settings, + 'changed::dash-max-icon-size', + () => { this.dash.setIconSize(settings.get_int('dash-max-icon-size')); } + ], [ + settings, + 'changed::icon-size-fixed', + () => { this.dash.setIconSize(settings.get_int('dash-max-icon-size')); } + ], [ + settings, + 'changed::show-favorites', + () => { this.dash.resetAppIcons(); } + ], [ + settings, + 'changed::show-trash', + () => { this.dash.resetAppIcons(); }, + Utils.SignalsHandlerFlags.CONNECT_AFTER, + ], [ + settings, + 'changed::show-mounts', + () => { this.dash.resetAppIcons(); }, + Utils.SignalsHandlerFlags.CONNECT_AFTER + ], [ + settings, + 'changed::isolate-locations', + () => this.dash.resetAppIcons(), + Utils.SignalsHandlerFlags.CONNECT_AFTER + ], [ + settings, + 'changed::show-running', + () => { this.dash.resetAppIcons(); } + ], [ + settings, + 'changed::show-apps-at-top', + () => { this.dash.updateShowAppsButton(); } + ], [ + settings, + 'changed::show-show-apps-button', + () => { + if (!Main.overview.isDummy && + settings.get_boolean('show-show-apps-button')) + this.dash.showShowAppsButton(); + else + this.dash.hideShowAppsButton(); + } + ], [ + settings, + 'changed::dock-fixed', + () => { + this._untrackDock(); + this._trackDock(); + + this._resetPosition(); + this._updateAutoHideBarriers(); + this._updateVisibilityMode(); + } + ], [ + settings, + 'changed::intellihide', + this._updateVisibilityMode.bind(this) + ], [ + settings, + 'changed::intellihide-mode', + () => { this._intellihide.forceUpdate(); } + ], [ + settings, + 'changed::autohide', + () => { + this._updateVisibilityMode(); + this._updateAutoHideBarriers(); + } + ], [ + settings, + 'changed::autohide-in-fullscreen', + this._updateBarrier.bind(this) + ], + [ + settings, + 'changed::extend-height', + this._resetPosition.bind(this) + ], [ + settings, + 'changed::height-fraction', + this._resetPosition.bind(this) + ], [ + settings, + 'changed::require-pressure-to-show', + () => this._updateAutoHideBarriers(), + ], [ + settings, + 'changed::pressure-threshold', + () => { + this._updatePressureBarrier(); + this._updateBarrier(); + } + ]); + + } + + /** + * This is call when visibility settings change + */ + _updateVisibilityMode() { + let settings = DockManager.settings; + if (DockManager.settings.dockFixed) { + this._autohideIsEnabled = false; + this._intellihideIsEnabled = false; + } + else { + this._autohideIsEnabled = settings.get_boolean('autohide') + this._intellihideIsEnabled = settings.get_boolean('intellihide') + } + + if (this._autohideIsEnabled) + this.add_style_class_name('autohide'); + else + this.remove_style_class_name('autohide'); + + if (this._intellihideIsEnabled) + this._intellihide.enable(); + else + this._intellihide.disable(); + + this._updateDashVisibility(); + } + + /** + * Show/hide dash based on, in order of priority: + * overview visibility + * fixed mode + * intellihide + * autohide + * overview visibility + */ + _updateDashVisibility() { + if (Main.overview.visibleTarget) + return; + + let settings = DockManager.settings; + + if (DockManager.settings.dockFixed) { + this._removeAnimations(); + this._animateIn(settings.get_double('animation-time'), 0); + } + else if (this._intellihideIsEnabled) { + if (!this.dash.requiresVisibility && this._intellihide.getOverlapStatus()) { + this._ignoreHover = false; + // Do not hide if autohide is enabled and mouse is hover + if (!this._box.hover || !this._autohideIsEnabled) + this._animateOut(settings.get_double('animation-time'), 0); + } + else { + this._ignoreHover = true; + this._removeAnimations(); + this._animateIn(settings.get_double('animation-time'), 0); + } + } + else { + if (this._autohideIsEnabled) { + this._ignoreHover = false; + + if (this._box.hover || this.dash.requiresVisibility) + this._animateIn(settings.get_double('animation-time'), 0); + else + this._animateOut(settings.get_double('animation-time'), 0); + } + else + this._animateOut(settings.get_double('animation-time'), 0); + } + } + + _onOverviewShowing() { + this.add_style_class_name('overview'); + + this._ignoreHover = true; + this._intellihide.disable(); + this._removeAnimations(); + this._animateIn(DockManager.settings.get_double('animation-time'), 0); + } + + _onOverviewHiding() { + this._ignoreHover = false; + this._intellihide.enable(); + this._updateDashVisibility(); + } + + _onOverviewHidden() { + this.remove_style_class_name('overview'); + } + + _hoverChanged() { + if (!this._ignoreHover) { + // Skip if dock is not in autohide mode for instance because it is shown + // by intellihide. + if (this._autohideIsEnabled) { + if (this._box.hover) + this._show(); + else + this._hide(); + } + } + } + + getDockState() { + return this._dockState; + } + + _show() { + this._delayedHide = false; + if ((this._dockState == State.HIDDEN) || (this._dockState == State.HIDING)) { + if (this._dockState == State.HIDING) + // suppress all potential queued transitions - i.e. added but not started, + // always give priority to show + this._removeAnimations(); + + this.emit('showing'); + this._animateIn(DockManager.settings.get_double('animation-time'), 0); + } + } + + _hide() { + // If no hiding animation is running or queued + if ((this._dockState == State.SHOWN) || (this._dockState == State.SHOWING)) { + let settings = DockManager.settings; + let delay = settings.get_double('hide-delay'); + + if (this._dockState == State.SHOWING) { + // if a show already started, let it finish; queue hide without removing the show. + // to obtain this, we wait for the animateIn animation to be completed + this._delayedHide = true; + return; + } + + this.emit('hiding'); + this._animateOut(settings.get_double('animation-time'), delay); + } + } + + _animateIn(time, delay) { + this._dockState = State.SHOWING; + this.dash.iconAnimator.start(); + this._delayedHide = false; + + this._slider.ease_property('slide-x', 1, { + duration: time * 1000, + delay: delay * 1000, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + this._dockState = State.SHOWN; + // Remove barrier so that mouse pointer is released and can access monitors on other side of dock + // NOTE: Delay needed to keep mouse from moving past dock and re-hiding dock immediately. This + // gives users an opportunity to hover over the dock + if (this._removeBarrierTimeoutId > 0) + GLib.source_remove(this._removeBarrierTimeoutId); + + if (!this._delayedHide) { + this._removeBarrierTimeoutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, 100, this._removeBarrier.bind(this)); + } else { + this._hide(); + } + } + }); + } + + _animateOut(time, delay) { + this._dockState = State.HIDING; + + this._slider.ease_property('slide-x', 0, { + duration: time * 1000, + delay: delay * 1000, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + this._dockState = State.HIDDEN; + // Remove queued barried removal if any + if (this._removeBarrierTimeoutId > 0) + GLib.source_remove(this._removeBarrierTimeoutId); + this._updateBarrier(); + this.dash.iconAnimator.pause(); + } + }); + } + + /** + * Dwelling system based on the GNOME Shell 3.14 messageTray code. + */ + _setupDockDwellIfNeeded() { + // If we don't have extended barrier features, then we need + // to support the old tray dwelling mechanism. + if (this._autohideIsEnabled && + (!global.display.supports_extended_barriers() || + !DockManager.settings.get_boolean('require-pressure-to-show'))) { + let pointerWatcher = PointerWatcher.getPointerWatcher(); + this._dockWatch = pointerWatcher.addWatch(DOCK_DWELL_CHECK_INTERVAL, this._checkDockDwell.bind(this)); + this._dockDwelling = false; + this._dockDwellUserTime = 0; + } + } + + _checkDockDwell(x, y) { + + let workArea = Main.layoutManager.getWorkAreaForMonitor(this._monitor.index) + let shouldDwell; + // Check for the correct screen edge, extending the sensitive area to the whole workarea, + // minus 1 px to avoid conflicting with other active corners. + if (this._position == St.Side.LEFT) + shouldDwell = (x == this._monitor.x) && (y > workArea.y) && (y < workArea.y + workArea.height); + else if (this._position == St.Side.RIGHT) + shouldDwell = (x == this._monitor.x + this._monitor.width - 1) && (y > workArea.y) && (y < workArea.y + workArea.height); + else if (this._position == St.Side.TOP) + shouldDwell = (y == this._monitor.y) && (x > workArea.x) && (x < workArea.x + workArea.width); + else if (this._position == St.Side.BOTTOM) + shouldDwell = (y == this._monitor.y + this._monitor.height - 1) && (x > workArea.x) && (x < workArea.x + workArea.width); + + if (shouldDwell) { + // We only set up dwell timeout when the user is not hovering over the dock + // already (!this._box.hover). + // The _dockDwelling variable is used so that we only try to + // fire off one dock dwell - if it fails (because, say, the user has the mouse down), + // we don't try again until the user moves the mouse up and down again. + if (!this._dockDwelling && !this._box.hover && (this._dockDwellTimeoutId == 0)) { + // Save the interaction timestamp so we can detect user input + let focusWindow = global.display.focus_window; + this._dockDwellUserTime = focusWindow ? focusWindow.user_time : 0; + + this._dockDwellTimeoutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + DockManager.settings.get_double('show-delay') * 1000, + this._dockDwellTimeout.bind(this)); + GLib.Source.set_name_by_id(this._dockDwellTimeoutId, '[dash-to-dock] this._dockDwellTimeout'); + } + this._dockDwelling = true; + } + else { + this._cancelDockDwell(); + this._dockDwelling = false; + } + } + + _cancelDockDwell() { + if (this._dockDwellTimeoutId != 0) { + GLib.source_remove(this._dockDwellTimeoutId); + this._dockDwellTimeoutId = 0; + } + } + + _dockDwellTimeout() { + this._dockDwellTimeoutId = 0; + + if (!DockManager.settings.get_boolean('autohide-in-fullscreen') && + this._monitor.inFullscreen) + return GLib.SOURCE_REMOVE; + + // We don't want to open the tray when a modal dialog + // is up, so we check the modal count for that. When we are in the + // overview we have to take the overview's modal push into account + if (Main.modalCount > (Main.overview.visible ? 1 : 0)) + return GLib.SOURCE_REMOVE; + + // If the user interacted with the focus window since we started the tray + // dwell (by clicking or typing), don't activate the message tray + let focusWindow = global.display.focus_window; + let currentUserTime = focusWindow ? focusWindow.user_time : 0; + if (currentUserTime != this._dockDwellUserTime) + return GLib.SOURCE_REMOVE; + + // Reuse the pressure version function, the logic is the same + this._onPressureSensed(); + return GLib.SOURCE_REMOVE; + } + + _updatePressureBarrier() { + let settings = DockManager.settings; + this._canUsePressure = global.display.supports_extended_barriers(); + let pressureThreshold = settings.get_double('pressure-threshold'); + + // Remove existing pressure barrier + if (this._pressureBarrier) { + this._pressureBarrier.destroy(); + this._pressureBarrier = null; + } + + if (this._barrier) { + this._barrier.destroy(); + this._barrier = null; + } + + // Create new pressure barrier based on pressure threshold setting + if (this._canUsePressure && this._autohideIsEnabled && + DockManager.settings.get_boolean('require-pressure-to-show')) { + this._pressureBarrier = new Layout.PressureBarrier(pressureThreshold, settings.get_double('show-delay')*1000, + Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW); + this._pressureBarrier.connect('trigger', (barrier) => { + if (!settings.get_boolean('autohide-in-fullscreen') && this._monitor.inFullscreen) + return; + this._onPressureSensed(); + }); + } + } + + /** + * handler for mouse pressure sensed + */ + _onPressureSensed() { + if (Main.overview.visibleTarget) + return; + + // In case the mouse move away from the dock area before hovering it, in such case the leave event + // would never be triggered and the dock would stay visible forever. + let triggerTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 250, () => { + triggerTimeoutId = 0; + + let [x, y, mods] = global.get_pointer(); + let shouldHide = true; + switch (this._position) { + case St.Side.LEFT: + if (x <= this.staticBox.x2 && + x >= this._monitor.x && + y >= this._monitor.y && + y <= this._monitor.y + this._monitor.height) { + shouldHide = false; + } + break; + case St.Side.RIGHT: + if (x >= this.staticBox.x1 && + x <= this._monitor.x + this._monitor.width && + y >= this._monitor.y && + y <= this._monitor.y + this._monitor.height) { + shouldHide = false; + } + break; + case St.Side.TOP: + if (x >= this._monitor.x && + x <= this._monitor.x + this._monitor.width && + y <= this.staticBox.y2 && + y >= this._monitor.y) { + shouldHide = false; + } + break; + case St.Side.BOTTOM: + if (x >= this._monitor.x && + x <= this._monitor.x + this._monitor.width && + y >= this.staticBox.y1 && + y <= this._monitor.y + this._monitor.height) { + shouldHide = false; + } + } + if (shouldHide) { + this._hoverChanged(); + return GLib.SOURCE_REMOVE; + } + else { + return GLib.SOURCE_CONTINUE; + } + + }); + + this._show(); + } + + /** + * Remove pressure barrier + */ + _removeBarrier() { + if (this._barrier) { + if (this._pressureBarrier) + this._pressureBarrier.removeBarrier(this._barrier); + this._barrier.destroy(); + this._barrier = null; + } + this._removeBarrierTimeoutId = 0; + return false; + } + + /** + * Update pressure barrier size + */ + _updateBarrier() { + // Remove existing barrier + this._removeBarrier(); + + // The barrier needs to be removed in fullscreen with autohide disabled, otherwise the mouse can + // get trapped on monitor. + if (this._monitor.inFullscreen && + !DockManager.settings.get_boolean('autohide-in-fullscreen')) + return + + // Manually reset pressure barrier + // This is necessary because we remove the pressure barrier when it is triggered to show the dock + if (this._pressureBarrier) { + this._pressureBarrier._reset(); + this._pressureBarrier._isTriggered = false; + } + + // Create new barrier + // The barrier extends to the whole workarea, minus 1 px to avoid conflicting with other active corners + // Note: dash in fixed position doesn't use pressure barrier. + if (this._canUsePressure && this._autohideIsEnabled && + DockManager.settings.get_boolean('require-pressure-to-show')) { + let x1, x2, y1, y2, direction; + let workArea = Main.layoutManager.getWorkAreaForMonitor(this._monitor.index) + + if (this._position == St.Side.LEFT) { + x1 = this._monitor.x + 1; + x2 = x1; + y1 = workArea.y + 1; + y2 = workArea.y + workArea.height - 1; + direction = Meta.BarrierDirection.POSITIVE_X; + } + else if (this._position == St.Side.RIGHT) { + x1 = this._monitor.x + this._monitor.width - 1; + x2 = x1; + y1 = workArea.y + 1; + y2 = workArea.y + workArea.height - 1; + direction = Meta.BarrierDirection.NEGATIVE_X; + } + else if (this._position == St.Side.TOP) { + x1 = workArea.x + 1; + x2 = workArea.x + workArea.width - 1; + y1 = this._monitor.y; + y2 = y1; + direction = Meta.BarrierDirection.POSITIVE_Y; + } + else if (this._position == St.Side.BOTTOM) { + x1 = workArea.x + 1; + x2 = workArea.x + workArea.width - 1; + y1 = this._monitor.y + this._monitor.height; + y2 = y1; + direction = Meta.BarrierDirection.NEGATIVE_Y; + } + + if (this._pressureBarrier && this._dockState == State.HIDDEN) { + this._barrier = new Meta.Barrier({ + display: global.display, + x1: x1, + x2: x2, + y1: y1, + y2: y2, + directions: direction + }); + this._pressureBarrier.addBarrier(this._barrier); + } + } + } + + _isPrimaryMonitor() { + return (this._monitorIndex == Main.layoutManager.primaryIndex); + } + + _resetPosition() { + // Ensure variables linked to settings are updated. + this._updateVisibilityMode(); + + const { dockFixed: fixedIsEnabled, dockExtended: extendHeight } = DockManager.settings; + + if (fixedIsEnabled) { + this.add_style_class_name('fixed'); + } else { + this.remove_style_class_name('fixed'); + } + + // Note: do not use the workarea coordinates in the direction on which the dock is placed, + // to avoid a loop [position change -> workArea change -> position change] with + // fixed dock. + let workArea = Main.layoutManager.getWorkAreaForMonitor(this._monitorIndex); + + + let fraction = DockManager.settings.get_double('height-fraction'); + + if (extendHeight) + fraction = 1; + else if ((fraction < 0) || (fraction > 1)) + fraction = 0.95; + + if (this._isHorizontal) { + this.width = Math.round(fraction * workArea.width); + + let pos_y = this._monitor.y; + if (this._position == St.Side.BOTTOM) + pos_y += this._monitor.height; + + this.x = workArea.x + Math.round((1 - fraction) / 2 * workArea.width); + this.y = pos_y; + + if (extendHeight) { + this.dash._container.set_width(this.width); + this.add_style_class_name('extended'); + } else { + this.dash._container.set_width(-1); + this.remove_style_class_name('extended'); + } + } else { + this.height = Math.round(fraction * workArea.height); + + let pos_x = this._monitor.x; + if (this._position == St.Side.RIGHT) + pos_x += this._monitor.width; + + this.x = pos_x; + this.y = workArea.y + Math.round((1 - fraction) / 2 * workArea.height); + + this._signalsHandler.removeWithLabel('verticalOffsetChecker'); + + if (extendHeight) { + this.dash._container.set_height(this.height); + this.add_style_class_name('extended'); + } else { + this.dash._container.set_height(-1); + this.remove_style_class_name('extended'); + } + } + } + + _updateStaticBox() { + this.staticBox.init_rect( + this.x + this._slider.x - (this._position == St.Side.RIGHT ? this._box.width : 0), + this.y + this._slider.y - (this._position == St.Side.BOTTOM ? this._box.height : 0), + this._box.width, + this._box.height + ); + + this._intellihide.updateTargetBox(this.staticBox); + } + + _removeAnimations() { + this._slider.remove_all_transitions(); + } + + _onDragStart() { + this._oldignoreHover = this._ignoreHover; + this._ignoreHover = true; + this._animateIn(DockManager.settings.get_double('animation-time'), 0); + } + + _onDragEnd() { + if (this._oldignoreHover !== null) + this._ignoreHover = this._oldignoreHover; + this._oldignoreHover = null; + this._box.sync_hover(); + } + + /** + * Show dock and give key focus to it + */ + _onAccessibilityFocus() { + this._box.navigate_focus(null, St.DirectionType.TAB_FORWARD, false); + this._animateIn(DockManager.settings.get_double('animation-time'), 0); + } + + // Optional features to be enabled only for the main Dock + _enableExtraFeatures() { + // Restore dash accessibility + Main.ctrlAltTabManager.addGroup( + this.dash, _('Dash'), 'user-bookmarks-symbolic', + {focusCallback: this._onAccessibilityFocus.bind(this)}); + } + + /** + * Switch workspace by scrolling over the dock + */ + _optionalScrollWorkspaceSwitch() { + let label = 'optionalScrollWorkspaceSwitch'; + + function isEnabled() { + return DockManager.settings.get_enum('scroll-action') === scrollAction.SWITCH_WORKSPACE; + } + + DockManager.settings.connect('changed::scroll-action', () => { + if (isEnabled.bind(this)()) + enable.bind(this)(); + else + disable.bind(this)(); + }); + + if (isEnabled.bind(this)()) + enable.bind(this)(); + + function enable() { + this._signalsHandler.removeWithLabel(label); + + this._signalsHandler.addWithLabel(label, + this._box, + 'scroll-event', + onScrollEvent.bind(this)); + } + + function disable() { + this._signalsHandler.removeWithLabel(label); + + if (this._optionalScrollWorkspaceSwitchDeadTimeId) { + GLib.source_remove(this._optionalScrollWorkspaceSwitchDeadTimeId); + this._optionalScrollWorkspaceSwitchDeadTimeId = 0; + } + } + + // This was inspired to desktop-scroller@obsidien.github.com + function onScrollEvent(actor, event) { + // When in overview change workspace only in windows view + if (Main.overview.visible) + return false; + + let activeWs = global.workspace_manager.get_active_workspace(); + let direction = null; + + let prev_direction, next_direction; + if (global.workspace_manager.layout_columns > global.workspace_manager.layout_rows) { + prev_direction = Meta.MotionDirection.UP; + next_direction = Meta.MotionDirection.DOWN; + } else { + prev_direction = Meta.MotionDirection.LEFT; + next_direction = Meta.MotionDirection.RIGHT; + } + + switch (event.get_scroll_direction()) { + case Clutter.ScrollDirection.UP: + direction = prev_direction; + break; + case Clutter.ScrollDirection.DOWN: + direction = next_direction; + break; + case Clutter.ScrollDirection.SMOOTH: + let [dx, dy] = event.get_scroll_delta(); + if (dy < 0) + direction = prev_direction; + else if (dy > 0) + direction = next_direction; + break; + } + + if (direction !== null) { + // Prevent scroll events from triggering too many workspace switches + // by adding a 250ms deadtime between each scroll event. + // Usefull on laptops when using a touchpad. + + // During the deadtime do nothing + if (this._optionalScrollWorkspaceSwitchDeadTimeId) + return false; + else + this._optionalScrollWorkspaceSwitchDeadTimeId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, 250, () => { + this._optionalScrollWorkspaceSwitchDeadTimeId = 0; + }); + + let ws; + + ws = activeWs.get_neighbor(direction) + + if (Main.wm._workspaceSwitcherPopup == null) + // Support Workspace Grid extension showing their custom Grid Workspace Switcher + if (global.workspace_manager.workspace_grid !== undefined) { + Main.wm._workspaceSwitcherPopup = + global.workspace_manager.workspace_grid.getWorkspaceSwitcherPopup(); + } else { + Main.wm._workspaceSwitcherPopup = new WorkspaceSwitcherPopup.WorkspaceSwitcherPopup(); + } + // Set the actor non reactive, so that it doesn't prevent the + // clicks events from reaching the dash actor. I can't see a reason + // why it should be reactive. + Main.wm._workspaceSwitcherPopup.reactive = false; + Main.wm._workspaceSwitcherPopup.connect('destroy', function() { + Main.wm._workspaceSwitcherPopup = null; + }); + + // If Workspace Grid is installed, let them handle the scroll behaviour. + if (global.workspace_manager.workspace_grid !== undefined) + ws = global.workspace_manager.workspace_grid.actionMoveWorkspace(direction); + else + Main.wm.actionMoveWorkspace(ws); + + // Do not show workspaceSwitcher in overview + if (!Main.overview.visible) + Main.wm._workspaceSwitcherPopup.display(direction, ws.index()); + + return true; + } + else + return false; + } + } + + _activateApp(appIndex) { + let children = this.dash._box.get_children().filter(function(actor) { + return actor.child && + actor.child.app; + }); + + // Apps currently in the dash + let apps = children.map(function(actor) { + return actor.child; + }); + + // Activate with button = 1, i.e. same as left click + let button = 1; + if (appIndex < apps.length) + apps[appIndex].activate(button); + } +}); + +/* + * Handle keybaord shortcuts + */ +const DashToDock_KeyboardShortcuts_NUM_HOTKEYS = 10; + +var KeyboardShortcuts = class DashToDock_KeyboardShortcuts { + + constructor() { + this._signalsHandler = new Utils.GlobalSignalsHandler(); + + this._hotKeysEnabled = false; + if (DockManager.settings.get_boolean('hot-keys')) + this._enableHotKeys(); + + this._signalsHandler.add([ + DockManager.settings, + 'changed::hot-keys', + () => { + if (DockManager.settings.get_boolean('hot-keys')) + this._enableHotKeys.bind(this)(); + else + this._disableHotKeys.bind(this)(); + } + ]); + + this._optionalNumberOverlay(); + } + + destroy() { + // Remove keybindings + this._disableHotKeys(); + this._disableExtraShortcut(); + this._signalsHandler.destroy(); + } + + _enableHotKeys() { + if (this._hotKeysEnabled) + return; + + // Setup keyboard bindings for dash elements + let keys = ['app-hotkey-', 'app-shift-hotkey-', 'app-ctrl-hotkey-']; + keys.forEach( function(key) { + for (let i = 0; i < DashToDock_KeyboardShortcuts_NUM_HOTKEYS; i++) { + let appNum = i; + Main.wm.addKeybinding(key + (i + 1), DockManager.settings, + Meta.KeyBindingFlags.IGNORE_AUTOREPEAT, + Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW, + () => { + DockManager.getDefault().mainDock._activateApp(appNum); + this._showOverlay(); + }); + } + }, this); + + this._hotKeysEnabled = true; + } + + _disableHotKeys() { + if (!this._hotKeysEnabled) + return; + + let keys = ['app-hotkey-', 'app-shift-hotkey-', 'app-ctrl-hotkey-']; + keys.forEach( function(key) { + for (let i = 0; i < DashToDock_KeyboardShortcuts_NUM_HOTKEYS; i++) + Main.wm.removeKeybinding(key + (i + 1)); + }, this); + + this._hotKeysEnabled = false; + } + + _optionalNumberOverlay() { + let settings = DockManager.settings; + this._shortcutIsSet = false; + // Enable extra shortcut if either 'overlay' or 'show-dock' are true + if (settings.get_boolean('hot-keys') && + (settings.get_boolean('hotkeys-overlay') || settings.get_boolean('hotkeys-show-dock'))) + this._enableExtraShortcut(); + + this._signalsHandler.add([ + settings, + 'changed::hot-keys', + this._checkHotkeysOptions.bind(this) + ], [ + settings, + 'changed::hotkeys-overlay', + this._checkHotkeysOptions.bind(this) + ], [ + settings, + 'changed::hotkeys-show-dock', + this._checkHotkeysOptions.bind(this) + ]); + } + + _checkHotkeysOptions() { + let settings = DockManager.settings; + + if (settings.get_boolean('hot-keys') && + (settings.get_boolean('hotkeys-overlay') || settings.get_boolean('hotkeys-show-dock'))) + this._enableExtraShortcut(); + else + this._disableExtraShortcut(); + } + + _enableExtraShortcut() { + if (!this._shortcutIsSet) { + Main.wm.addKeybinding('shortcut', DockManager.settings, + Meta.KeyBindingFlags.IGNORE_AUTOREPEAT, + Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW, + this._showOverlay.bind(this)); + this._shortcutIsSet = true; + } + } + + _disableExtraShortcut() { + if (this._shortcutIsSet) { + Main.wm.removeKeybinding('shortcut'); + this._shortcutIsSet = false; + } + } + + _showOverlay() { + for (let dock of DockManager.allDocks) { + if (DockManager.settings.get_boolean('hotkeys-overlay')) + dock.dash.toggleNumberOverlay(true); + + // Restart the counting if the shortcut is pressed again + if (dock._numberOverlayTimeoutId) { + GLib.source_remove(dock._numberOverlayTimeoutId); + dock._numberOverlayTimeoutId = 0; + } + + // Hide the overlay/dock after the timeout + let timeout = DockManager.settings.get_double('shortcut-timeout') * 1000; + dock._numberOverlayTimeoutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, timeout, () => { + dock._numberOverlayTimeoutId = 0; + dock.dash.toggleNumberOverlay(false); + // Hide the dock again if necessary + dock._updateDashVisibility(); + }); + + // Show the dock if it is hidden + if (DockManager.settings.get_boolean('hotkeys-show-dock')) { + let showDock = (dock._intellihideIsEnabled || dock._autohideIsEnabled); + if (showDock) + dock._show(); + } + } + } +}; + +/** + * Isolate overview to open new windows for inactive apps + * Note: the future implementaion is not fully contained here. Some bits are around in other methods of other classes. + * This class just take care of enabling/disabling the option. + */ +var WorkspaceIsolation = class DashToDock_WorkspaceIsolation { + + constructor() { + + let settings = DockManager.settings; + + this._signalsHandler = new Utils.GlobalSignalsHandler(); + this._injectionsHandler = new Utils.InjectionsHandler(); + + this._signalsHandler.add([ + settings, + 'changed::isolate-workspaces', + () => { + DockManager.allDocks.forEach((dock) => + dock.dash.resetAppIcons()); + if (settings.get_boolean('isolate-workspaces') || + settings.get_boolean('isolate-monitors')) + this._enable.bind(this)(); + else + this._disable.bind(this)(); + } + ],[ + settings, + 'changed::isolate-monitors', + () => { + DockManager.allDocks.forEach((dock) => + dock.dash.resetAppIcons()); + if (settings.get_boolean('isolate-workspaces') || + settings.get_boolean('isolate-monitors')) + this._enable.bind(this)(); + else + this._disable.bind(this)(); + } + ]); + + if (settings.get_boolean('isolate-workspaces') || + settings.get_boolean('isolate-monitors')) + this._enable(); + + } + + _enable() { + + // ensure I never double-register/inject + // although it should never happen + this._disable(); + + DockManager.allDocks.forEach((dock) => { + this._signalsHandler.addWithLabel('isolation', [ + global.display, + 'restacked', + dock.dash._queueRedisplay.bind(dock.dash) + ], [ + global.window_manager, + 'switch-workspace', + dock.dash._queueRedisplay.bind(dock.dash) + ]); + + // This last signal is only needed for monitor isolation, as windows + // might migrate from one monitor to another without triggering 'restacked' + if (DockManager.settings.get_boolean('isolate-monitors')) + this._signalsHandler.addWithLabel('isolation', + global.display, + 'window-entered-monitor', + dock.dash._queueRedisplay.bind(dock.dash)); + }, this); + + // here this is the Shell.App + function IsolatedOverview() { + // These lines take care of Nautilus for icons on Desktop + let windows = this.get_windows().filter(function(w) { + return w.get_workspace().index() == global.workspace_manager.get_active_workspace_index(); + }); + if (windows.length == 1) + if (windows[0].skip_taskbar) + return this.open_new_window(-1); + + if (this.is_on_workspace(global.workspace_manager.get_active_workspace())) + return Main.activateWindow(windows[0]); + return this.open_new_window(-1); + } + + this._injectionsHandler.addWithLabel('isolation', + Shell.App.prototype, + 'activate', + IsolatedOverview); + } + + _disable () { + this._signalsHandler.removeWithLabel('isolation'); + this._injectionsHandler.removeWithLabel('isolation'); + } + + destroy() { + this._signalsHandler.destroy(); + this._injectionsHandler.destroy(); + } +}; + + +var DockManager = class DashToDock_DockManager { + + constructor() { + if (Me.imports.extension.dockManager) + throw new Error('DashToDock has been already initialized'); + + Me.imports.extension.dockManager = this; + + this._iconTheme = new Utils.IconTheme(); + this._remoteModel = new LauncherAPI.LauncherEntryRemoteModel(); + this._signalsHandler = new Utils.GlobalSignalsHandler(this); + this._methodInjections = new Utils.InjectionsHandler(this); + this._vfuncInjections = new Utils.VFuncInjectionsHandler(this); + this._propertyInjections = new Utils.PropertyInjectionsHandler(this); + this._settings = ExtensionUtils.getSettings('org.gnome.shell.extensions.dash-to-dock'); + this._oldDash = Main.overview.isDummy ? null : Main.overview.dash; + + // Connect relevant signals to the toggling function + this._bindSettingsChanges(); + + this._ensureLocations(); + + /* Array of all the docks created */ + this._allDocks = []; + this._createDocks(); + + // status variable: true when the overview is shown through the dash + // applications button. + this._forcedOverview = false; + } + + static getDefault() { + return Me.imports.extension.dockManager + } + + static get allDocks() { + return DockManager.getDefault()._allDocks; + } + + static get settings() { + return DockManager.getDefault().settings; + } + + get settings() { + return this._settings; + } + + static get iconTheme() { + return DockManager.getDefault().iconTheme; + } + + get settings() { + return this._settings; + } + + get iconTheme() { + return this._iconTheme.iconTheme; + } + + get fm1Client() { + return this._fm1Client; + } + + get remoteModel() { + return this._remoteModel; + } + + get mainDock() { + return this._allDocks.length ? this._allDocks[0] : null; + } + + get removables() { + return this._removables; + } + + get trash() { + return this._trash; + } + + getDockByMonitor(monitorIndex) { + return this._allDocks.find(d => (d.monitorIndex === monitorIndex)); + } + + _ensureLocations() { + const showTrash = this._settings.get_boolean('show-trash'); + const showMounts = this._settings.get_boolean('show-mounts'); + + if (showTrash || showMounts) { + if (!this._fm1Client) + this._fm1Client = new FileManager1API.FileManager1Client(); + } else if (this._fm1Client) { + this._fm1Client.destroy(); + this._fm1Client = null; + } + + if (showMounts && !this._removables) { + this._removables = new Locations.Removables(); + } else if (!showMounts && this._removables) { + this._removables.destroy(); + this._removables = null; + } + + if (showTrash && !this._trash) { + this._trash = new Locations.Trash(); + } else if (!showTrash && this._trash) { + this._trash.destroy(); + this._trash = null; + } + + Locations.unWrapWindowsManagerApp(); + [this._methodInjections, this._vfuncInjections, this._propertyInjections].forEach( + injections => injections.removeWithLabel('locations')); + + if (showMounts || showTrash) { + this._vfuncInjections.addWithLabel('locations', Gio.DesktopAppInfo.prototype, + 'get_id', function () { return this.customId ?? this.vfunc_get_id() }); + + if (this.settings.isolateLocations) { + const fileManagerApp = Locations.wrapWindowsManagerApp(); + + this._methodInjections.addWithLabel('locations', [ + Shell.AppSystem.prototype, 'get_running', + function (originalMethod, ...args) { + const runningApps = originalMethod.call(this, ...args); + const locationApps = Locations.getRunningApps(); + if (!locationApps.length) + return runningApps; + + const fileManagerIdx = runningApps.indexOf(fileManagerApp); + if (fileManagerIdx > -1 && fileManagerApp?.state !== Shell.AppState.RUNNING) + runningApps.splice(fileManagerIdx, 1); + + return [...runningApps, ...locationApps].sort(Locations.shellAppCompare); + } + ], + [ + Shell.WindowTracker.prototype, 'get_window_app', + function (originalMethod, window) { + const locationApp = Locations.getRunningApps().find(a => + a.get_windows().includes(window)); + return locationApp ?? originalMethod.call(this, window); + } + ], + [ + Shell.WindowTracker.prototype, 'get_app_from_pid', + function (originalMethod, pid) { + const locationApp = Locations.getRunningApps().find(a => + a.get_pids().includes(pid)); + return locationApp ?? originalMethod.call(this, pid); + } + ]); + + const { get: defaultFocusAppGetter } = Object.getOwnPropertyDescriptor( + Shell.WindowTracker.prototype, 'focus_app'); + this._propertyInjections.addWithLabel('locations', + Shell.WindowTracker.prototype, 'focus_app', { + get: function () { + const locationApp = Locations.getRunningApps().find(a => a.isFocused); + return locationApp ?? defaultFocusAppGetter.call(this); + } + }); + } + } + } + + _toggle() { + if (this._toggleLater) + Meta.later_remove(this._toggleLater); + + this._toggleLater = Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => { + delete this._toggleLater; + this._restoreDash(); + this._deleteDocks(); + this._createDocks(); + this.emit('toggled'); + }); + } + + _bindSettingsChanges() { + this.settings.settingsSchema.list_keys().forEach(key => { + const camelKey = key.replace(/-([a-z\d])/g, k => k[1].toUpperCase()); + const updateSetting = () => + (this.settings[camelKey] = this.settings.get_value(key).recursiveUnpack()); + updateSetting(); + this._signalsHandler.addWithLabel('settings', this.settings, + `changed::${key}`, updateSetting); + }); + Object.defineProperties(this.settings, { + dockExtended: { get: () => this.settings.extendHeight }, + }); + + // Connect relevant signals to the toggling function + this._signalsHandler.add([ + Meta.MonitorManager.get(), + 'monitors-changed', + this._toggle.bind(this) + ], [ + Main.sessionMode, + 'updated', + this._toggle.bind(this) + ], [ + this._settings, + 'changed::multi-monitor', + this._toggle.bind(this) + ], [ + this._settings, + 'changed::preferred-monitor', + this._toggle.bind(this) + ], [ + this._settings, + 'changed::dock-position', + this._toggle.bind(this) + ], [ + this._settings, + 'changed::extend-height', + () => this._adjustPanelCorners() + ], [ + this._settings, + 'changed::dock-fixed', + () => this._adjustPanelCorners() + ], [ + this._settings, + 'changed::show-trash', + () => this._ensureLocations() + ], [ + this._settings, + 'changed::show-mounts', + () => this._ensureLocations() + ], [ + this._settings, + 'changed::isolate-locations', + () => this._ensureLocations() + ]); + } + + _createDocks() { + + // If there are no monitors (headless configurations, but it can also happen temporary while disconnecting + // and reconnecting monitors), just do nothing. When a monitor will be connected we we'll be notified and + // and thus create the docks. This prevents pointing trying to access monitors throughout the code, were we + // are assuming that at least the primary monitor is present. + if (Main.layoutManager.monitors.length <= 0) { + return; + } + + this._preferredMonitorIndex = this._settings.get_int('preferred-monitor'); + // In case of multi-monitor, we consider the dock on the primary monitor to be the preferred (main) one + // regardless of the settings + // The dock goes on the primary monitor also if the settings are incosistent (e.g. desired monitor not connected). + if (this._settings.get_boolean('multi-monitor') || + this._preferredMonitorIndex < 0 || this._preferredMonitorIndex > Main.layoutManager.monitors.length - 1 + ) { + this._preferredMonitorIndex = Main.layoutManager.primaryIndex; + } else { + // Primary monitor used to be always 0 in Gdk, but the shell has a different + // concept (where the order depends on mutter order). + // So even if now the extension settings may use the same logic of the shell + // we prefer not to break the previously configured systems, and so we still + // assume that the gsettings monitor numbering follows the old strategy. + // This ensure the indexing in the settings and in the shell are matched, + // i.e. that we start counting from the primaryMonitorIndex + this._preferredMonitorIndex = (Main.layoutManager.primaryIndex + this._preferredMonitorIndex) % Main.layoutManager.monitors.length ; + } + + // First we create the main Dock, to get the extra features to bind to this one + let dock = new DockedDash(this._preferredMonitorIndex); + this._allDocks.push(dock); + + // connect app icon into the view selector + dock.dash.showAppsButton.connect('notify::checked', this._onShowAppsButtonToggled.bind(this)); + + // Make the necessary changes to Main.overview.dash + this._prepareMainDash(); + + // Adjust corners if necessary + this._adjustPanelCorners(); + + if (this._settings.get_boolean('multi-monitor')) { + let nMon = Main.layoutManager.monitors.length; + for (let iMon = 0; iMon < nMon; iMon++) { + if (iMon == this._preferredMonitorIndex) + continue; + let dock = new DockedDash(iMon); + this._allDocks.push(dock); + // connect app icon into the view selector + dock.dash.showAppsButton.connect('notify::checked', this._onShowAppsButtonToggled.bind(this)); + } + } + + // Load optional features. We load *after* the docks are created, since + // we need to connect the signals to all dock instances. + this._workspaceIsolation = new WorkspaceIsolation(); + this._keyboardShortcuts = new KeyboardShortcuts(); + + this.emit('docks-ready'); + } + + _prepareMainDash() { + // Ensure Main.overview.dash is set to our dash in dummy mode + // while just use the default getter otherwise. + // The getter must be dynamic and not set only when we've a dummy + // overview because the mode can change dynamically. + this._propertyInjections.removeWithLabel('main-dash'); + let defaultDashGetter = Object.getOwnPropertyDescriptor( + Main.overview.constructor.prototype, 'dash').get; + this._propertyInjections.addWithLabel('main-dash', Main.overview, 'dash', { + get: () => Main.overview.isDummy ? + this.mainDock.dash : defaultDashGetter.call(Main.overview), + }); + + if (Main.overview.isDummy) + return; + + // Hide usual Dash + this._oldDash.hide(); + + // Also set dash width to 1, so it's almost not taken into account by code + // calculaing the reserved space in the overview. The reason to keep it at 1 is + // to allow its visibility change to trigger an allocaion of the appGrid which + // in turn is triggergin the appsIcon spring animation, required when no other + // actors has this effect, i.e in horizontal mode and without the workspaceThumnails + // 1 static workspace only) + this._oldDash.set_height(1); + + this._signalsHandler.addWithLabel('old-dash-changes', [ + this._oldDash, + 'notify::visible', + () => this._oldDash.hide() + ], [ + this._oldDash, + 'notify::height', + () => this._oldDash.set_height(1) + ]); + + // Pretend I'm the dash: meant to make appgrid swarm animation come from + // the right position of the appShowButton. + this.overviewControls.dash = this.mainDock.dash; + this.searchController._showAppsButton = this.mainDock.dash.showAppsButton; + + // We also need to ignore max-size changes + this._methodInjections.addWithLabel('main-dash', this._oldDash, + 'setMaxSize', () => {}); + this._methodInjections.addWithLabel('main-dash', this._oldDash, + 'allocate', () => {}); + // And to return the preferred height depending on the state + this._methodInjections.addWithLabel('main-dash', this._oldDash, + 'get_preferred_height', (_originalMethod, ...args) => { + if (this.mainDock.isHorizontal && !this.settings.dockFixed) + return this.mainDock.get_preferred_height(...args); + return [0, 0]; + }); + + const { ControlsManager, ControlsManagerLayout } = OverviewControls; + + this._methodInjections.addWithLabel('main-dash', ControlsManager.prototype, + 'runStartupAnimation', async function (originalMethod, callback) { + const injections = new Utils.InjectionsHandler(); + DockManager.allDocks.forEach(dock => (dock.opacity = 0)); + injections.add(DockManager.getDefault().mainDock.dash, 'ease', () => {}); + let callbackArgs = []; + const ret = await originalMethod.call(this, + (...args) => (callbackArgs = [...args])); + injections.destroy(); + + if (!DockManager.allDocks.length) { + // Docks may have been destroyed, let's wait till we've one again + const readyPromise = new Promise(resolve => { + const id = DockManager.getDefault().connect('docks-ready', () => { + DockManager.getDefault().disconnect(id); + resolve(); + }); + }) + await readyPromise; + } + + DockManager.allDocks.forEach(dock => { + const { dash } = dock; + + dash.set({ + opacity: 0, + translation_x: 0, + translation_y: 0, + }); + dock.opacity = 255; + + switch (dock.position) { + case St.Side.LEFT: + dash.translation_x = -dash.width; + break; + case St.Side.RIGHT: + dash.translation_x = dash.width; + break; + case St.Side.BOTTOM: + dash.translation_y = dash.height; + break; + case St.Side.TOP: + dash.translation_y = -dash.height; + break; + } + + const mainDockProperties = {}; + if (dock === DockManager.getDefault().mainDock) + mainDockProperties.onComplete = callback(...callbackArgs); + + const { STARTUP_ANIMATION_TIME } = Layout; + dash.ease({ + opacity: 255, + translation_x: 0, + translation_y: 0, + delay: STARTUP_ANIMATION_TIME, + duration: STARTUP_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + ...mainDockProperties, + }); + }); + return ret; + }); + + const maybeAdjustBoxToDock = box => { + if (this.mainDock.isHorizontal || this.settings.dockFixed) + return box; + + const [, preferredWidth] = this.mainDock.get_preferred_width(box.get_height()); + box.x2 -= preferredWidth; + if (this.mainDock.position === St.Side.LEFT) + box.set_origin(box.x1 + preferredWidth, box.y1); + + return box; + } + + this._vfuncInjections.addWithLabel('main-dash', ControlsManagerLayout.prototype, + 'allocate', function (container) { + const oldPostAllocation = this._runPostAllocation; + this._runPostAllocation = () => {}; + + const monitor = Main.layoutManager.findMonitorForActor(this._container); + const workArea = Main.layoutManager.getWorkAreaForMonitor(monitor.index); + const startX = workArea.x - monitor.x; + const startY = workArea.y - monitor.y; + const workAreaBox = new Clutter.ActorBox(); + workAreaBox.set_origin(startX, startY); + workAreaBox.set_size(workArea.width, workArea.height); + + const propertyInjections = new Utils.PropertyInjectionsHandler(); + propertyInjections.add(Main.layoutManager.panelBox, 'height', { value: workAreaBox.y1 }); + + if (Main.layoutManager.panelBox.y === Main.layoutManager.primaryMonitor.y) + workAreaBox.y1 -= startY; + + this.vfunc_allocate(container, workAreaBox); + + propertyInjections.destroy(); + workAreaBox.y1 = startY; + maybeAdjustBoxToDock(workAreaBox); + + const adjustActorHorizontalAllocation = actor => { + if (!actor.visible || !workAreaBox.x1) + return; + + const contentBox = actor.get_allocation_box(); + contentBox.set_size(workAreaBox.get_width(), contentBox.get_height()); + contentBox.set_origin(workAreaBox.x1, contentBox.y1); + actor.allocate(contentBox); + }; + + [this._searchEntry, this._workspacesThumbnails, this._searchController].forEach( + actor => adjustActorHorizontalAllocation(actor)); + + this._runPostAllocation = oldPostAllocation; + this._runPostAllocation(); + }); + + // This can be removed or bypassed when GNOME/gnome-shell!1892 will be merged + function workspaceBoxOriginFixer(originalFunction, state, workAreaBox, ...args) { + const workspaceBox = originalFunction.call(this, state, workAreaBox, ...args); + workspaceBox.set_origin(workAreaBox.x1, workspaceBox.y1); + return workspaceBox; + }; + + this._methodInjections.addWithLabel('main-dash', [ + ControlsManagerLayout.prototype, + '_computeWorkspacesBoxForState', + function (originalFunction, state, ...args) { + const box = workspaceBoxOriginFixer.call(this, originalFunction, state, ...args); + if (state !== OverviewControls.ControlsState.HIDDEN) + maybeAdjustBoxToDock(box); + return box; + } + ], [ + ControlsManagerLayout.prototype, + '_getAppDisplayBoxForState', + function (...args) { + return maybeAdjustBoxToDock(workspaceBoxOriginFixer.call(this, ...args)); + } + ]); + + this._vfuncInjections.addWithLabel('main-dash', Workspace.WorkspaceBackground.prototype, + 'allocate', function (box) { + this.vfunc_allocate(box); + + // This code has been submitted upstream via GNOME/gnome-shell!1892 + // so can be removed when that gets merged (or bypassed on newer shell + // versions). + const monitor = Main.layoutManager.monitors[this._monitorIndex]; + const [contentWidth, contentHeight] = this._bin.get_content_box().get_size(); + const [mX1, mX2] = [monitor.x, monitor.x + monitor.width]; + const [mY1, mY2] = [monitor.y, monitor.y + monitor.height]; + const [wX1, wX2] = [this._workarea.x, this._workarea.x + this._workarea.width]; + const [wY1, wY2] = [this._workarea.y, this._workarea.y + this._workarea.height]; + const xScale = contentWidth / this._workarea.width; + const yScale = contentHeight / this._workarea.height; + const leftOffset = wX1 - mX1; + const topOffset = wY1 - mY1; + const rightOffset = mX2 - wX2; + const bottomOffset = mY2 - wY2; + + const contentBox = new Clutter.ActorBox(); + contentBox.set_origin(-leftOffset * xScale, -topOffset * yScale); + contentBox.set_size( + contentWidth + (leftOffset + rightOffset) * xScale, + contentHeight + (topOffset + bottomOffset) * yScale); + + this._backgroundGroup.allocate(contentBox); + }); + + // Always show the thumbnails box in fixed mode, so that we'll reduce the + // vertical space, causing the Workspace layout to show more workspaces. + // We might get the same also reducing the height of the workspace boxes + // in _computeWorkspacesBoxForState, but it would just waste vertical space + if (!this.mainDock.isHorizontal || this.settings.dockFixed) { + this._methodInjections.addWithLabel('main-dash', + WorkspaceThumbnail.ThumbnailsBox.prototype, '_updateShouldShow', + function () { + const shouldShow = global.workspace_manager.nWorkspaces > 1; + if (this._shouldShow === shouldShow) + return; + + this._shouldShow = shouldShow; + this.notify('should-show'); + }); + } + + // Reduce the space that the workspaces can use in secondary monitors + this._methodInjections.addWithLabel('main-dash', WorkspacesView.WorkspacesView.prototype, + '_getFirstFitAllWorkspaceBox', function (originalFunction, ...args) { + const box = originalFunction.call(this, ...args); + if (DockManager.settings.dockFixed || + this._monitorIndex === Main.layoutManager.primaryIndex) + return box; + + const dock = DockManager.getDefault().getDockByMonitor(this._monitorIndex); + if (!dock) + return box; + + if (dock.isHorizontal) { + const [, preferredHeight] = dock.get_preferred_height(box.get_width()); + box.y2 -= preferredHeight; + if (dock.position === St.Side.TOP) + box.set_origin(box.x1, box.y1 + preferredHeight); + } else { + const [, preferredWidth] = dock.get_preferred_width(box.get_height()); + box.x2 -= preferredWidth / 2; + if (dock.position === St.Side.LEFT) + box.set_origin(box.x1 + preferredWidth, box.y1); + } + return box; + }); + + // Ensure we handle Dnd events happening on the dock when we're dragging from AppDisplay + // Remove when merged https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2002 + this._methodInjections.addWithLabel('main-dash', AppDisplay.BaseAppView.prototype, + '_pageForCoords', function (originalFunction, ...args) { + if (!this._scrollView.has_pointer) + return AppDisplay.SidePages.NONE; + return originalFunction.call(this, ...args); + }); + } + + _deleteDocks() { + if (!this._allDocks.length) + return; + + // Remove extra features + this._workspaceIsolation.destroy(); + this._keyboardShortcuts.destroy(); + + // Delete all docks + this._allDocks.forEach(d => d.destroy()); + this._allDocks = []; + } + + _restoreDash() { + if (!this._oldDash) + return; + + this._signalsHandler.removeWithLabel('old-dash-changes'); + [this._methodInjections, this._vfuncInjections, this._propertyInjections].forEach( + injections => injections.removeWithLabel('main-dash')); + + this.overviewControls.layout_manager._dash = this._oldDash; + this.overviewControls.dash = this._oldDash; + this.searchController._showAppsButton = this._oldDash.showAppsButton; + Main.overview.dash.show(); + Main.overview.dash.set_height(-1); // reset default dash size + // This force the recalculation of the icon size + Main.overview.dash._maxHeight = -1; + } + + get overviewControls() { + return Main.overview._overview.controls; + } + + get searchController() { + return this.overviewControls._searchController; + } + + _onShowAppsButtonToggled(button) { + const { checked } = button; + const { overviewControls } = this; + + if (!Main.overview.visible) { + this.mainDock.dash.showAppsButton._fromDesktop = true; + if (this._settings.get_boolean('animate-show-apps')) { + Main.overview.show(OverviewControls.ControlsState.APP_GRID); + } else { + GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { + const oldAnimationTime = OverviewControls.SIDE_CONTROLS_ANIMATION_TIME; + Overview.ANIMATION_TIME = 1; + const id = Main.overview.connect('shown', () => { + Overview.ANIMATION_TIME = oldAnimationTime; + Main.overview.disconnect(id); + }); + Main.overview.show(OverviewControls.ControlsState.APP_GRID); + return GLib.SOURCE_REMOVE; + }); + } + } else { + if (!checked && this.mainDock.dash.showAppsButton._fromDesktop) { + if (this._settings.get_boolean('animate-show-apps')) { + Main.overview.hide(); + this.mainDock.dash.showAppsButton._fromDesktop = false; + } else { + GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { + const oldAnimationTime = Overview.ANIMATION_TIME; + Overview.ANIMATION_TIME = 1; + const id = Main.overview.connect('hidden', () => { + Overview.ANIMATION_TIME = oldAnimationTime; + Main.overview.disconnect(id); + }); + Main.overview.hide(); + this.mainDock.dash.showAppsButton._fromDesktop = false; + return GLib.SOURCE_REMOVE; + }); + } + } else { + // TODO: I'm not sure how reliable this is, we might need to move the + // _onShowAppsButtonToggled logic into the extension. + if (!checked) { + this.mainDock.dash.showAppsButton._fromDesktop = false; + } + + // Instead of "syncing" the stock button, let's call its callback directly. + overviewControls._onShowAppsButtonToggled.call(overviewControls); + } + } + + // Because we "disconnected" from the search controller, we have to manage its state. + this.searchController._setSearchActive(false); + } + + destroy() { + this.emit('destroy'); + if (this._toggleLater) { + Meta.later_remove(this._toggleLater); + delete this._toggleLater; + } + this._restoreDash(); + this._deleteDocks(); + this._revertPanelCorners(); + if (this._oldSelectorMargin) + this.searchController.margin_bottom = this._oldSelectorMargin; + if (this._fm1Client) { + this._fm1Client.destroy(); + this._fm1Client = null; + } + this._trash?.destroy(); + this._trash = null; + this._removables?.destroy(); + this._removables = null; + this._iconTheme.destroy(); + this._remoteModel.destroy(); + this._settings.run_dispose(); + this._settings = null; + this._oldDash = null; + + Me.imports.extension.dockManager = null; + } + + /** + * Adjust Panel corners + */ + _adjustPanelCorners() { + let position = Utils.getPosition(); + let isHorizontal = ((position == St.Side.TOP) || (position == St.Side.BOTTOM)); + let dockOnPrimary = this._settings.get_boolean('multi-monitor') || + this._preferredMonitorIndex == Main.layoutManager.primaryIndex; + + if (!isHorizontal && dockOnPrimary && this.settings.dockExtended && this.settings.dockFixed) { + Main.panel._rightCorner.hide(); + Main.panel._leftCorner.hide(); + } + else + this._revertPanelCorners(); + } + + _revertPanelCorners() { + Main.panel._leftCorner.show(); + Main.panel._rightCorner.show(); + } +}; +Signals.addSignalMethods(DockManager.prototype); + +// This class drives long-running icon animations, to keep them running in sync +// with each other, and to save CPU by pausing them when the dock is hidden. +var IconAnimator = class DashToDock_IconAnimator { + constructor(actor) { + this._count = 0; + this._started = false; + this._animations = { + dance: [], + }; + this._timeline = new Clutter.Timeline({ + duration: 3000, + repeat_count: -1, + actor + }); + + this._timeline.connect('new-frame', () => { + const progress = this._timeline.get_progress(); + const danceRotation = progress < 1/6 ? 15*Math.sin(progress*24*Math.PI) : 0; + const dancers = this._animations.dance; + for (let i = 0, iMax = dancers.length; i < iMax; i++) { + dancers[i].target.rotation_angle_z = danceRotation; + } + }); + } + + destroy() { + this._timeline.stop(); + this._timeline = null; + for (const name in this._animations) { + const pairs = this._animations[name]; + for (let i = 0, iMax = pairs.length; i < iMax; i++) { + const pair = pairs[i]; + pair.target.disconnect(pair.targetDestroyId); + } + } + this._animations = null; + } + + pause() { + if (this._started && this._count > 0) { + this._timeline.stop(); + } + this._started = false; + } + + start() { + if (!this._started && this._count > 0) { + this._timeline.start(); + } + this._started = true; + } + + addAnimation(target, name) { + const targetDestroyId = target.connect('destroy', () => this.removeAnimation(target, name)); + this._animations[name].push({ target, targetDestroyId }); + if (this._started && this._count === 0) { + this._timeline.start(); + } + this._count++; + } + + removeAnimation(target, name) { + const pairs = this._animations[name]; + for (let i = 0, iMax = pairs.length; i < iMax; i++) { + const pair = pairs[i]; + if (pair.target === target) { + target.disconnect(pair.targetDestroyId); + pairs.splice(i, 1); + this._count--; + if (this._started && this._count === 0) { + this._timeline.stop(); + } + return; + } + } + } +}; diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/extension.js b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/extension.js new file mode 100644 index 0000000..0b7b5c6 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/extension.js @@ -0,0 +1,21 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Docking = Me.imports.docking; + +// We declare this with var so it can be accessed by other extensions in +// GNOME Shell 3.26+ (mozjs52+). +var dockManager; + +function init() { + ExtensionUtils.initTranslations('dashtodock'); +} + +function enable() { + new Docking.DockManager(); +} + +function disable() { + dockManager.destroy(); +} diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/fileManager1API.js b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/fileManager1API.js new file mode 100644 index 0000000..4ad7dc2 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/fileManager1API.js @@ -0,0 +1,154 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Gio = imports.gi.Gio; +const Signals = imports.signals; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Utils = Me.imports.utils; + +const FileManager1Iface = '\ + \ + '; + +const FileManager1Proxy = Gio.DBusProxy.makeProxyWrapper(FileManager1Iface); + +/** + * This class implements a client for the org.freedesktop.FileManager1 dbus + * interface, and specifically for the OpenWindowsWithLocations property + * which is published by Nautilus, but is not an official part of the interface. + * + * The property is a map from window identifiers to a list of locations open in + * the window. + */ +var FileManager1Client = class DashToDock_FileManager1Client { + + constructor() { + this._signalsHandler = new Utils.GlobalSignalsHandler(); + this._cancellable = new Gio.Cancellable(); + + this._locationMap = new Map(); + this._proxy = new FileManager1Proxy(Gio.DBus.session, + "org.freedesktop.FileManager1", + "/org/freedesktop/FileManager1", + (initable, error) => { + // Use async construction to avoid blocking on errors. + if (error) { + if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + global.log(error); + } else { + this._updateLocationMap(); + } + }, this._cancellable); + + this._signalsHandler.add([ + this._proxy, + 'g-properties-changed', + this._onPropertyChanged.bind(this) + ], [ + // We must additionally listen for Screen events to know when to + // rebuild our location map when the set of available windows changes. + global.workspace_manager, + 'workspace-switched', + this._updateLocationMap.bind(this) + ], [ + global.display, + 'window-entered-monitor', + this._updateLocationMap.bind(this) + ], [ + global.display, + 'window-left-monitor', + this._updateLocationMap.bind(this) + ]); + } + + destroy() { + this._cancellable.cancel(); + this._signalsHandler.destroy(); + this._proxy.run_dispose(); + } + + /** + * Return an array of windows that are showing a location or + * sub-directories of that location. + */ + getWindows(location) { + let ret = new Set(); + let locationEsc = location; + + if (!location.endsWith('/')) { + locationEsc += '/'; + } + + for (let [k,v] of this._locationMap) { + if ((k + '/').startsWith(locationEsc)) { + for (let l of v) { + ret.add(l); + } + } + } + return Array.from(ret); + } + + _onPropertyChanged(proxy, changed, invalidated) { + let property = changed.unpack(); + if (property && + ('OpenWindowsWithLocations' in property)) { + this._updateLocationMap(); + } + } + + _updateLocationMap() { + let properties = this._proxy.get_cached_property_names(); + if (properties == null) { + // Nothing to check yet. + return; + } + + if (properties.includes('OpenWindowsWithLocations')) { + this._updateFromPaths(); + } + } + + _updateFromPaths() { + let pathToLocations = this._proxy.OpenWindowsWithLocations; + let pathToWindow = getPathToWindow(); + + let locationToWindow = new Map(); + for (let path in pathToLocations) { + let locations = pathToLocations[path]; + for (let i = 0; i < locations.length; i++) { + let l = locations[i]; + // Use a set to deduplicate when a window has a + // location open in multiple tabs. + if (!locationToWindow.has(l)) { + locationToWindow.set(l, new Set()); + } + let window = pathToWindow.get(path); + if (window != null) { + locationToWindow.get(l).add(window); + } + } + } + this._locationMap = locationToWindow; + this.emit('windows-changed'); + } +} +Signals.addSignalMethods(FileManager1Client.prototype); + +/** + * Construct a map of gtk application window object paths to MetaWindows. + */ +function getPathToWindow() { + let pathToWindow = new Map(); + + for (let i = 0; i < global.workspace_manager.n_workspaces; i++) { + let ws = global.workspace_manager.get_workspace_by_index(i); + ws.list_windows().map(function(w) { + let path = w.get_gtk_window_object_path(); + if (path != null) { + pathToWindow.set(path, w); + } + }); + } + return pathToWindow; +} diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/intellihide.js b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/intellihide.js new file mode 100644 index 0000000..9c10938 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/intellihide.js @@ -0,0 +1,321 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const GLib = imports.gi.GLib; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; + +const Main = imports.ui.main; +const Signals = imports.signals; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Docking = Me.imports.docking; +const Utils = Me.imports.utils; + +// A good compromise between reactivity and efficiency; to be tuned. +const INTELLIHIDE_CHECK_INTERVAL = 100; + +const OverlapStatus = { + UNDEFINED: -1, + FALSE: 0, + TRUE: 1 +}; + +const IntellihideMode = { + ALL_WINDOWS: 0, + FOCUS_APPLICATION_WINDOWS: 1, + MAXIMIZED_WINDOWS : 2 +}; + +// List of windows type taken into account. Order is important (keep the original +// enum order). +const handledWindowTypes = [ + Meta.WindowType.NORMAL, + Meta.WindowType.DOCK, + Meta.WindowType.DIALOG, + Meta.WindowType.MODAL_DIALOG, + Meta.WindowType.TOOLBAR, + Meta.WindowType.MENU, + Meta.WindowType.UTILITY, + Meta.WindowType.SPLASHSCREEN +]; + +/** + * A rough and ugly implementation of the intellihide behaviour. + * Intallihide object: emit 'status-changed' signal when the overlap of windows + * with the provided targetBoxClutter.ActorBox changes; + */ +var Intellihide = class DashToDock_Intellihide { + + constructor(monitorIndex) { + // Load settings + this._monitorIndex = monitorIndex; + + this._signalsHandler = new Utils.GlobalSignalsHandler(); + this._tracker = Shell.WindowTracker.get_default(); + this._focusApp = null; // The application whose window is focused. + this._topApp = null; // The application whose window is on top on the monitor with the dock. + + this._isEnabled = false; + this.status = OverlapStatus.UNDEFINED; + this._targetBox = null; + + this._checkOverlapTimeoutContinue = false; + this._checkOverlapTimeoutId = 0; + + this._trackedWindows = new Map(); + + // Connect global signals + this._signalsHandler.add([ + // Add signals on windows created from now on + global.display, + 'window-created', + this._windowCreated.bind(this) + ], [ + // triggered for instance when the window list order changes, + // included when the workspace is switched + global.display, + 'restacked', + this._checkOverlap.bind(this) + ], [ + // when windows are alwasy on top, the focus window can change + // without the windows being restacked. Thus monitor window focus change. + this._tracker, + 'notify::focus-app', + this._checkOverlap.bind(this) + ], [ + // update wne monitor changes, for instance in multimonitor when monitor are attached + Meta.MonitorManager.get(), + 'monitors-changed', + this._checkOverlap.bind(this) + ]); + } + + destroy() { + // Disconnect global signals + this._signalsHandler.destroy(); + + // Remove residual windows signals + this.disable(); + } + + enable() { + this._isEnabled = true; + this._status = OverlapStatus.UNDEFINED; + global.get_window_actors().forEach(function(wa) { + this._addWindowSignals(wa); + }, this); + this._doCheckOverlap(); + } + + disable() { + this._isEnabled = false; + + for (let wa of this._trackedWindows.keys()) { + this._removeWindowSignals(wa); + } + this._trackedWindows.clear(); + + if (this._checkOverlapTimeoutId > 0) { + GLib.source_remove(this._checkOverlapTimeoutId); + this._checkOverlapTimeoutId = 0; + } + } + + _windowCreated(display, metaWindow) { + this._addWindowSignals(metaWindow.get_compositor_private()); + } + + _addWindowSignals(wa) { + if (!this._handledWindow(wa)) + return; + let signalId = wa.connect('notify::allocation', this._checkOverlap.bind(this)); + this._trackedWindows.set(wa, signalId); + wa.connect('destroy', this._removeWindowSignals.bind(this)); + } + + _removeWindowSignals(wa) { + if (this._trackedWindows.get(wa)) { + wa.disconnect(this._trackedWindows.get(wa)); + this._trackedWindows.delete(wa); + } + + } + + updateTargetBox(box) { + this._targetBox = box; + this._checkOverlap(); + } + + forceUpdate() { + this._status = OverlapStatus.UNDEFINED; + this._doCheckOverlap(); + } + + getOverlapStatus() { + return (this._status == OverlapStatus.TRUE); + } + + _checkOverlap() { + if (!this._isEnabled || (this._targetBox == null)) + return; + + /* Limit the number of calls to the doCheckOverlap function */ + if (this._checkOverlapTimeoutId) { + this._checkOverlapTimeoutContinue = true; + return + } + + this._doCheckOverlap(); + + this._checkOverlapTimeoutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, INTELLIHIDE_CHECK_INTERVAL, () => { + this._doCheckOverlap(); + if (this._checkOverlapTimeoutContinue) { + this._checkOverlapTimeoutContinue = false; + return GLib.SOURCE_CONTINUE; + } else { + this._checkOverlapTimeoutId = 0; + return GLib.SOURCE_REMOVE; + } + }); + } + + _doCheckOverlap() { + + if (!this._isEnabled || (this._targetBox == null)) + return; + + let overlaps = OverlapStatus.FALSE; + let windows = global.get_window_actors(); + + if (windows.length > 0) { + /* + * Get the top window on the monitor where the dock is placed. + * The idea is that we dont want to overlap with the windows of the topmost application, + * event is it's not the focused app -- for instance because in multimonitor the user + * select a window in the secondary monitor. + */ + + let topWindow = null; + for (let i = windows.length - 1; i >= 0; i--) { + let meta_win = windows[i].get_meta_window(); + if (this._handledWindow(windows[i]) && (meta_win.get_monitor() == this._monitorIndex)) { + topWindow = meta_win; + break; + } + } + + if (topWindow !== null) { + this._topApp = this._tracker.get_window_app(topWindow); + // If there isn't a focused app, use that of the window on top + this._focusApp = this._tracker.focus_app || this._topApp + + windows = windows.filter(this._intellihideFilterInteresting, this); + + for (let i = 0; i < windows.length; i++) { + let win = windows[i].get_meta_window(); + + if (win) { + let rect = win.get_frame_rect(); + + let test = (rect.x < this._targetBox.x2) && + (rect.x + rect.width > this._targetBox.x1) && + (rect.y < this._targetBox.y2) && + (rect.y + rect.height > this._targetBox.y1); + + if (test) { + overlaps = OverlapStatus.TRUE; + break; + } + } + } + } + } + + if (this._status !== overlaps) { + this._status = overlaps; + this.emit('status-changed', this._status); + } + + } + + // Filter interesting windows to be considered for intellihide. + // Consider all windows visible on the current workspace. + // Optionally skip windows of other applications + _intellihideFilterInteresting(wa) { + let meta_win = wa.get_meta_window(); + if (!this._handledWindow(wa)) + return false; + + let currentWorkspace = global.workspace_manager.get_active_workspace_index(); + let wksp = meta_win.get_workspace(); + let wksp_index = wksp.index(); + + // Depending on the intellihide mode, exclude non-relevent windows + switch (Docking.DockManager.settings.get_enum('intellihide-mode')) { + case IntellihideMode.ALL_WINDOWS: + // Do nothing + break; + + case IntellihideMode.FOCUS_APPLICATION_WINDOWS: + // Skip windows of other apps + if (this._focusApp) { + // The DropDownTerminal extension is not an application per se + // so we match its window by wm class instead + if (meta_win.get_wm_class() == 'DropDownTerminalWindow') + return true; + + let currentApp = this._tracker.get_window_app(meta_win); + let focusWindow = global.display.get_focus_window() + + // Consider half maximized windows side by side + // and windows which are alwayson top + if((currentApp != this._focusApp) && (currentApp != this._topApp) + && !((focusWindow && focusWindow.maximized_vertically && !focusWindow.maximized_horizontally) + && (meta_win.maximized_vertically && !meta_win.maximized_horizontally) + && meta_win.get_monitor() == focusWindow.get_monitor()) + && !meta_win.is_above()) + return false; + } + break; + + case IntellihideMode.MAXIMIZED_WINDOWS: + // Skip unmaximized windows + if (!meta_win.maximized_vertically && !meta_win.maximized_horizontally) + return false; + break; + } + + if ( wksp_index == currentWorkspace && meta_win.showing_on_its_workspace() ) + return true; + else + return false; + + } + + // Filter windows by type + // inspired by Opacify@gnome-shell.localdomain.pl + _handledWindow(wa) { + let metaWindow = wa.get_meta_window(); + + if (!metaWindow) + return false; + + // The DropDownTerminal extension uses the POPUP_MENU window type hint + // so we match its window by wm class instead + if (metaWindow.get_wm_class() == 'DropDownTerminalWindow') + return true; + + let wtype = metaWindow.get_window_type(); + for (let i = 0; i < handledWindowTypes.length; i++) { + var hwtype = handledWindowTypes[i]; + if (hwtype == wtype) + return true; + else if (hwtype > wtype) + return false; + } + return false; + } +}; + +Signals.addSignalMethods(Intellihide.prototype); diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/launcherAPI.js b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/launcherAPI.js new file mode 100644 index 0000000..55f44f7 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/launcherAPI.js @@ -0,0 +1,281 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Gio = imports.gi.Gio; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const DbusmenuUtils = Me.imports.dbusmenuUtils; + +const Dbusmenu = DbusmenuUtils.haveDBusMenu(); + +var LauncherEntryRemoteModel = class DashToDock_LauncherEntryRemoteModel { + + constructor() { + this._entrySourceStacks = new Map(); + this._remoteMaps = new Map(); + + this._launcher_entry_dbus_signal_id = + Gio.DBus.session.signal_subscribe(null, // sender + 'com.canonical.Unity.LauncherEntry', // iface + 'Update', // member + null, // path + null, // arg0 + Gio.DBusSignalFlags.NONE, + (connection, sender_name, object_path, interface_name, signal_name, parameters) => + this._onUpdate(sender_name, ...parameters.deep_unpack())); + + this._dbus_name_owner_changed_signal_id = + Gio.DBus.session.signal_subscribe('org.freedesktop.DBus', // sender + 'org.freedesktop.DBus', // interface + 'NameOwnerChanged', // member + '/org/freedesktop/DBus', // path + null, // arg0 + Gio.DBusSignalFlags.NONE, + (connection, sender_name, object_path, interface_name, signal_name, parameters) => + this._onDBusNameChange(...parameters.deep_unpack().slice(1))); + + this._acquireUnityDBus(); + } + + destroy() { + if (this._launcher_entry_dbus_signal_id) { + Gio.DBus.session.signal_unsubscribe(this._launcher_entry_dbus_signal_id); + } + + if (this._dbus_name_owner_changed_signal_id) { + Gio.DBus.session.signal_unsubscribe(this._dbus_name_owner_changed_signal_id); + } + + this._releaseUnityDBus(); + } + + _lookupStackById(appId) { + let sourceStack = this._entrySourceStacks.get(appId); + if (!sourceStack) { + this._entrySourceStacks.set(appId, sourceStack = new PropertySourceStack(new LauncherEntry(), launcherEntryDefaults)); + } + return sourceStack; + } + + lookupById(appId) { + return this._lookupStackById(appId).target; + } + + _acquireUnityDBus() { + if (!this._unity_bus_id) { + this._unity_bus_id = Gio.DBus.session.own_name('com.canonical.Unity', + Gio.BusNameOwnerFlags.ALLOW_REPLACEMENT | Gio.BusNameOwnerFlags.REPLACE, + null, () => this._unity_bus_id = 0); + } + } + + _releaseUnityDBus() { + if (this._unity_bus_id) { + Gio.DBus.session.unown_name(this._unity_bus_id); + this._unity_bus_id = 0; + } + } + + _onDBusNameChange(before, after) { + if (!before || !this._remoteMaps.size) { + return; + } + const remoteMap = this._remoteMaps.get(before); + if (!remoteMap) { + return; + } + this._remoteMaps.delete(before); + if (after && !this._remoteMaps.has(after)) { + this._remoteMaps.set(after, remoteMap); + } else { + for (const [appId, remote] of remoteMap) { + const sourceStack = this._entrySourceStacks.get(appId); + const changed = sourceStack.remove(remote); + if (changed) { + sourceStack.target._emitChangedEvents(changed); + } + } + } + } + + _onUpdate(senderName, appUri, properties) { + if (!senderName) { + return; + } + + const appId = appUri.replace(/(^\w+:|^)\/\//, ''); + if (!appId) { + return; + } + + let remoteMap = this._remoteMaps.get(senderName); + if (!remoteMap) { + this._remoteMaps.set(senderName, remoteMap = new Map()); + } + let remote = remoteMap.get(appId); + if (!remote) { + remoteMap.set(appId, remote = Object.assign({}, launcherEntryDefaults)); + } + for (const name in properties) { + if (name === 'quicklist' && Dbusmenu) { + const quicklistPath = properties[name].unpack(); + if (quicklistPath && (!remote._quicklistMenuClient || remote._quicklistMenuClient.dbus_object !== quicklistPath)) { + remote.quicklist = null; + let menuClient = remote._quicklistMenuClient; + if (menuClient) { + menuClient.dbus_object = quicklistPath; + } else { + // This property should not be enumerable + Object.defineProperty(remote, '_quicklistMenuClient', { + writable: true, + value: menuClient = new Dbusmenu.Client({ dbus_name: senderName, dbus_object: quicklistPath }), + }); + } + const handler = () => { + const root = menuClient.get_root(); + if (remote.quicklist !== root) { + remote.quicklist = root; + if (sourceStack.isTop(remote)) { + sourceStack.target.quicklist = root; + sourceStack.target._emitChangedEvents(['quicklist']); + } + } + }; + menuClient.connect(Dbusmenu.CLIENT_SIGNAL_ROOT_CHANGED, handler); + } + } else { + remote[name] = properties[name].unpack(); + } + } + + const sourceStack = this._lookupStackById(appId); + sourceStack.target._emitChangedEvents(sourceStack.update(remote)); + } +}; + +const launcherEntryDefaults = { + count: 0, + progress: 0, + urgent: false, + quicklist: null, + 'count-visible': false, + 'progress-visible': false, +}; + +const LauncherEntry = class DashToDock_LauncherEntry { + constructor() { + this._connections = new Map(); + this._handlers = new Map(); + this._nextId = 0; + } + + connect(eventNames, callback) { + if (typeof eventNames === 'string') { + eventNames = [eventNames]; + } + callback(this, this); + const id = this._nextId++; + const handler = { id, callback }; + eventNames.forEach(name => { + let handlerList = this._handlers.get(name); + if (!handlerList) { + this._handlers.set(name, handlerList = []); + } + handlerList.push(handler); + }); + this._connections.set(id, eventNames); + return id; + } + + disconnect(id) { + const eventNames = this._connections.get(id); + if (!eventNames) { + return; + } + this._connections.delete(id); + eventNames.forEach(name => { + const handlerList = this._handlers.get(name); + if (handlerList) { + for (let i = 0, iMax = handlerList.length; i < iMax; i++) { + if (handlerList[i].id === id) { + handlerList.splice(i, 1); + break; + } + } + } + }); + } + + _emitChangedEvents(propertyNames) { + const handlers = new Set(); + propertyNames.forEach(name => { + const handlerList = this._handlers.get(name + '-changed'); + if (handlerList) { + for (let i = 0, iMax = handlerList.length; i < iMax; i++) { + handlers.add(handlerList[i]); + } + } + }); + Array.from(handlers).sort((x, y) => x.id - y.id).forEach(handler => handler.callback(this, this)); + } +} + +for (const name in launcherEntryDefaults) { + const jsName = name.replace(/-/g, '_'); + LauncherEntry.prototype[jsName] = launcherEntryDefaults[name]; + if (jsName !== name) { + Object.defineProperty(LauncherEntry.prototype, name, { + get() { + return this[jsName]; + }, + set(value) { + this[jsName] = value; + }, + }); + } +} + +const PropertySourceStack = class DashToDock_PropertySourceStack { + constructor(target, bottom) { + this.target = target; + this._bottom = bottom; + this._stack = []; + } + + isTop(source) { + return this._stack.length > 0 && this._stack[this._stack.length - 1] === source; + } + + update(source) { + if (!this.isTop(source)) { + this.remove(source); + this._stack.push(source); + } + return this._assignFrom(source); + } + + remove(source) { + const stack = this._stack; + const top = stack[stack.length - 1]; + if (top === source) { + stack.length--; + return this._assignFrom(stack.length > 0 ? stack[stack.length - 1] : this._bottom); + } + for (let i = 0, iMax = stack.length; i < iMax; i++) { + if (stack[i] === source) { + stack.splice(i, 1); + break; + } + } + } + + _assignFrom(source) { + const changedProperties = []; + for (const name in source) { + if (this.target[name] !== source[name]) { + this.target[name] = source[name]; + changedProperties.push(name); + } + } + return changedProperties; + } +} diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/ar/LC_MESSAGES/dashtodock.mo b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/ar/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000000000000000000000000000000000000..78deaa584f2d2593e60609b7a4dc8502b9e6fc46 GIT binary patch literal 8303 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 J{;Q~@{u@j=uwnoJ literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/cs/LC_MESSAGES/dashtodock.mo b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/cs/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000000000000000000000000000000000000..1cc799db8d8a580c195b45e834f93152e8af4491 GIT binary patch literal 10321 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 Ji2SdJ`fnXRi#q@S literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/de/LC_MESSAGES/dashtodock.mo b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/de/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000000000000000000000000000000000000..f7b2ed5d7df55c366fd7e3e10f644829af6eb948 GIT binary patch literal 8726 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 MZ%co>;BRRE4^anh-T(jq literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/es/LC_MESSAGES/dashtodock.mo b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/es/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000000000000000000000000000000000000..99888b074cd0217febd7f9fd378a69a381652ebc GIT binary patch literal 10083 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 PGx14=|8Bt1%W8$?P?>bJ>7jkCi?99#F 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/fr/LC_MESSAGES/dashtodock.mo b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/fr/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000000000000000000000000000000000000..a9a712cbd7a81c5408ab33dda9e41f8d70a9455e GIT binary patch literal 10218 zcmbuEYm8*aRmUs%5j>6^;{+S*1Xnf*UT3F!*GufR$Fm#H&djbi?Cg$bX1#WT!MD4s zyRY~5z3uy$*)bmomV&T^gcR`*Mv)zeWN(5%K~W-55#%SVL>e6AQWVT z5+LIDulwkp-LZ(oRoDEx@2y*P>eM;^b570Q-F)@08?J)(9@@)S8S`l{dOLr({^2@f zUIqUVyaxQ@j4{`Pe_Y|8f$!t@tKco*n;;)^J)M3SyrsfBz}xt}AGF{icogh`N5J0! z-vRzjg|CCx^7~CtqWo8N|0+63j+;Qua|`(6;QPQkzz4ut@Z;c*fWHiCUC)6(27Vd* z0QeW+kAnXJz7xFuhH}2!LCv=h)c79)_52=?k6Gr=)!;g)evg7(@Coov@MTc)d>ecQ zy!OUY-e+|Ph;3oJ8_;v8z;C?#o1CN4Q-xJ`w zz_h|&0@*6_94I+I3u?Sep!D`fp!)v_sQvj%P~(0Rl-%C|)&F`17v2uefd{~Oun!`t z`CU+Q{2{3Id;^p|z72j5d=unjZoAnSK4!MUkANC?4Lk^TK*{+jkgYJk3F^Jif|B=3 zU>p1jh)T^HARqHx{%D==U@<=l-U90WQBdnx1%Cp35_~WCEGRpD5tJRi0$T8&K-s}H zDB*VSc2Msh0S|!>gL?lN5Y?C$zz4uDRKNcfls>)>@-a6snAW!sl>CQ5>E%99?>`92 zP96r&gWDi3XTA(w0-`~5ny#H?) zyX5(9g*UROd-(l+Q0sgQ)cXafe!l=p|Gx%Gj?aMF-yQHl@LS*#cqht3G!udHr)NOP z|J$JCdmfZNUaWq936vjw4Yc4Z;4JtrpzP^Zgpq%4fFA�(oS91C+d905#9+p!BNF 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/gl/LC_MESSAGES/dashtodock.mo b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/gl/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000000000000000000000000000000000000..b89c1027c134f064ab40cd815342b98bb7babc27 GIT binary patch literal 8819 zcmbuETZ|mpS;x;Nn}i*bT`(c{z*%R*-lp4QJG<*0k0)u{W3Q7LdonX#dy_yo-F>FJ z;_9m2y3EB<06_>+q#!{ANGL+FcnJHj4@(3O4oD~?B!qb2^1w<+D=q(Cdu*?GpfsodT~+6N=evLB>v`?S{l8(j9OFZbN8W18FM;!K=Y#9i4;k||@LBM$ z!H?JXM;15lf-Ue_n37xrFam!L{1Nb9LD6{w{0#U2#GVKH;Jd-^fJec<1b+OfYR48sPz{?(f=H{2*%(N_$`n>^F2O9|NEf$dk>S}0k*)8 zf=_|c$7jJK;Hbu50Qoat=i?aoyP){^8xT>f`N@M2mme&EvdgEy4}uQV`Y(YC;BSFi|7Rde&A))B z!Po2ilQ1E>p9f*h^g+!lz(>Jf2CsoHgSfVN8%oyx4}d%}PlB4a3`%d?;3k-Wvd?$H z&V9!G1t>i{2(#Y<-wkU18=&^P59JGwfTI5qQ1YyRqT@jM;SEsw`!XoLe;s@b{C%(k z{#}h9es5)m&x5ja49YLQ2ufdH1|`pLgYxI!0WJ6vI1m0g2r2VADEmCf=F<05pq{UT zqB{VYVs=1OYJL@b5&S0j2zUfz=>8e-6!;RT`Tq#M0R9&!dRq_|{TqCg;8(!UfIk2~ z0$xKIPl8_rWzTPc($AmrA^!gXlzjgVLel&vD7`&I5KEu$2c_TB;0bUQRNnY`kS)v~ zgWBiS`u+!?*1wm{{&oFK1?D6uStPG6#lb~}{8!gy#uDQh zhGOO%L)S7xIyuWwPLcfUjN=UHOxIf&Pca_V4VQdD`C8X?#x;igUDt&Q%oCt|=yQxS z^)vax3gc>h-@vE%BL8rVR{cIPz8A>%Wbd~#9%m?bW2$idET5+t@{8tD3@l5L#igSJ4V0?`6aosGm7Zw)GQa*Nx&7+(C%+Y-0;`r$D z)-cNLI7-=v>}yj0XR|U%^ukS9o{(===e;bA<9jVn?pT**eV<|9SnO9fyHURwa#IxP$OQJS z>q#@_T$pw)^Qf+GyZh!`nKKEuvFKI$!w`9~AN!d%qe>4!Alq$JHC>sGT`wwTX*Ci~ zXy!BIq>R4Hhc(aV_It4$FH6h85D%?PeLl5GS`1NuIiL1!1kbi{G>QuD`vX6jxRSUo z>o?1>&z^j2q4n(J*5x)IrdiP|i`*7zkbOtKg2MB*y;Rs9vEt%9MH*Y?Xrf359=koQXJ=k-?hD3g_ZBwk#|XgKeU?}%Rf%RC*L$^a@%TDcjr)cB}?hucc6c^&W zHt(fv+mCWh?$2wl9Ci=U>h*k{M>6_$mX7QoA+@!NUGHwRiZn&sEU@VJ3Nn?#f7f^O zs98QFtb^wiR-n9WQPuU zS$&ZfQj+aX}^@FHNQ?<#ZrFK|KP&FYeBi8Sm z-7LP9BU zopIJ(;^PpBi#FI>kz84)3Q|w1FzYGimm?^m&tiLa80JhqMdJfg&8 z`_NA!@}gNUNm|h7?d~Fu{$d$1e>_=Oy{VLgvdpJTvekx*@ZehU8+dcb4gD(qbF-l^ zG@A~C1Q&p8zwAkKW7qEoUxn#zmDe-Zk4l_mGZcr$_M4sv>d!E0&8Bvd5nb=bfW!)k zP#}V5H^zfbsA7i-#NCB$vQ3>*Cp4P0H(nK;Hli8Y$v7ExAdDj%J zncZqyL1Ri1T{#2pYKQXSrn1)sLB3XRGg%OAhX}|m$Be#DDkdgxDae7V$%AlkAY}Tf z&yz=|33yh%hW%!%nN%8xHPZDkJN0B**Chqh0EE+3jr>X8i5Fy(Jloy zSe~~_Du6<2lOUAYWwYf+<1}+wL;&Z~t0b7wTVtgV>fvRmKJ%(Kgv%Iz4QaHF)!MV# zW-A?=tum=g5iHJnaDB{Zo2wY0TSjrwBDJvd$cINa{C+oe86p0r#ot`>-LWGjTvLUs zW05=+(#t$*=d?<3Yff&>8;TgNBpDm!);VuR-p-uyJ+V& zJSr<%YxyARx6YP>ytS3`c>PLi12#IqDsn|@+%G`QLJEb)Z8!H zK|Hs1X>Dc7+wu0o+_@BQN!Vh43=aj_^w>CdQF7Y$hAu-@XRdBtXq}qQ3t8T0t(Byg z%9@MzR5vQ-uD7bAf8cRF^?lq~t-;B;^_XLL+~Nc>$`@@i4!8N46Q^zU^~__5efW$$ z{`l#+H*acIa-zL%Y8>4}VmB{cwii3vD^-Y_z znRclnDZbC4SZ7y+Dh_VtcRvUXO{OHf#vX(pAt!b?Fs32bn)7J57m1a=quNovs=KQ1 z;Ut#yB4;{-R5rDavkFWVkQlKiNB&e{U4*;z@Ex&16)Hp;U9fq4=X-D0Ty$_dw@<$5 z#E6v~$5T5l;~WMO7penwfnzxNdLj*HT+Q3=_xJ>6jn`IciGz zeP_1F9S5(Z@=eZIL6ER|WhW}6+^IJ{=elTI4>u2PPkSxo!=iHOlph=9?CLakw02@m zdM@@kcqu`v9#iM+UfU0D_b5{-tS62C_`2)r)PxY3UJKr^3au4eNz&0cCX&q6#t-x! z`Gb`>LwTeped{)aDQ#BUp7(aobCe^U2jwVm<}oUj*$x}|C@!d{KpeM6XNS(+i{-(~ z)^%O9mr|o@m2GpUKDKgUGFqR)mN|S93PBU2I!ZYtXK84?V&n;I|xWLYbD!xGLgdOqrmO zU}E%%5N0!iKeCU6X*AMDM?7SbD3eyJDdd>hy;*>w?{3CQk>U-9V;#cPjL9N(cSdyU z_!Z$3wc{|c;#Mh%?~@z_7qx~Q+V$87Hc6d3)PItf(#j+$ur9bHKLQ2G@1hl&^O(+A zX>TUjChDEeC}nq{|Rxk#a>oGox~q(4;ZF$-}TX^&H%p zJjOY&7mYE8;zQ{ow1m6}4HfzHUK>q2yMtT!D8JBhwZNTvHAR{58EEX+-TaMhIygTewMv2rXV^O$=0p?94M5 zZE}fOJ$RX)mH16c2MeF8;-HSu@->R0+O3Sn{N{lbWv%uQkok2iZjV0X|7t{=ooDI*S*XrNhgiSkgh&FG#}jI5Mme=iOGfB%_0NEg>OV}PlO_wv zc|wHHh2G=jK~_yO+!7_wl4@yvGLm$@7wb3@LN=sty`LVEsDgEUL_3N>O5!9r*t!*U j*?c&@g%~Zz96hUF<(t>?Qj9lazr*fccd4W}+SLCArqePd literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/hu/LC_MESSAGES/dashtodock.mo b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/hu/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000000000000000000000000000000000000..6e2d85e753a86dd0fc1842521009d7711b4f7413 GIT binary patch literal 9276 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 MU2R*TOQT2oU*Ol9fdBvi literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/it/LC_MESSAGES/dashtodock.mo b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/it/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000000000000000000000000000000000000..fc1f7228714a3dce08d4c91435e56c3ac9a43a0d GIT binary patch literal 9880 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 L)Ag4GoRaxp;6^~+ literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/ja/LC_MESSAGES/dashtodock.mo b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/ja/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000000000000000000000000000000000000..9b9b9435c6df9d64c9dd1f9beae9234c5bb79bc6 GIT binary patch literal 12122 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/nb/LC_MESSAGES/dashtodock.mo b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/nb/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000000000000000000000000000000000000..005884f3d914017e5aa4f8fec3e86f02211cb193 GIT binary patch literal 9203 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 T)DEZN<7b(nv}uI^Y+C;h&0L5! literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/pt_BR/LC_MESSAGES/dashtodock.mo b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/pt_BR/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000000000000000000000000000000000000..cbe7c664e09ac302402acdf2d673605d06cc94e6 GIT binary patch literal 9895 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 TAv9W#e7%&pnj&@~(VZ9Qp9u@+;M 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/sk/LC_MESSAGES/dashtodock.mo b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/sk/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000000000000000000000000000000000000..5b25bec81aefd6d07f59bc9f2b2cb8a5be84d793 GIT binary patch literal 6290 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/sr/LC_MESSAGES/dashtodock.mo b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/sr/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000000000000000000000000000000000000..da3e086199f2e685e378f0782bc638eb22763eeb GIT binary patch literal 11186 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/sr@latin/LC_MESSAGES/dashtodock.mo b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/sr@latin/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000000000000000000000000000000000000..1fcfb47a64385b209ea62252130336388c33e02d GIT binary patch literal 8824 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 scp)9%vJI{e=R(O{8aG2T?{LUjP6A literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/sv/LC_MESSAGES/dashtodock.mo b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/sv/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000000000000000000000000000000000000..bab036ea01d4cdcc3e4d6d9a35c0e4e292c48119 GIT binary patch literal 10093 zcmchcd#oJSUB^$-I3aFn)ADE=T6%CB^78Jroi>k~8`sy*xbeMqe6Q^!rJ=Jsb9cu( zJF}UY-Rn(TRDpRV2no3asAz-)sHqJFfdh!{-x>_b|TwCSyJi{@D%u;nUq|%wK^q zxC2~xvoZWL2R$x>Kg9K8;P-={1et0sf!_r_>+$p8&0IeZTJUA?0q`~O9`KI07{fo) z@|c0wbNxwBqWpxv|1>B$ei_t0&w}p;e*=6UxB>14{}p^Yc++=R=h_9ni|Y@A9{?W# zzYqKr_%`qosQrE&)P7$Cwf^t;=YI(D&wPzP*MYBqn)gqjs@X*8KvgbN>PO!{DF#`>%mI*LT1l1mDeJ-wWOgipOU` z@$>>{!Aqd*@&fn+;7g$1|2nt_)I+_$7ew{uBzQmgaew`*p!D)vAg`DgL7nF>LCOEm zp!D+%Q15>W6mO)wbHHsNE@?guYW_t~ay|v>y)S_}?+YHk3hMoT@XxP(C%WQ#Cn$Y9 z4(_@JzXo6C`e~fvTJRi)lKvj^*aa02MjkJL;>Yu#v!B0y8I->M2DIR}z`MblxOp3R z4Ai~@k578M2ul8+2et1rpw4&MKmQ_#D$EO@*U^Bx6n0aK8p zo6msK_iuvo>#u;4|Er+v@fRSXntudkkJmxTb0dpt-rGT4-waB>yTG3VKLqMre+(ju z`FHSB;PsF|e7y+n1Ai909efeg`(Fnyg0F+(-H-7{?|%+_0{lHN1MeV+$-bWj=fD?1 z#kJS`{kPak|4mSG9{?rqDNuTO6qFv%gF4>;)bpo6wl1hsDgN>4un-T{6ARJ{Ld5EGf# zKwRG3_<<@uy%)p<%mGmG;}KAD|1>xceh!2Z=I=r2=iA`@;B}Bp>m2|k|HnYZmjS5# zehJ(GJ`albS3sTbE8wl*pMy)_fBEN!d0Ba58N3gC4Ags{0d=ltLFw^#LFwabp!B|j z#a6+$f|}n3uK^L$*+)8%elo@d#V2`;`pozr)byV-=V#DE`U@ z(uZ^?{Wlom#d$`=xSpZUlND_FD^BYpTkm58jD+zK#>W{?Fr?QOqsP!k_WvkjKV!^z zgmIQ}l7ZUY=STTF^51K~rA^MjWbRXV9C&;TRIJix2P0;*8Fw)rWqgpK&wUlRzi;y2 zcY{B|ILA1_xSOGmID3#WWQeEwJkD5m->N_F1U|vo!#KncZ?9v>&-9T$q>LlF;4{ZK zz*uI~pGE&gTzrrr4(cN=Kg_s+ahlO*xX(f{2vS?bPeu>yD*8c^?ApKDjSD--vrZoL zZ9D5ngBej2o!d?r=61asrM9it;On9Sd9r-{cG1qWB-!ru^s4u=JdASen@IfXVm%JaE*E8)_06F;;behv z$FMoBbG#5BoMq zL)#B7xbN8@XvgK)j)G(u6*G(Z2QznSwM+z`H{ooKpF zR`S+34f=80962AgOLL?@;HaxPE1Dx|&|>$M;ULQIylqe8zT0h3*rJ=|WqVi_l0dc? ziNg?~XxihYZR5j1QeKFCAIJIlTB!OBH{Oo5C|7f&s+urn*@7R76q@~1*(I>G3`JD2l3@3%V zsmMTI$elMTlx9 zVXTf&ONuOJiItpgW@u5^){q#B&*Ctstrd0RDztklN&8LrDh^*qFpaw2>s#Vj3~D%xmN+sO>yxTWrWk)=$trVP6cU{ zIPaggjz@|zmDQ`j5E13fsSH|)U<&49@br!-?Pi)&Icy!`1H{+}r6wV_NXz-Sw!1ks ztfH+{K92)EF^rjy$NCqu%7vYBP?mXkNTAgpdrLUU`=^5#Px0nCjn}yN6jm{)FsJ41 zW+lKtj#z9H4%^b?AP7U}6C9#d<;FY+;~~^raS68O&Ps%*hBJa&vl4asu%6hoqWxrE zf@+6k!eTDnI$x^I%8ITg*pz0smI;#$eIl5wQ3uJOg{fJKxJGHSI4u!eRpa7c_0)N` ztTV#`n>w}>)r_@Zl;s46*%|&(-(}0tj00%%UQ_bsFPm6;O*`Z#5IGtHqSPm8z<>tHVyt%1we~Z{o~mB}-x$^2Cr-z;Q{C z#Is3=Gb`h?3@*&u1;>Q_yX}H9w~%BhaP`4{vl{gW6lr;kcNfy8&NQRR+ORGTxQ8wg z?pBUt;{oJ#vD-VcCs)&~W&=~zBdf#IXF;8BhRQz7HO-mSvBI3eK&@e%lntT~a`Yp2 zE2KP3RT@;Kre^h`aVraQ0>m4ZHRfE@8UzHCbIQBkb6Lnm>fKn1{(zKQ*UI%17zz8N zgqm$WV$SHKX?!y=ZwWh@DzL`!uyMBPFy`&f(-Ar?8%sqe4jTuDouaXtar@NC#%ZXo z^w~H}j+(do_Uzl!*mG}V&s}!!ee)mOdnea>cAf}|vVro_f=rg>^Qr+l6ljYnA-^c_ zkcGax(?l!Podcad#Xys0Yd^22ogpW0tVThfZR=}Fo6*jt<4Z@TlJ0Hp*?A}<)}(AW z9w1RkWS$!&L7d)i+ua~XaSxnXJ=(Z;I?rW{C~q7|+nJ0%Z|`lz<<3VMRqN#>K9xlw z{;;ok*G{udhj^IUBY@jgi`dKheZ2C)BPUkvnbpN*du&mi;c<0_|6ffZwFV*-l!$p$ zHHI5ks?IPuALWzQ@MK@TU|`oau242m<6U(Hz34s_K+jTc)hE2FEu?~?Ey;skN&Xi!%@om-DO=r+nualtDudbh7a;m zKNmy^>7kyNZ(Qz)L6vGZt_%k03|G!>T)qHzs)J}sMWI`YYWi${XjaNRqAFRTG^IKS z%`urA@$gjsazD$Hs8@8@5jHtXDlZ2F({L2`rmIsIaXa@7W6`6oMLGPwvAS`kPn|+> zo)A)0`-ae0K&0=>W~ zs6=C$z@XnJvY`c{w|LrcR%?@;8h#)3a{P)?ui6SIeyDx0dNP(}TjG6S(ez-z$tDwH z+l`WVSc)? zk0vhC1|tr>xe?wv-=!Lo2Xw=wM1N(N+PY0M`#i{roy8a#XJ-&*au}43otk;3x<)_W z;s!j(BXd^9RKk1Ehu28}t@`4PpR9s<#if~8U3ImvdGckJ)-ID2FRrZu^P>O1nh#NjEKNeFU1`91tFA!ceVmTq$}{LX;qHR(34&F-b_R zwTN9!Wg<7L=W+vur+M2$h!G^;^2n>#ERKp~SO#P+g$Xs;lVd@t^V2JsL`HHU#vjOs z;!3d8F%TlI5c`$L@-bSr1U3?t=rhV}eu=IzvevYQSmqP7{77CCEzDBeP5o zH|f5eobZHBWRibe6;K^~D@SPST-D_Yvc_bC&3>_^Ff*%LQ>$0)B(cD!43brDOl9d^ zQ$=M%QL}pBDCf@VDMd#P`>Kb&A@@(Jdvw@x5=>1mK4z+WIZCNW>(|5&+97_mb5SM8 zeJl~}w zXF^5OZPKCFOijt{Gnh-nD%M><-CR1kTW_eBdR_AnY#nS9VSGFP&?pG?!#Dq-GA0|> zv3jPP7suYXn|D$u3QRJRKbXr1+a#uS|tgT$@%aEqoct&|t_4}!5nP@)Zvg|b4 zR&=eMWMZ`~L6nMaVk=A7&~CJC~b0+2#PV1znx2>!7Ni x0QiwQ3@D0ddVB%FuZ~Ipgo!HO6g;ahh~mQ*Wu|lDiu$$bs$)*nS-h=|gv&_&1y*n-5YL6v~gl>Ad6N+rZY2||cORj7pc z{LVdhX4Y}wAFk%i_dd=&=XZXO@9%f+-@oVPUr`(d?Z;?4H!1ZwFnkAp9RKirO1%Vr z2fPLR{V}EXfPduh&%qCJ{StUP_!`Jh?V-~8mF zhyM)T%JplYi1Iym|0X(#9Pb8Yp4-761wROW06YqggC7BZ5d1|@*7Y2C2lxlz-QeGV zKLWlE{s6e=wsO9^K$&kZDC6G?%Jcg{erle-o55vJ`aK3V!6(3d;0`GAyb69Cy!HD_ zc|Qr(xc((j^z>DSe+vd&zXHA+yuf5K-ztBPgKOYL@EhO{gZt^U7n}iQeNTWt1f~w3 z0Wp<&4iq_`2W7k$LDAbEg3|v_L9x#_K^gb&K#}`DKPs`@QZ zM*7Gtb`gj$*2Ye0Wr|x`@QvB4o!-qf_cL|&T8=%PfX%JJWUjya6=RuM8Dp&`% zK~$<<0r{yn_>*YDDu4w$~wO5a17;2|M!71&OEpbE`YNBFMwCxrqq|ff8qLWoLuhT#v(+H z_d47M-p}0$%|i0`Eh4GHwPUqWT3;_i!H!AQ9iaI4I4J$^2cHFJzysjdLGh0_z^B1GFtXU|H^3?Ic~JcO z+n~tzb~^juouJ6~^Pu?abKn=j7r-{S1PO_q{}>zx{}L3r-vH^N_UIDt1x4P|py=;1 zDE|FXP~_-<;!jV3V$WwlL{*;x?*_jN$~s>H#eV+;irlY*4}p^?SLEn|BFD!-ng4e{ z(Z^qb`@xq%>%XA%zl}x9y50}Uya&PK;QgS;bp@35{2Yi&s84}n=P!a{uiph_oNs}$ zzE{98@Oz-le=ANU`n(5}^~`|c508Vgu8)Bt?{B;N-vmX@{{qEtKSZZ};54W~AAA6O z+Fkz%D0=xeI0ODCDDxb;r{t+~Af`|T6n#JK@H3$3y;T#OuxoYUQ|=N9lRjj&c- z+UOzrdyICBb`x!qRvk@F>h2-~7ifkiYY}_M@sqR*H1QWXo}yi{XQhpv;9S-!dY0qE zH1X}~IN?r2uSeZE=C$$GaMvlg;?6q`g)e@BcAB<_CVnQiLbdjo=0B&kXu?(JX>vSS zf=WQ~y_&lYK=HNvXoqQH*E2LZeugG-x=MSH_9#v4_6SYvBsRn4t!+F|Y;~C?$06F= zX<}c(f7c;CC5NVoU(V5RO?8fTk|u}nRdpPwPC)UCQ#6UUdueh^&~BxPkNp(w2<;+G zj`28{|QZLfYBr=&^5AwF2J-&SY?1h#2vzPU`^ZL^4($eg?mCN<9Wn*;SHhS9A?bNK! z>}}_HH=CN6SYKbSx8kCnq^*fCXqq@P*+dfhwZTQJ8`cijC+qDzV&YThF6dJxHmMit z#iBtkeYSjUT(6ll3zAqL)=8>EFE{D*gm+A@a^;)c3&N~EJt1p3re+&Sk*nD-)R%(T zPu4SgzFwWjLB#8NIyko6PS*8ox64Fc&fwYfL~yLGW^E6d%r8>=K=02EC^{N1_2Q;c zvqhe?1K+6QrtPf-Njf;O0xbl-ADY?;#_iO1KU!`FtNFnjb|_N4-Zrsr%43P5y$et8;U#_zIr=rrb!sy==J!z_mb2%DfSIT{PJQw@bfkod7eb-L=dtv zPuXQ?uPGW*Cv5iLlZuHcNBBL|zqtUPE5rbrTFEk@hqEa7rVY*SNZ1`prpz$Ko zUhM10yJF9iuGb9mo?i39!ek?Zxd$V6Dzyv*80{_^^u4U@7CM~N`2+Wh7;hSi&)dCb zC@!5QMXQbPmrgV%qvVO6OPU>hPp0G=$>z+eSA@BmOLUy%Z8Xm*JtU-b7({_Q@>^y& zPLlIhFZQCKspey^!CcEl*Q5vcPu3nhpuJ3I?Ig{cMV`rgqWLxPSj?W)_32zUiC8bp z5_GAH3_IjW%dk)!NaED`m{sP9JPqsB92a8rc?6LmWB#R`m;F zy;_7Jj%4u0D8bE7nswcDhAtyTN5+eD5idY{Fa`2iRm;+?6!xmrjXrhm{KC9Ek4#<9 zhNwGcm^Bmj#&y;;&0v*KPr5b+?dn*Vv6{ihOqi_K)yd$B@yq`*B1FGax^#DuBQRNt*^IO}(JiMw>|r<&3FE2#D&8Eq+Ks;wPO(NZjVd+}FZoEz{PZG3{S`p2a9MTmgTQ`ji zE7ui67nvmy$6<);jb7eG5LtB{Hv zZ*+6FAF0z>B4m!6;hjmEF_OEpo}?Z4oQ2laS+9tjZ5`u=L7e55gUfql1$EY}=4xR# zZ>R;6r->RR+KR#!gl%vh9+oGft%Up{-m&ZEZnqh9FA1US32Rk^w7?ivDyuu46#IfSSs>My2;d%1h!iCFo<;_hV_f4Xs+w|zV&{DZj?zc_57f~ z$(C*Ath{8|5R?9hpjs`PR)l{;L(4Lsm=peNktG<+=206pl^Ge)Ci`9a^Ue8xFrnl8 z!59@EdJRm=SWp->%j_`s+j7NHUHR0;uvlZn>N>XGk(E7Gy|pBT21a|hN0Ci~zFteh zLMlEP$A>V9zw%wNH1seGR4Y-^g(jU}4H#Bdj-?Ff{PIm)PBeH*fnsMK+^_`|(c&j2i}wxLd{hCI_|;S+yj%{JdQv`k zZtQZggS9dtbl4dLuUbyR01tVpfb>}|xe(?WkeOQU#kqH7N>5u)I5w`QB}ogC7X@r7 za7?Y3s7v*g21Iiv+LSeoXtFY_jeGmh=Dcx9XH4A1c`Zx3z;(`5S1UqMZ(L!CheZiE5Sdu4AGq0WYYZuGS!j!H2$cQx? z8j+V#d5kR?6r0x;vR2^Nju)-0wvtTgYR}T5f+`wywmlQnXKfRj)`ZlL)hJ_&=W9#E zg=E#*9I0zcAD%q)VC~Q$e)`ansl$g3PTn^;Id+!WYAeFpB)BA<(#u{W4RrsrF;&WR z7&NG%pXBMot%$-vCOpPFajQU6wH4zmo)D zS5I_9FNlxoX4^|q^vs2oleGti{cH{~X>C4kCgL7b`hiA}k1dAmbHW;RQIt*TxN9%7 znL|f)c{($BbnLNOxihm%Sxk&iRLRcm!(%tvLLM&&ka7drF_aq=otU^C^j~NMscy+O zDi35&7X`f}t=Gq9d+E+robPOf)E;!h?f&1klc7H2MFm^Q40Z2pQF8XT)BbkEUUJR! zJl)^U^8U8nPBy%q%@Emkw&)mp#6}0KHkOZg9l7*9wIZ)C8uo%I9+H;VP+@fqQD5k_ ziqK<6?Xgo0j%ly|0;}8EN;?DrY{E1?SCOY%{j1`bb#-F&t_X?hY<+mDe-(jaCROLz zZBaXSbSv0klQrk9q061EYn$qPntOU>XR9N#;`!Bsoh@NDww?N*{@?oaKj9(XwM{k` zVx+>!&)`y2QMZ4b|QL;08 zOC0FBx62C5F8}(0ozv;e>oNAOG2W`H-@h8SaPt1P-LpzDHz(!(J@~jDah{WKkVJGWafRLyg03 zuERc7qJw=GA2WzBw+GIHZa^tWz1+b(*wC6_R6w=Dw=dJdYjlUsF@x=Tyd2<25qBF! zSdbFSr`1N+kZM@*V0wIB+!T>ZB_>b{rlZb9#hn26i;+(Ee|6}JRzu>f8P7JDh8;Hq z(Ccp#MO_bD#Dq2Lk~)|FGDMwurt};^LP2HY)-Fz_8*1GJqB>9fp&`eESDA^xDYfBT zOP%RoMMyh_-{0oBjg&EUN{of7cN2xM2aj*Fg_U>}1y;)!zml>+GvD2@8_8u1j-Q$EKn3nf+VIB!?$6cf_r+GggBi`Lwr zo-@tPSKw&8r<`g3^&95{gVhNC`;}(O?-zy~M%uEf*k9EPgDrN(r%ra^^0D&(de4O} zsZcdZK*S!n;BEqEV_kU(iI4;ZCyoz&KQ~Z4v%X1(Wfx1-Hc_}}5-Zz8f$nCBpiuN` zl}!aVxRLD2A&4-_fu;G?y!bxWuy(#CwIk_$<#d?w5&tij{U_~}4`n%S zu_1@!B@(ovi&V;FHH%pTzW7-(bvyRm8Ojm)nf`YFh5j~_KRAPze1ZtN2+J%@2E6Ve zw08r4b_CjkVFshlW|SmDNk({Ukokgv`|gv3SS~AW+4T_3xixaV+&c$Ou7Ncm3d(5P zWfu|P{0Q0KCZudhf?6!C_av!Y+oV9|hHvoLqzqKi9WFJRnq9<+S5}=HmM0hz&(Xu@ zd?P6}U8Wb|wJB}wj4G-iC&jxoYtVMW_EDJ2vVQ3kHe+|>L80cMbS;9~jEovAV^(Q) zNH;+~rI#G}S@k(`2|~7-f%9GASLRM3#1v;E371(WHQ356X*5j@LtB){+IKej(i*l3 z5tt&&77;cBdmCPu+a!Wwq0bEu?mSc0B6zcNvYoGxpPE7FXusO-qgeMv#7iL<(SMbg zOKurfSJ&4cH?F$ebeD^cM3+^kOMFCnRs2@jPu-xBtco$TRn{|u>`XMW_R2lV8o|b} z#A(%eS<0J=bVl8&jP|PAaVdp)jeHtJmh%|e>*4Vvd!dD@qpgLl;khp{S(UT1Qt@^w z2ZRzv!V#_*^s9(v#ItwLa8kaX+8IV1m9l+93NPREq@qS5n_F?O;m9_9#1Ta!JJlQX z7$|P!twGgN32w_?o69IgBCjf5S!p%mt|`ma2hojkR{swPOw1~EtJ@ebKdfQvnHE(+&)CAMZ?r_MvwwkSS4Lo00ZUf3ki~j+XFKnFv literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/uk_UA/LC_MESSAGES/dashtodock.mo b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/uk_UA/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000000000000000000000000000000000000..a475c96e2e0d6cc93a45fbc49eaf6d316920e592 GIT binary patch literal 15770 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/zh_CN/LC_MESSAGES/dashtodock.mo b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/zh_CN/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000000000000000000000000000000000000..8ebfc33144988cb5c24ad3c49b1f6703146f0b7b GIT binary patch literal 9442 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 Nm6MV3S`SaM{{^Jo_GSP8 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/zh_TW/LC_MESSAGES/dashtodock.mo b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locale/zh_TW/LC_MESSAGES/dashtodock.mo new file mode 100644 index 0000000000000000000000000000000000000000..3ca257aa96892bf4a59b1901ca21357fe3b9493b GIT binary patch literal 9484 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locations.js b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locations.js new file mode 100644 index 0000000..7373335 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/locations.js @@ -0,0 +1,660 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; +const Shell = imports.gi.Shell; +const Signals = imports.signals; + +// Use __ () and N__() for the extension gettext domain, and reuse +// the shell domain with the default _() and N_() +const Gettext = imports.gettext.domain('dashtodock'); +const __ = Gettext.gettext; +const N__ = function(e) { return e }; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Docking = Me.imports.docking; +const Utils = Me.imports.utils; + +const FILE_MANAGER_DESKTOP_APP_ID = 'org.gnome.Nautilus.desktop'; +const TRASH_URI = 'trash://'; +const UPDATE_TRASH_DELAY = 500; + +const NautilusFileOperations2Interface = '\ + \ + \ + \ + \ + \ + \ +'; + +const NautilusFileOperations2ProxyInterface = Gio.DBusProxy.makeProxyWrapper(NautilusFileOperations2Interface); + +function makeNautilusFileOperationsProxy() { + const proxy = new NautilusFileOperations2ProxyInterface( + Gio.DBus.session, + 'org.gnome.Nautilus', + '/org/gnome/Nautilus/FileOperations2', (_p, error) => { + if (error) + logError(error, 'Error connecting to Nautilus'); + } + ); + + proxy.platformData = params => { + const defaultParams = { + parentHandle: '', + timestamp: global.get_current_time(), + windowPosition: 'center', + }; + const { parentHandle, timestamp, windowPosition } = { + ...defaultParams, + ...params, + }; + + return { + 'parent-handle': new GLib.Variant('s', parentHandle), + 'timestamp': new GLib.Variant('u', timestamp), + 'window-position': new GLib.Variant('s', windowPosition), + }; + }; + + return proxy; +} + +function wrapWindowsBackedApp(shellApp) { + if (shellApp._dtdData) + throw new Error('%s has been already wrapped'.format(shellApp)); + + shellApp._dtdData = { + windows: [], + methodInjections: new Utils.InjectionsHandler(), + propertyInjections: new Utils.PropertyInjectionsHandler(), + destroy: function () { + this.windows = []; + this.methodInjections.destroy(); + this.propertyInjections.destroy(); + } + }; + + const m = (...args) => shellApp._dtdData.methodInjections.add(shellApp, ...args); + const p = (...args) => shellApp._dtdData.propertyInjections.add(shellApp, ...args); + shellApp._mi = m; + shellApp._pi = p; + + m('get_state', () => + shellApp.get_windows().length ? Shell.AppState.RUNNING : Shell.AppState.STOPPED); + p('state', { get: () => shellApp.get_state() }); + + m('get_windows', () => shellApp._dtdData.windows); + m('get_n_windows', () => shellApp.get_windows().length); + m('get_pids', () => shellApp.get_windows().reduce((pids, w) => { + if (w.get_pid() > 0 && !pids.includes(w.get_pid())) + pids.push(w.get_pid()); + return pids; + }, [])); + m('is_on_workspace', (_om, workspace) => shellApp.get_windows().some(w => + w.get_workspace() === workspace)); + m('request_quit', () => shellApp.get_windows().filter(w => + w.can_close()).forEach(w => w.delete(global.get_current_time()))); + + shellApp._updateWindows = function () { + throw new GObject.NotImplementedError(`_updateWindows in ${this.constructor.name}`); + }; + + let updateWindowsIdle = GLib.idle_add(GLib.DEFAULT_PRIORITY, () => { + shellApp._updateWindows(); + updateWindowsIdle = undefined; + return GLib.SOURCE_REMOVE; + }); + + const windowTracker = Shell.WindowTracker.get_default(); + shellApp._checkFocused = function () { + if (this.get_windows().some(w => w.has_focus())) { + this.isFocused = true; + windowTracker.notify('focus-app'); + } else if (this.isFocused) { + this.isFocused = false; + windowTracker.notify('focus-app'); + } + } + + shellApp._checkFocused(); + const focusWindowNotifyId = global.display.connect('notify::focus-window', () => + shellApp._checkFocused()); + + // Re-implements shell_app_activate_window for generic activation and alt-tab support + m('activate_window', function (_om, window, timestamp) { + if (!window) + [window] = this.get_windows(); + else if (!this.get_windows().includes(window)) + return; + + const currentWorkspace = global.workspace_manager.get_active_workspace(); + const workspace = window.get_workspace(); + const sameWorkspaceWindows = this.get_windows().filter(w => + w.get_workspace() === workspace); + sameWorkspaceWindows.forEach(w => w.raise()); + + if (workspace !== currentWorkspace) + workspace.activate_with_focus(window, timestamp); + else + window.activate(timestamp); + }); + + // Re-implements shell_app_activate_full for generic activation and dash support + m('activate_full', function (_om, workspace, timestamp) { + if (!timestamp) + timestamp = global.get_current_time(); + + switch (this.state) { + case Shell.AppState.STOPPED: + try { + this.launch(timestamp, workspace, Shell.AppLaunchGpu.APP_PREF); + } catch (e) { + global.notify_error(__("Failed to launch “%s”".format( + this.get_name())), e.message); + } + break; + case Shell.AppState.RUNNING: + this.activate_window(null, timestamp); + break; + } + }); + + m('activate', () => shellApp.activate_full(-1, 0)); + + m('compare', (_om, other) => shellAppCompare(shellApp, other)); + + shellApp.destroy = function() { + global.display.disconnect(focusWindowNotifyId); + updateWindowsIdle && GLib.source_remove(updateWindowsIdle); + this._dtdData.destroy(); + this._dtdData = undefined; + this.destroy = undefined; + } + + return shellApp; +} + +// We can't inherit from Shell.App as it's a final type, so let's patch it +function makeLocationApp(params) { + if (!params.location) + throw new TypeError('Invalid location'); + + location = params.location; + delete params.location; + + const shellApp = new Shell.App(params); + wrapWindowsBackedApp(shellApp); + shellApp.appInfo.customId = 'location:%s'.format(location); + + Object.defineProperties(shellApp, { + location: { value: location }, + isTrash: { value: location.startsWith(TRASH_URI) }, + }); + + shellApp._mi('toString', defaultToString => + '[LocationApp - %s]'.format(defaultToString.call(shellApp))); + + // FIXME: We need to add a new API to Nautilus to open new windows + shellApp._mi('can_open_new_window', () => false); + + const { fm1Client } = Docking.DockManager.getDefault(); + shellApp._updateWindows = function () { + const oldState = this.state; + const oldWindows = this.get_windows(); + this._dtdData.windows = fm1Client.getWindows(this.location); + + if (this.get_windows().length !== oldWindows.length || + this.get_windows().some((win, index) => win !== oldWindows[index])) + this.emit('windows-changed'); + + if (oldState !== this.state) { + Shell.AppSystem.get_default().emit('app-state-changed', this); + this.notify('state'); + this._checkFocused(); + } + }; + + const windowsChangedId = fm1Client.connect('windows-changed', () => + shellApp._updateWindows()); + + const parentDestroy = shellApp.destroy; + shellApp.destroy = function () { + fm1Client.disconnect(windowsChangedId); + parentDestroy.call(this); + } + + return shellApp; +} + +function getFileManagerApp() { + return Shell.AppSystem.get_default().lookup_app(FILE_MANAGER_DESKTOP_APP_ID); +} + +function wrapWindowsManagerApp() { + const fileManagerApp = getFileManagerApp(); + if (!fileManagerApp) + return null; + + if (fileManagerApp._dtdData) + return fileManagerApp; + + const originalGetWindows = fileManagerApp.get_windows; + wrapWindowsBackedApp(fileManagerApp); + + const { fm1Client } = Docking.DockManager.getDefault(); + const windowsChangedId = fileManagerApp.connect('windows-changed', () => + fileManagerApp._updateWindows()); + const fm1WindowsChangedId = fm1Client.connect('windows-changed', () => + fileManagerApp._updateWindows()); + + fileManagerApp._updateWindows = function () { + const oldState = this.state; + const oldWindows = this.get_windows(); + const locationWindows = []; + getRunningApps().forEach(a => locationWindows.push(...a.get_windows())); + this._dtdData.windows = originalGetWindows.call(this).filter(w => + !locationWindows.includes(w)); + + if (this.get_windows().length !== oldWindows.length || + this.get_windows().some((win, index) => win !== oldWindows[index])) { + this.block_signal_handler(windowsChangedId); + this.emit('windows-changed'); + this.unblock_signal_handler(windowsChangedId); + } + + if (oldState !== this.state) { + Shell.AppSystem.get_default().emit('app-state-changed', this); + this.notify('state'); + this._checkFocused(); + } + }; + + fileManagerApp._mi('toString', defaultToString => + '[FileManagerApp - %s]'.format(defaultToString.call(fileManagerApp))); + + const parentDestroy = fileManagerApp.destroy; + fileManagerApp.destroy = function () { + fileManagerApp.disconnect(windowsChangedId); + fm1Client.disconnect(fm1WindowsChangedId); + parentDestroy.call(this); + } + + return fileManagerApp; +} + +function unWrapWindowsManagerApp() { + const fileManagerApp = getFileManagerApp(); + if (!fileManagerApp || !fileManagerApp._dtdData) + return; + + fileManagerApp.destroy(); +} + +// Re-implements shell_app_compare so that can be used to resort running apps +function shellAppCompare(app, other) { + if (app.state !== other.state) { + if (app.state === Shell.AppState.RUNNING) + return -1; + return 1; + } + + const windows = app.get_windows(); + const otherWindows = other.get_windows(); + + const isMinimized = windows => !windows.some(w => w.showing_on_its_workspace()); + const otherMinimized = isMinimized(otherWindows); + if (isMinimized(windows) != otherMinimized) { + if (otherMinimized) + return -1; + return 1; + } + + if (app.state === Shell.AppState.RUNNING) { + if (windows.length && !otherWindows.length) + return -1; + else if (!windows.length && otherWindows.length) + return 1; + + const lastUserTime = windows => + Math.max(...windows.map(w => w.get_user_time())); + return lastUserTime(otherWindows) - lastUserTime(windows); + } + + return 0; +} + +/** + * This class maintains a Shell.App representing the Trash and keeps it + * up-to-date as the trash fills and is emptied over time. + */ +var Trash = class DashToDock_Trash { + _promisified = false; + + static initPromises() { + if (Trash._promisified) + return; + + Gio._promisify(Gio.FileEnumerator.prototype, 'close_async', 'close_finish'); + Gio._promisify(Gio.FileEnumerator.prototype, 'next_files_async', 'next_files_finish'); + Gio._promisify(Gio.file_new_for_uri(TRASH_URI).constructor.prototype, + 'enumerate_children_async', 'enumerate_children_finish'); + Trash._promisified = true; + } + + constructor() { + Trash.initPromises(); + this._cancellable = new Gio.Cancellable(); + this._file = Gio.file_new_for_uri(TRASH_URI); + try { + this._monitor = this._file.monitor_directory(0, this._cancellable); + this._signalId = this._monitor.connect( + 'changed', + this._onTrashChange.bind(this) + ); + } catch (e) { + if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + return; + logError(e, 'Impossible to monitor trash'); + } + this._empty = true; + this._schedUpdateId = 0; + this._updateTrash(); + } + + destroy() { + this._cancellable.cancel(); + this._cancellable = null; + this._monitor?.disconnect(this._signalId); + this._monitor = null; + this._file = null; + this._trashApp?.destroy(); + } + + _onTrashChange() { + if (this._schedUpdateId) { + GLib.source_remove(this._schedUpdateId); + } + this._schedUpdateId = GLib.timeout_add( + GLib.PRIORITY_LOW, UPDATE_TRASH_DELAY, () => { + this._schedUpdateId = 0; + this._updateTrash(); + return GLib.SOURCE_REMOVE; + }); + } + + async _updateTrash() { + try { + const priority = GLib.PRIORITY_LOW; + const cancellable = this._cancellable; + const childrenEnumerator = await this._file.enumerate_children_async( + Gio.FILE_ATTRIBUTE_STANDARD_TYPE, Gio.FileQueryInfoFlags.NONE, + priority, cancellable); + const children = await childrenEnumerator.next_files_async(1, + priority, cancellable); + this._empty = !children.length; + this._ensureApp(); + + await childrenEnumerator.close_async(priority, null); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e, 'Impossible to enumerate trash children'); + } + } + + _ensureApp() { + if (this._trashApp == null || + this._lastEmpty !== this._empty) { + let trashKeys = new GLib.KeyFile(); + trashKeys.set_string('Desktop Entry', 'Name', __('Trash')); + trashKeys.set_string('Desktop Entry', 'Icon', + this._empty ? 'user-trash' : 'user-trash-full'); + trashKeys.set_string('Desktop Entry', 'Type', 'Application'); + trashKeys.set_string('Desktop Entry', 'Exec', 'gio open %s'.format(TRASH_URI)); + trashKeys.set_string('Desktop Entry', 'StartupNotify', 'false'); + if (!this._empty) { + trashKeys.set_string('Desktop Entry', 'Actions', 'empty-trash;'); + trashKeys.set_string('Desktop Action empty-trash', 'Name', __('Empty Trash')); + trashKeys.set_string('Desktop Action empty-trash', 'Exec', 'true'); + } + + let trashAppInfo = Gio.DesktopAppInfo.new_from_keyfile(trashKeys); + this._trashApp?.destroy(); + this._trashApp = makeLocationApp({ + location: TRASH_URI + '/', + appInfo: trashAppInfo, + }); + + if (!this._empty) { + this._trashApp._mi('launch_action', + (launchAction, actionName, timestamp, ...args) => { + if (actionName === 'empty-trash') { + const nautilus = makeNautilusFileOperationsProxy(); + const askConfirmation = true; + nautilus.EmptyTrashRemote(askConfirmation, + nautilus.platformData({ timestamp }), (_p, error) => { + if (error) + logError(error, 'Empty trash failed'); + }); + return; + } + + return launchAction.call(this, actionName, timestamp, ...args); + }); + } + this._lastEmpty = this._empty; + + this.emit('changed'); + } + } + + getApp() { + this._ensureApp(); + return this._trashApp; + } +} +Signals.addSignalMethods(Trash.prototype); + +/** + * This class maintains Shell.App representations for removable devices + * plugged into the system, and keeps the list of Apps up-to-date as + * devices come and go and are mounted and unmounted. + */ +var Removables = class DashToDock_Removables { + + constructor() { + this._signalsHandler = new Utils.GlobalSignalsHandler(); + + this._monitor = Gio.VolumeMonitor.get(); + this._volumeApps = [] + this._mountApps = [] + + this._monitor.get_volumes().forEach( + (volume) => { + this._onVolumeAdded(this._monitor, volume); + } + ); + + this._monitor.get_mounts().forEach( + (mount) => { + this._onMountAdded(this._monitor, mount); + } + ); + + this._signalsHandler.add([ + this._monitor, + 'mount-added', + this._onMountAdded.bind(this) + ], [ + this._monitor, + 'mount-removed', + this._onMountRemoved.bind(this) + ], [ + this._monitor, + 'volume-added', + this._onVolumeAdded.bind(this) + ], [ + this._monitor, + 'volume-removed', + this._onVolumeRemoved.bind(this) + ]); + } + + destroy() { + this._signalsHandler.destroy(); + this._monitor.run_dispose(); + } + + _getWorkingIconName(icon) { + if (icon instanceof Gio.EmblemedIcon) { + icon = icon.get_icon(); + } + if (icon instanceof Gio.ThemedIcon) { + const { iconTheme } = Docking.DockManager.getDefault(); + let names = icon.get_names(); + for (let i = 0; i < names.length; i++) { + let iconName = names[i]; + if (iconTheme.has_icon(iconName)) { + return iconName; + } + } + return ''; + } else { + return icon.to_string(); + } + } + + _onVolumeAdded(monitor, volume) { + if (!volume.can_mount()) { + return; + } + + if (volume.get_identifier('class') == 'network') { + return; + } + + let activationRoot = volume.get_activation_root(); + if (!activationRoot) { + // Can't offer to mount a device if we don't know + // where to mount it. + // These devices are usually ejectable so you + // don't normally unmount them anyway. + return; + } + + let escapedUri = activationRoot.get_uri() + let uri = GLib.uri_unescape_string(escapedUri, null); + + let volumeKeys = new GLib.KeyFile(); + volumeKeys.set_string('Desktop Entry', 'Name', volume.get_name()); + volumeKeys.set_string('Desktop Entry', 'Icon', this._getWorkingIconName(volume.get_icon())); + volumeKeys.set_string('Desktop Entry', 'Type', 'Application'); + volumeKeys.set_string('Desktop Entry', 'Exec', 'gio open "' + uri + '"'); + volumeKeys.set_string('Desktop Entry', 'StartupNotify', 'false'); + volumeKeys.set_string('Desktop Entry', 'Actions', 'mount;'); + volumeKeys.set_string('Desktop Action mount', 'Name', __('Mount')); + volumeKeys.set_string('Desktop Action mount', 'Exec', 'gio mount "' + uri + '"'); + let volumeAppInfo = Gio.DesktopAppInfo.new_from_keyfile(volumeKeys); + const volumeApp = makeLocationApp({ + location: escapedUri, + appInfo: volumeAppInfo, + }); + this._volumeApps.push(volumeApp); + this.emit('changed'); + } + + _onVolumeRemoved(monitor, volume) { + for (let i = 0; i < this._volumeApps.length; i++) { + let app = this._volumeApps[i]; + if (app.get_name() == volume.get_name()) { + const [volumeApp] = this._volumeApps.splice(i, 1); + volumeApp.destroy(); + } + } + this.emit('changed'); + } + + _onMountAdded(monitor, mount) { + // Filter out uninteresting mounts + if (!mount.can_eject() && !mount.can_unmount()) + return; + if (mount.is_shadowed()) + return; + + let volume = mount.get_volume(); + if (!volume || volume.get_identifier('class') == 'network') { + return; + } + + const escapedUri = mount.get_default_location().get_uri() + let uri = GLib.uri_unescape_string(escapedUri, null); + + let mountKeys = new GLib.KeyFile(); + mountKeys.set_string('Desktop Entry', 'Name', mount.get_name()); + mountKeys.set_string('Desktop Entry', 'Icon', + this._getWorkingIconName(volume.get_icon())); + mountKeys.set_string('Desktop Entry', 'Type', 'Application'); + mountKeys.set_string('Desktop Entry', 'Exec', 'gio open "' + uri + '"'); + mountKeys.set_string('Desktop Entry', 'StartupNotify', 'false'); + mountKeys.set_string('Desktop Entry', 'Actions', 'unmount;'); + if (mount.can_eject()) { + mountKeys.set_string('Desktop Action unmount', 'Name', __('Eject')); + mountKeys.set_string('Desktop Action unmount', 'Exec', + 'gio mount -e "' + uri + '"'); + } else { + mountKeys.set_string('Desktop Entry', 'Actions', 'unmount;'); + mountKeys.set_string('Desktop Action unmount', 'Name', __('Unmount')); + mountKeys.set_string('Desktop Action unmount', 'Exec', + 'gio mount -u "' + uri + '"'); + } + let mountAppInfo = Gio.DesktopAppInfo.new_from_keyfile(mountKeys); + const mountApp = makeLocationApp({ + appInfo: mountAppInfo, + location: escapedUri, + }); + this._mountApps.push(mountApp); + this.emit('changed'); + } + + _onMountRemoved(monitor, mount) { + for (let i = 0; i < this._mountApps.length; i++) { + let app = this._mountApps[i]; + if (app.get_name() == mount.get_name()) { + const [mountApp] = this._mountApps.splice(i, 1); + mountApp.destroy(); + } + } + this.emit('changed'); + } + + getApps() { + // When we have both a volume app and a mount app, we prefer + // the mount app. + let apps = new Map(); + this._volumeApps.map(function(app) { + apps.set(app.get_name(), app); + }); + this._mountApps.map(function(app) { + apps.set(app.get_name(), app); + }); + + return [...apps.values()]; + } +} +Signals.addSignalMethods(Removables.prototype); + +function getRunningApps() { + const dockManager = Docking.DockManager.getDefault(); + const locationApps = []; + + if (dockManager.removables) + locationApps.push(...dockManager.removables.getApps()); + + if (dockManager.trash) + locationApps.push(dockManager.trash.getApp()); + + return locationApps.filter(a => a.state === Shell.AppState.RUNNING); +} diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/media/glossy.svg b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/media/glossy.svg new file mode 100644 index 0000000..55b71ba --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/media/glossy.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/media/highlight_stacked_bg.svg b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/media/highlight_stacked_bg.svg new file mode 100644 index 0000000..19be5a9 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/media/highlight_stacked_bg.svg @@ -0,0 +1,82 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/media/highlight_stacked_bg_h.svg b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/media/highlight_stacked_bg_h.svg new file mode 100644 index 0000000..eeaa869 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/media/highlight_stacked_bg_h.svg @@ -0,0 +1,82 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/media/logo.svg b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/media/logo.svg new file mode 100644 index 0000000..eebd0b1 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/media/logo.svg @@ -0,0 +1,528 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dash to Dock + Michele + + + + + + + + + + + + + + + + + + + + + + diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/metadata.json b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/metadata.json new file mode 100644 index 0000000..5aa5350 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/metadata.json @@ -0,0 +1,14 @@ +{ + "_generated": "Generated by SweetTooth, do not edit", + "description": "A dock for the Gnome Shell. This extension moves the dash out of the overview transforming it in a dock for an easier launching of applications and a faster switching between windows and desktops. Side and bottom placement options are available.", + "gettext-domain": "dashtodock", + "name": "Dash to Dock", + "original-author": "micxgx@gmail.com", + "shell-version": [ + "40", + "41" + ], + "url": "https://micheleg.github.io/dash-to-dock/", + "uuid": "dash-to-dock@micxgx.gmail.com", + "version": 71 +} \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/prefs.js b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/prefs.js new file mode 100644 index 0000000..532eef4 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/prefs.js @@ -0,0 +1,1029 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +imports.gi.versions.Gtk = '4.0'; +imports.gi.versions.Gdk = '4.0'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; +const Gdk = imports.gi.Gdk; +const Signals = imports.signals; + +// Use __ () and N__() for the extension gettext domain, and reuse +// the shell domain with the default _() and N_() +const Gettext = imports.gettext.domain('dashtodock'); +const __ = Gettext.gettext; +const N__ = function (e) { return e }; + +try { + imports.misc.extensionUtils; +} catch (e) { + const resource = Gio.Resource.load( + '/usr/share/gnome-shell/org.gnome.Extensions.src.gresource'); + resource._register(); + imports.searchPath.push('resource:///org/gnome/Extensions/js'); +} + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); + +const SCALE_UPDATE_TIMEOUT = 500; +const DEFAULT_ICONS_SIZES = [128, 96, 64, 48, 32, 24, 16]; + +const TransparencyMode = { + DEFAULT: 0, + FIXED: 1, + DYNAMIC: 3 +}; + +const RunningIndicatorStyle = { + DEFAULT: 0, + DOTS: 1, + SQUARES: 2, + DASHES: 3, + SEGMENTED: 4, + SOLID: 5, + CILIORA: 6, + METRO: 7 +}; + +class MonitorsConfig { + static XML_INTERFACE = + '\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + '; + + static ProxyWrapper = Gio.DBusProxy.makeProxyWrapper(MonitorsConfig.XML_INTERFACE); + + constructor() { + this._monitorsConfigProxy = new MonitorsConfig.ProxyWrapper( + Gio.DBus.session, + "org.gnome.Mutter.DisplayConfig", + "/org/gnome/Mutter/DisplayConfig" + ); + + // Connecting to a D-Bus signal + this._monitorsConfigProxy.connectSignal("MonitorsChanged", + () => this._updateResources()); + + this._primaryMonitor = null; + this._monitors = []; + this._logicalMonitors = []; + + this._updateResources(); + } + + _updateResources() { + this._monitorsConfigProxy.GetCurrentStateRemote((resources, err) => { + if (err) { + logError(err); + return; + } + + const [_serial, monitors, logicalMonitors] = resources; + let index = 0; + for (const monitor of monitors) { + const [monitorSpecs, _modes, props] = monitor; + const [connector, vendor, product, serial] = monitorSpecs; + this._monitors.push({ + index: index++, + active: false, + connector, vendor, product, serial, + displayName: props['display-name'].unpack(), + }); + } + + for (const logicalMonitor of logicalMonitors) { + const [_x, _y, _scale, _transform, isPrimary, monitorsSpecs] = + logicalMonitor; + + // We only care about the first one really + for (const monitorSpecs of monitorsSpecs) { + const [connector, vendor, product, serial] = monitorSpecs; + const monitor = this._monitors.find(m => + m.connector === connector && m.vendor === vendor && + m.product === product && m.serial === serial); + + if (monitor) { + monitor.active = true; + monitor.isPrimary = isPrimary; + if (monitor.isPrimary) + this._primaryMonitor = monitor; + break; + } + } + } + + const activeMonitors = this._monitors.filter(m => m.active); + if (activeMonitors.length > 1 && logicalMonitors.length == 1) { + // We're in cloning mode, so let's just activate the primary monitor + this._monitors.forEach(m => (m.active = false)); + this._primaryMonitor.active = true; + } + + this._updateMonitorsIndexes(); + this.emit('updated'); + }); + } + + _updateMonitorsIndexes() { + // This function ensures that we follow the old Gdk indexing strategy + // for monitors, it can be removed when we don't care about breaking + // old user configurations or external apps configuring this extension + // such as ubuntu's gnome-control-center. + const { index: primaryMonitorIndex } = this._primaryMonitor; + for (const monitor of this._monitors) { + let { index } = monitor; + // The The dock uses the Gdk index for monitors, where the primary monitor + // always has index 0, so let's follow what dash-to-dock does in docking.js + // (as part of _createDocks), but using inverted math + index -= primaryMonitorIndex; + + if (index < 0) + index += this._monitors.length; + + monitor.index = index; + } + } + + get primaryMonitor() { + return this._primaryMonitor; + } + + get monitors() { + return this._monitors; + } +} +Signals.addSignalMethods(MonitorsConfig.prototype); + +function setShortcut(settings) { + const shortcutText = settings.get_string('shortcut-text'); + const [success, key, mods] = Gtk.accelerator_parse(shortcutText); + + if (success && Gtk.accelerator_valid(key, mods)) { + let shortcut = Gtk.accelerator_name(key, mods); + settings.set_strv('shortcut', [shortcut]); + } else { + settings.set_strv('shortcut', []); + } +} + +var Settings = GObject.registerClass({ + Implements: [Gtk.BuilderScope], +}, class DashToDock_Settings extends GObject.Object { + + _init() { + super._init(); + + if (Me) + this._settings = ExtensionUtils.getSettings('org.gnome.shell.extensions.dash-to-dock'); + else + this._settings = new Gio.Settings({schema_id: 'org.gnome.shell.extensions.dash-to-dock'}); + + this._rtl = (Gtk.Widget.get_default_direction() == Gtk.TextDirection.RTL); + + this._builder = new Gtk.Builder(); + this._builder.set_scope(this); + if (Me) { + this._builder.set_translation_domain(Me.metadata['gettext-domain']); + this._builder.add_from_file(Me.path + '/Settings.ui'); + } else { + this._builder.add_from_file('./Settings.ui'); + } + + this.widget = new Gtk.ScrolledWindow({ hscrollbar_policy: Gtk.PolicyType.NEVER }); + this._notebook = this._builder.get_object('settings_notebook'); + this.widget.set_child(this._notebook); + + // Set a reasonable initial window height + this.widget.connect('realize', () => { + const window = this.widget.get_root(); + window.set_size_request(-1, 750); + }); + + // Timeout to delay the update of the settings + this._dock_size_timeout = 0; + this._icon_size_timeout = 0; + this._opacity_timeout = 0; + + this._monitorsConfig = new MonitorsConfig(); + this._bindSettings(); + } + + vfunc_create_closure(builder, handlerName, flags, connectObject) { + if (flags & Gtk.BuilderClosureFlags.SWAPPED) + throw new Error('Unsupported template signal flag "swapped"'); + + if (typeof this[handlerName] === 'undefined') + throw new Error(`${handlerName} is undefined`); + + return this[handlerName].bind(connectObject || this); + } + + dock_display_combo_changed_cb(combo) { + if (!this._monitors?.length) + return; + + const preferredMonitor = this._monitors[combo.get_active()].index; + this._settings.set_int('preferred-monitor', preferredMonitor); + } + + position_top_button_toggled_cb(button) { + if (button.get_active()) + this._settings.set_enum('dock-position', 0); + } + + position_right_button_toggled_cb(button) { + if (button.get_active()) + this._settings.set_enum('dock-position', 1); + } + + position_bottom_button_toggled_cb(button) { + if (button.get_active()) + this._settings.set_enum('dock-position', 2); + } + + position_left_button_toggled_cb(button) { + if (button.get_active()) + this._settings.set_enum('dock-position', 3); + } + + icon_size_combo_changed_cb(combo) { + this._settings.set_int('dash-max-icon-size', this._allIconSizes[combo.get_active()]); + } + + dock_size_scale_value_changed_cb(scale) { + // Avoid settings the size continuously + if (this._dock_size_timeout > 0) + GLib.source_remove(this._dock_size_timeout); + const id = this._dock_size_timeout = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, SCALE_UPDATE_TIMEOUT, () => { + if (id === this._dock_size_timeout) { + this._settings.set_double('height-fraction', scale.get_value()); + this._dock_size_timeout = 0; + return GLib.SOURCE_REMOVE; + } + }); + } + + icon_size_scale_value_changed_cb(scale) { + // Avoid settings the size consinuosly + if (this._icon_size_timeout > 0) + GLib.source_remove(this._icon_size_timeout); + this._icon_size_timeout = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, SCALE_UPDATE_TIMEOUT, () => { + log(scale.get_value()); + this._settings.set_int('dash-max-icon-size', scale.get_value()); + this._icon_size_timeout = 0; + return GLib.SOURCE_REMOVE; + }); + } + preview_size_scale_format_value_cb(scale, value) { + return value == 0 ? 'auto' : value; + } + preview_size_scale_value_changed_cb(scale) { + this._settings.set_double('preview-size-scale', scale.get_value()); + } + custom_opacity_scale_value_changed_cb(scale) { + // Avoid settings the opacity consinuosly as it's change is animated + if (this._opacity_timeout > 0) + GLib.source_remove(this._opacity_timeout); + this._opacity_timeout = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, SCALE_UPDATE_TIMEOUT, () => { + this._settings.set_double('background-opacity', scale.get_value()); + this._opacity_timeout = 0; + return GLib.SOURCE_REMOVE; + }); + } + min_opacity_scale_value_changed_cb(scale) { + // Avoid settings the opacity consinuosly as it's change is animated + if (this._opacity_timeout > 0) + GLib.source_remove(this._opacity_timeout); + this._opacity_timeout = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, SCALE_UPDATE_TIMEOUT, () => { + this._settings.set_double('min-alpha', scale.get_value()); + this._opacity_timeout = 0; + return GLib.SOURCE_REMOVE; + }); + } + max_opacity_scale_value_changed_cb(scale) { + // Avoid settings the opacity consinuosly as it's change is animated + if (this._opacity_timeout > 0) + GLib.source_remove(this._opacity_timeout); + this._opacity_timeout = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, SCALE_UPDATE_TIMEOUT, () => { + this._settings.set_double('max-alpha', scale.get_value()); + this._opacity_timeout = 0; + return GLib.SOURCE_REMOVE; + }); + } + + all_windows_radio_button_toggled_cb(button) { + if (button.get_active()) + this._settings.set_enum('intellihide-mode', 0); + } + focus_application_windows_radio_button_toggled_cb(button) { + if (button.get_active()) + this._settings.set_enum('intellihide-mode', 1); + } + maximized_windows_radio_button_toggled_cb(button) { + if (button.get_active()) + this._settings.set_enum('intellihide-mode', 2); + } + + _updateMonitorsSettings() { + // Monitor options + const preferredMonitor = this._settings.get_int('preferred-monitor'); + const dockMonitorCombo = this._builder.get_object('dock_monitor_combo'); + + this._monitors = []; + dockMonitorCombo.remove_all(); + + // Add connected monitors + for (const monitor of this._monitorsConfig.monitors) { + if (!monitor.active && monitor.index !== preferredMonitor) + continue; + + if (monitor.isPrimary) { + dockMonitorCombo.append_text( + /* Translators: This will be followed by Display Name - Connector. */ + __('Primary monitor: ') + monitor.displayName + ' - ' + + monitor.connector); + } else { + dockMonitorCombo.append_text( + /* Translators: Followed by monitor index, Display Name - Connector. */ + __('Secondary monitor ') + (monitor.index + 1) + ' - ' + + monitor.displayName + ' - ' + monitor.connector); + } + + this._monitors.push(monitor); + + if (monitor.index === preferredMonitor) + dockMonitorCombo.set_active(this._monitors.length - 1); + } + } + + _bindSettings() { + // Position and size panel + + this._updateMonitorsSettings(); + this._monitorsConfig.connect('updated', () => this._updateMonitorsSettings()); + + // Position option + let position = this._settings.get_enum('dock-position'); + + switch (position) { + case 0: + this._builder.get_object('position_top_button').set_active(true); + break; + case 1: + this._builder.get_object('position_right_button').set_active(true); + break; + case 2: + this._builder.get_object('position_bottom_button').set_active(true); + break; + case 3: + this._builder.get_object('position_left_button').set_active(true); + break; + } + + if (this._rtl) { + /* Left is Right in rtl as a setting */ + this._builder.get_object('position_left_button').set_label(__('Right')); + this._builder.get_object('position_right_button').set_label(__('Left')); + } + + // Intelligent autohide options + this._settings.bind('dock-fixed', + this._builder.get_object('intelligent_autohide_switch'), + 'active', + Gio.SettingsBindFlags.INVERT_BOOLEAN); + this._settings.bind('dock-fixed', + this._builder.get_object('intelligent_autohide_button'), + 'sensitive', + Gio.SettingsBindFlags.INVERT_BOOLEAN); + this._settings.bind('autohide', + this._builder.get_object('autohide_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('autohide-in-fullscreen', + this._builder.get_object('autohide_enable_in_fullscreen_checkbutton'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('require-pressure-to-show', + this._builder.get_object('require_pressure_checkbutton'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('intellihide', + this._builder.get_object('intellihide_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('animation-time', + this._builder.get_object('animation_duration_spinbutton'), + 'value', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('hide-delay', + this._builder.get_object('hide_timeout_spinbutton'), + 'value', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-delay', + this._builder.get_object('show_timeout_spinbutton'), + 'value', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('pressure-threshold', + this._builder.get_object('pressure_threshold_spinbutton'), + 'value', + Gio.SettingsBindFlags.DEFAULT); + + //this._builder.get_object('animation_duration_spinbutton').set_value(this._settings.get_double('animation-time')); + + // Create dialog for intelligent autohide advanced settings + this._builder.get_object('intelligent_autohide_button').connect('clicked', () => { + + let dialog = new Gtk.Dialog({ + title: __('Intelligent autohide customization'), + transient_for: this.widget.get_root(), + use_header_bar: true, + modal: true + }); + + // GTK+ leaves positive values for application-defined response ids. + // Use +1 for the reset action + dialog.add_button(__('Reset to defaults'), 1); + + let box = this._builder.get_object('intelligent_autohide_advanced_settings_box'); + dialog.get_content_area().append(box); + + this._settings.bind('intellihide', + this._builder.get_object('intellihide_mode_box'), + 'sensitive', + Gio.SettingsBindFlags.GET); + + // intellihide mode + + let intellihideModeRadioButtons = [ + this._builder.get_object('all_windows_radio_button'), + this._builder.get_object('focus_application_windows_radio_button'), + this._builder.get_object('maximized_windows_radio_button') + ]; + + intellihideModeRadioButtons[this._settings.get_enum('intellihide-mode')].set_active(true); + + this._settings.bind('autohide', + this._builder.get_object('require_pressure_checkbutton'), + 'sensitive', + Gio.SettingsBindFlags.GET); + + this._settings.bind('autohide', + this._builder.get_object('autohide_enable_in_fullscreen_checkbutton'), + 'sensitive', + Gio.SettingsBindFlags.GET); + + this._settings.bind('require-pressure-to-show', + this._builder.get_object('show_timeout_spinbutton'), + 'sensitive', + Gio.SettingsBindFlags.INVERT_BOOLEAN); + this._settings.bind('require-pressure-to-show', + this._builder.get_object('show_timeout_label'), + 'sensitive', + Gio.SettingsBindFlags.INVERT_BOOLEAN); + this._settings.bind('require-pressure-to-show', + this._builder.get_object('pressure_threshold_spinbutton'), + 'sensitive', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('require-pressure-to-show', + this._builder.get_object('pressure_threshold_label'), + 'sensitive', + Gio.SettingsBindFlags.DEFAULT); + + dialog.connect('response', (dialog, id) => { + if (id == 1) { + // restore default settings for the relevant keys + let keys = ['intellihide', 'autohide', 'intellihide-mode', 'autohide-in-fullscreen', 'require-pressure-to-show', + 'animation-time', 'show-delay', 'hide-delay', 'pressure-threshold']; + keys.forEach(function (val) { + this._settings.set_value(val, this._settings.get_default_value(val)); + }, this); + intellihideModeRadioButtons[this._settings.get_enum('intellihide-mode')].set_active(true); + } else { + // remove the settings box so it doesn't get destroyed; + dialog.get_content_area().remove(box); + dialog.destroy(); + } + return; + }); + + dialog.present(); + + }); + + // size options + const dock_size_scale = this._builder.get_object('dock_size_scale'); + dock_size_scale.set_value(this._settings.get_double('height-fraction')); + dock_size_scale.add_mark(0.9, Gtk.PositionType.TOP, null); + dock_size_scale.set_format_value_func((_, value) => { + return Math.round(value * 100) + ' %'; + }); + let icon_size_scale = this._builder.get_object('icon_size_scale'); + icon_size_scale.set_range(8, DEFAULT_ICONS_SIZES[0]); + icon_size_scale.set_value(this._settings.get_int('dash-max-icon-size')); + DEFAULT_ICONS_SIZES.forEach(function (val) { + icon_size_scale.add_mark(val, Gtk.PositionType.TOP, val.toString()); + }); + icon_size_scale.set_format_value_func((_, value) => { + return value + ' px'; + }); + this._builder.get_object('preview_size_scale').set_value(this._settings.get_double('preview-size-scale')); + + // Corrent for rtl languages + if (this._rtl) { + // Flip value position: this is not done automatically + dock_size_scale.set_value_pos(Gtk.PositionType.LEFT); + icon_size_scale.set_value_pos(Gtk.PositionType.LEFT); + // I suppose due to a bug, having a more than one mark and one above a value of 100 + // makes the rendering of the marks wrong in rtl. This doesn't happen setting the scale as not flippable + // and then manually inverting it + icon_size_scale.set_flippable(false); + icon_size_scale.set_inverted(true); + } + + this._settings.bind('icon-size-fixed', this._builder.get_object('icon_size_fixed_checkbutton'), 'active', Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('extend-height', this._builder.get_object('dock_size_extend_checkbutton'), 'active', Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('extend-height', this._builder.get_object('dock_size_scale'), 'sensitive', Gio.SettingsBindFlags.INVERT_BOOLEAN); + + this._settings.bind('multi-monitor', + this._builder.get_object('dock_monitor_combo'), + 'sensitive', + Gio.SettingsBindFlags.INVERT_BOOLEAN); + + + // Apps panel + + this._settings.bind('show-running', + this._builder.get_object('show_running_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('isolate-workspaces', + this._builder.get_object('application_button_isolation_button'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('isolate-monitors', + this._builder.get_object('application_button_monitor_isolation_button'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-windows-preview', + this._builder.get_object('windows_preview_button'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('multi-monitor', + this._builder.get_object('multi_monitor_button'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-favorites', + this._builder.get_object('show_favorite_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-trash', + this._builder.get_object('show_trash_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-mounts', + this._builder.get_object('show_mounts_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('isolate-locations', + this._builder.get_object('isolate_locations_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + const isolateLocationsBindings = ['show_trash_switch', 'show_mounts_switch']; + const updateIsolateLocations = () => { + this._builder.get_object('isolate_locations_row').sensitive = + isolateLocationsBindings.some(s => this._builder.get_object(s).active); + }; + updateIsolateLocations(); + isolateLocationsBindings.forEach(s => this._builder.get_object(s).connect( + 'notify::active', () => updateIsolateLocations())); + this._settings.bind('show-show-apps-button', + this._builder.get_object('show_applications_button_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-apps-at-top', + this._builder.get_object('application_button_first_button'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-show-apps-button', + this._builder.get_object('application_button_first_button'), + 'sensitive', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('animate-show-apps', + this._builder.get_object('application_button_animation_button'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('show-show-apps-button', + this._builder.get_object('application_button_animation_button'), + 'sensitive', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('scroll-to-focused-application', + this._builder.get_object('scroll_to_icon_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + + + // Behavior panel + + this._settings.bind('hot-keys', + this._builder.get_object('hot_keys_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('hot-keys', + this._builder.get_object('overlay_button'), + 'sensitive', + Gio.SettingsBindFlags.DEFAULT); + + this._builder.get_object('click_action_combo').set_active(this._settings.get_enum('click-action')); + this._builder.get_object('click_action_combo').connect('changed', (widget) => { + this._settings.set_enum('click-action', widget.get_active()); + }); + + this._builder.get_object('scroll_action_combo').set_active(this._settings.get_enum('scroll-action')); + this._builder.get_object('scroll_action_combo').connect('changed', (widget) => { + this._settings.set_enum('scroll-action', widget.get_active()); + }); + + this._builder.get_object('shift_click_action_combo').connect('changed', (widget) => { + this._settings.set_enum('shift-click-action', widget.get_active()); + }); + + this._builder.get_object('middle_click_action_combo').connect('changed', (widget) => { + this._settings.set_enum('middle-click-action', widget.get_active()); + }); + this._builder.get_object('shift_middle_click_action_combo').connect('changed', (widget) => { + this._settings.set_enum('shift-middle-click-action', widget.get_active()); + }); + + // Create dialog for number overlay options + this._builder.get_object('overlay_button').connect('clicked', () => { + + let dialog = new Gtk.Dialog({ + title: __('Show dock and application numbers'), + transient_for: this.widget.get_root(), + use_header_bar: true, + modal: true + }); + + // GTK+ leaves positive values for application-defined response ids. + // Use +1 for the reset action + dialog.add_button(__('Reset to defaults'), 1); + + let box = this._builder.get_object('box_overlay_shortcut'); + dialog.get_content_area().append(box); + + this._builder.get_object('overlay_switch').set_active(this._settings.get_boolean('hotkeys-overlay')); + this._builder.get_object('show_dock_switch').set_active(this._settings.get_boolean('hotkeys-show-dock')); + + // We need to update the shortcut 'strv' when the text is modified + this._settings.connect('changed::shortcut-text', () => setShortcut(this._settings)); + this._settings.bind('shortcut-text', + this._builder.get_object('shortcut_entry'), + 'text', + Gio.SettingsBindFlags.DEFAULT); + + this._settings.bind('hotkeys-overlay', + this._builder.get_object('overlay_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('hotkeys-show-dock', + this._builder.get_object('show_dock_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('shortcut-timeout', + this._builder.get_object('timeout_spinbutton'), + 'value', + Gio.SettingsBindFlags.DEFAULT); + + dialog.connect('response', (dialog, id) => { + if (id == 1) { + // restore default settings for the relevant keys + let keys = ['shortcut-text', 'hotkeys-overlay', 'hotkeys-show-dock', 'shortcut-timeout']; + keys.forEach(function (val) { + this._settings.set_value(val, this._settings.get_default_value(val)); + }, this); + } else { + // remove the settings box so it doesn't get destroyed; + dialog.get_content_area().remove(box); + dialog.destroy(); + } + return; + }); + + dialog.present(); + }); + + // Create dialog for middle-click options + this._builder.get_object('middle_click_options_button').connect('clicked', () => { + + let dialog = new Gtk.Dialog({ + title: __('Customize middle-click behavior'), + transient_for: this.widget.get_root(), + use_header_bar: true, + modal: true + }); + + // GTK+ leaves positive values for application-defined response ids. + // Use +1 for the reset action + dialog.add_button(__('Reset to defaults'), 1); + + let box = this._builder.get_object('box_middle_click_options'); + dialog.get_content_area().append(box); + + this._builder.get_object('shift_click_action_combo').set_active(this._settings.get_enum('shift-click-action')); + + this._builder.get_object('middle_click_action_combo').set_active(this._settings.get_enum('middle-click-action')); + + this._builder.get_object('shift_middle_click_action_combo').set_active(this._settings.get_enum('shift-middle-click-action')); + + this._settings.bind('shift-click-action', + this._builder.get_object('shift_click_action_combo'), + 'active-id', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('middle-click-action', + this._builder.get_object('middle_click_action_combo'), + 'active-id', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('shift-middle-click-action', + this._builder.get_object('shift_middle_click_action_combo'), + 'active-id', + Gio.SettingsBindFlags.DEFAULT); + + dialog.connect('response', (dialog, id) => { + if (id == 1) { + // restore default settings for the relevant keys + let keys = ['shift-click-action', 'middle-click-action', 'shift-middle-click-action']; + keys.forEach(function (val) { + this._settings.set_value(val, this._settings.get_default_value(val)); + }, this); + this._builder.get_object('shift_click_action_combo').set_active(this._settings.get_enum('shift-click-action')); + this._builder.get_object('middle_click_action_combo').set_active(this._settings.get_enum('middle-click-action')); + this._builder.get_object('shift_middle_click_action_combo').set_active(this._settings.get_enum('shift-middle-click-action')); + } else { + // remove the settings box so it doesn't get destroyed; + dialog.get_content_area().remove(box); + dialog.destroy(); + } + return; + }); + + dialog.present(); + + }); + + // Appearance Panel + + this._settings.bind('apply-custom-theme', this._builder.get_object('customize_theme'), 'sensitive', Gio.SettingsBindFlags.INVERT_BOOLEAN | Gio.SettingsBindFlags.GET); + this._settings.bind('apply-custom-theme', this._builder.get_object('builtin_theme_switch'), 'active', Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('custom-theme-shrink', this._builder.get_object('shrink_dash_switch'), 'active', Gio.SettingsBindFlags.DEFAULT); + + // Running indicators + this._builder.get_object('running_indicators_combo').set_active( + this._settings.get_enum('running-indicator-style') + ); + this._builder.get_object('running_indicators_combo').connect( + 'changed', + (widget) => { + this._settings.set_enum('running-indicator-style', widget.get_active()); + } + ); + + if (this._settings.get_enum('running-indicator-style') == RunningIndicatorStyle.DEFAULT) + this._builder.get_object('running_indicators_advance_settings_button').set_sensitive(false); + + this._settings.connect('changed::running-indicator-style', () => { + if (this._settings.get_enum('running-indicator-style') == RunningIndicatorStyle.DEFAULT) + this._builder.get_object('running_indicators_advance_settings_button').set_sensitive(false); + else + this._builder.get_object('running_indicators_advance_settings_button').set_sensitive(true); + }); + + // Create dialog for running indicators advanced settings + this._builder.get_object('running_indicators_advance_settings_button').connect('clicked', () => { + + let dialog = new Gtk.Dialog({ + title: __('Customize running indicators'), + transient_for: this.widget.get_root(), + use_header_bar: true, + modal: true + }); + + let box = this._builder.get_object('running_dots_advance_settings_box'); + dialog.get_content_area().append(box); + + this._settings.bind('running-indicator-dominant-color', + this._builder.get_object('dominant_color_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + + this._settings.bind('custom-theme-customize-running-dots', + this._builder.get_object('dot_style_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('custom-theme-customize-running-dots', + this._builder.get_object('dot_style_settings_box'), + 'sensitive', Gio.SettingsBindFlags.DEFAULT); + + let rgba = new Gdk.RGBA(); + rgba.parse(this._settings.get_string('custom-theme-running-dots-color')); + this._builder.get_object('dot_color_colorbutton').set_rgba(rgba); + + this._builder.get_object('dot_color_colorbutton').connect('notify::rgba', (button) => { + let css = button.rgba.to_string(); + + this._settings.set_string('custom-theme-running-dots-color', css); + }); + + rgba.parse(this._settings.get_string('custom-theme-running-dots-border-color')); + this._builder.get_object('dot_border_color_colorbutton').set_rgba(rgba); + + this._builder.get_object('dot_border_color_colorbutton').connect('notify::rgba', (button) => { + let css = button.rgba.to_string(); + + this._settings.set_string('custom-theme-running-dots-border-color', css); + }); + + this._settings.bind('custom-theme-running-dots-border-width', + this._builder.get_object('dot_border_width_spin_button'), + 'value', + Gio.SettingsBindFlags.DEFAULT); + + + dialog.connect('response', (dialog, id) => { + // remove the settings box so it doesn't get destroyed; + dialog.get_content_area().remove(box); + dialog.destroy(); + return; + }); + + dialog.present(); + + }); + + this._settings.bind('custom-background-color', this._builder.get_object('custom_background_color_switch'), 'active', Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('custom-background-color', this._builder.get_object('custom_background_color'), 'sensitive', Gio.SettingsBindFlags.DEFAULT); + + let rgba = new Gdk.RGBA(); + rgba.parse(this._settings.get_string('background-color')); + this._builder.get_object('custom_background_color').set_rgba(rgba); + + this._builder.get_object('custom_background_color').connect('notify::rgba', (button) => { + let css = button.rgba.to_string(); + + this._settings.set_string('background-color', css); + }); + + // Opacity + this._builder.get_object('customize_opacity_combo').set_active_id( + this._settings.get_enum('transparency-mode').toString() + ); + this._builder.get_object('customize_opacity_combo').connect( + 'changed', + (widget) => { + this._settings.set_enum('transparency-mode', parseInt(widget.get_active_id())); + } + ); + + const custom_opacity_scale = this._builder.get_object('custom_opacity_scale'); + custom_opacity_scale.set_value(this._settings.get_double('background-opacity')); + custom_opacity_scale.set_format_value_func((_, value) => { + return Math.round(value * 100) + '%'; + }); + + if (this._settings.get_enum('transparency-mode') !== TransparencyMode.FIXED) + this._builder.get_object('custom_opacity_scale').set_sensitive(false); + + this._settings.connect('changed::transparency-mode', () => { + if (this._settings.get_enum('transparency-mode') !== TransparencyMode.FIXED) + this._builder.get_object('custom_opacity_scale').set_sensitive(false); + else + this._builder.get_object('custom_opacity_scale').set_sensitive(true); + }); + + if (this._settings.get_enum('transparency-mode') !== TransparencyMode.DYNAMIC) { + this._builder.get_object('dynamic_opacity_button').set_sensitive(false); + } + + this._settings.connect('changed::transparency-mode', () => { + if (this._settings.get_enum('transparency-mode') !== TransparencyMode.DYNAMIC) { + this._builder.get_object('dynamic_opacity_button').set_sensitive(false); + } + else { + this._builder.get_object('dynamic_opacity_button').set_sensitive(true); + } + }); + + // Create dialog for transparency advanced settings + this._builder.get_object('dynamic_opacity_button').connect('clicked', () => { + + let dialog = new Gtk.Dialog({ + title: __('Customize opacity'), + transient_for: this.widget.get_root(), + use_header_bar: true, + modal: true + }); + + let box = this._builder.get_object('advanced_transparency_dialog'); + dialog.get_content_area().append(box); + + this._settings.bind( + 'customize-alphas', + this._builder.get_object('customize_alphas_switch'), + 'active', + Gio.SettingsBindFlags.DEFAULT + ); + this._settings.bind( + 'customize-alphas', + this._builder.get_object('min_alpha_scale'), + 'sensitive', + Gio.SettingsBindFlags.DEFAULT + ); + this._settings.bind( + 'customize-alphas', + this._builder.get_object('max_alpha_scale'), + 'sensitive', + Gio.SettingsBindFlags.DEFAULT + ); + + const min_alpha_scale = this._builder.get_object('min_alpha_scale'); + const max_alpha_scale = this._builder.get_object('max_alpha_scale'); + min_alpha_scale.set_value( + this._settings.get_double('min-alpha') + ); + min_alpha_scale.set_format_value_func((_, value) => { + return Math.round(value * 100) + ' %'; + }); + max_alpha_scale.set_format_value_func((_, value) => { + return Math.round(value * 100) + ' %'; + }); + + max_alpha_scale.set_value( + this._settings.get_double('max-alpha') + ); + + dialog.connect('response', (dialog, id) => { + // remove the settings box so it doesn't get destroyed; + dialog.get_content_area().remove(box); + dialog.destroy(); + return; + }); + + dialog.present(); + }); + + + this._settings.bind('unity-backlit-items', + this._builder.get_object('unity_backlit_items_switch'), + 'active', Gio.SettingsBindFlags.DEFAULT + ); + + this._settings.bind('force-straight-corner', + this._builder.get_object('force_straight_corner_switch'), + 'active', Gio.SettingsBindFlags.DEFAULT); + + // About Panel + + if (Me) + this._builder.get_object('extension_version').set_label(Me.metadata.version.toString()); + else + this._builder.get_object('extension_version').set_label('Unknown'); + } +}); + +function init() { + ExtensionUtils.initTranslations(); +} + +function buildPrefsWidget() { + let settings = new Settings(); + let widget = settings.widget; + return widget; +} + +if (!Me) { + GLib.setenv('GSETTINGS_SCHEMA_DIR', './schemas', true); + Gtk.init(); + + const loop = GLib.MainLoop.new(null, false); + const win = new Gtk.Window(); + win.set_child(buildPrefsWidget()); + win.connect('close-request', () => loop.quit()); + win.present(); + + loop.run(); +} diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/schemas/gschemas.compiled b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/schemas/gschemas.compiled new file mode 100644 index 0000000000000000000000000000000000000000..a519241737a7f17fe25851b3d9d701ac4ee38a5b GIT binary patch literal 7431 zcmds6YitzP865}&^L~RNKqz2XL&i4P7*k$0e&7u@Hhu&{aLBMb_72|NS!QN2ZXkiE zs)(wlv<)cGswGV!jv%3Fs+9gnLQ84dh$bj$D_3n=R1t-yO+alUfm*UX=gyt=>>8{8 z8R_^td*+)v_wn6(zdPRWkY*WL+)?C}3)|-Mz)(+w9X?f2Qn=iEb;4&m^n;a!i|Izb zNU2r64Ex(riZTS&v^onq<7P}Rup@djTA=TB^tf%9al0U_*%8$-)vy`rf~OLAgnx1I zo%cu#gB=ds3*-PJ0Tiu_m6!lK378B_2W9~G0keSFz+4~ZfzJmP0C^G%VHW`bAM(Ko zHi}r{!vo-c6yUdqLhx)9K_iyk0maZtC04+e0jq%3z#3rPASj1k;lp}xq6(-6YJiPE z9k2;#02+ZNpc!ZdIELE*j#)cU_v%+ZVgGR4#wdG~E3nsr$8Vl(qE0>mx-uH$0n|%> za{A}OE&|?varVdbC!Ywt40a8$@A!;U)XAqoZ-Z?I&OUPSJ?iB2PrxRDrd`2q>g3~~ zAA$tOYYgS`qgFZ|2X)XB#} zzX^Mre9>2zQzu^tJun922kdWu^ageE8PFSHOA&+z^Lle4`CV2=Qi3;DyTlT+`7eF?~s{^Zop!~PB!DRpw{*I;i0zx>{*XITz; zF7(`Smw-%NjUj&B4u20Z`H${OnAqoD7F z-4BeG<&d+S!>}iTj{{9#r$0Hz{XFbNV2t!9r+**pC%{;#lT%m5E6P-0oYcvw2VmC# z-&i#J5X&d0-U$0mV7&Avr``+uIxzg?2%G-o^PyjX{SX-1(e!8Pz*`H+In!uwPQ+U9?LSe-4~}?bS2X$;Etuy$Y=AsMtuId^X=Z12y6_{s{ShGHM!`2*j?C% z%E#p8(4U;^zya8!z`+xvKc`N9KlF33zX0-{d+}ZB<7Seo!7*B$@$(Ju>S?7 z&T9WI{mJP+VH(N~6jeEKQs+kw_+ zhdoN2oa3H=-3QD{j^x6e3+6DU-@SxW&srSKt1Z@;NPT#Q2;YF}qc`+e5^2Nym&B zVO=%iYDY2}wL_M!$K3*W-+Nos3~3JYUgpKnV+M*dzQp2;4oCP4J&8*7s5`Y#SJZG+ z!_i}S0{(cb<%E*-l=a#z>y@x{+fG`#>O{aIW;86{?wYOTN+6ss^6s!2(T&cC`!?Q1 zxuK-(m@z!7$8^<7#^Xl36Dhf4t2<3AtXpcg5q2W|B?b&s1vOSM8tAvw z2qj<#dbDJIz8el`wdxo#(bVI|c>Q`E`&$HwY=!qaH(n5!A;*e((YX}kS&83k<)|*j z^2;c7gV%PW%{?OB@%d$0&o7hDGoJnLDbDgMmVTCwmLYihqsSERM4QF?jJShyo52vj z zT_xkaYU`$o`d}p*f>=z)R#qk56)7W5R?7Eh%wP8hR~7$E7X9oSM_Em!rlGR6c}GQK zV_mSaq9xd{X~&k}rmBW5&8SRz?pD;*`D9uDK_7JIbbUozus*o0x+?u-YG%4^;@BV4 z_Nqn*bH_$yi#Uo{SFn|fb3cwQXdL<*KpKtxkU0R)Gm3(3HhsRI1KtE+sKvSVA=ozH zVPHGJb1g5fGwfhYmGl{T1NcUu9@qpl0F6K`uomFDlLssTc-BUkC>(|R0e&06@le5( z66f`;uwMX>dWr7^I4X-XI6_$o0^5Lc07nGzEno`lTwplBcu)bJ^?By!_Xw_Im{#Ka z&b221Oa}O!g5NJ51Q3qmI|P;w?0eipky8R+1hxPzz-C|pFbyaG3W0F|wnv5Qp%)`q zYYl8pSQl-lC#wJd_^oPaL5#R#-MqQAqN$o8QX=?D#1F?w2%w*iLabK(-+!;G9NMSW(MokO){0?!U zqik-gsuoQ|H0b{Icwk$@CVcVLD_snpz3G6rSOoy zUSioxn6}~DC)YQ$v@{?9y*H#<8W@7^9BT@0sBMvb>^b6|*dyzzYcRgPy~x)oWFjwo|LE>debk*v z$MH6N@thbcEQWSOmgCLt^kVOo!{i|&Yw$K+)*;;&cTvuOwA0HOu&%q=lgv$*a_w8) z%!JBI0E)0qFaa1m(!3D!$5$*Fbqp`1Wk-q&N`h?I=|zew)?cSys%l=^O*OTPSD9=zl@Fr$zQzkl8H z(r6wVqMBG1$y#qp z`rjArQT_2Ko)>4^u+F~o@27HB^e-xSTi8_NrV~Ma?pqW5qPMqYE+B3mF>rpRMyKcC zfIeetmDP^rvK`pKJ9Nv!4kr)z;(u1mF9(#u#Bo45rTzVpIif2v`+vn?e)!(tU(d@1 z^IMtew^I6vjkH7CV_G=;bMtlMm6i7vYKpkhCr06 nD!$(Emsn?g?-zp)UDj{8EXU{J4y?RiI`BTT`SkNE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 'BOTTOM' + Dock position + Dock is shown on the Left, Right, Top or Bottom side of the screen. + + + 0.2 + Animation time + Sets the time duration of the autohide effect. + + + 0.25 + Show delay + Sets the delay after the mouse reaches the screen border before showing the dock. + + + 0.20 + Show delay + Sets the delay after the mouse left the dock before hiding it. + + + false + Set a custom dash background background color + Sets the color for the dash background. + + + "#ffffff" + Dash background color. + Customize the background color of the dash. + + + 'DEFAULT' + Transparency mode for the dock + FIXED: constant transparency. DYNAMIC: dock takes the opaque style only when windows are close to it. + + + 'DEFAULT' + ... + DEFAULT: .... DOTS: .... + + + false + Use application icon dominant color for the indicator color + + + + false + Manually set the min and max opacity + For the dynamic mode, the min/max opacity values will be given by 'min-alpha' and 'max-alpha'. + + + 0.2 + Opacity of the dash background when free-floating + Sets the opacity of the dash background when no windows are close. + + + 0.8 + Opacity of the dash background when windows are close. + Sets the opacity of the dash background when windows are close. + + + 0.8 + Opacity of the dash background + Sets the opacity of the dash background when in autohide mode. + + + true + Dock dodges windows + Enable or disable intellihide mode + + + 'FOCUS_APPLICATION_WINDOWS' + Define which windows are considered for intellihide. + + + + true + Dock shown on mouse over + Enable or disable autohide mode + + + true + Require pressure to show dash + Enable or disable requiring pressure to show the dash + + + 100 + Pressure threshold + Sets how much pressure is needed to show the dash. + + + false + Enable autohide in fullscreen mode. + Enable autohide in fullscreen mode. + + + false + Dock always visible + Dock is always visible + + + true + Switch workspace by scrolling over the dock + Add the possibility to switch workspace by mouse scrolling over the dock. + + + 48 + Maximum dash icon size + Set the allowed maximum dash icon size. Allowed range: 16..64. + + + 0 + Preview size scale + Set the allowed maximum dash preview size scale. Allowed range: 0,00..1,00. + + + false + Fixed icon size + Keep the icon size fixed by scrolling the dock. + + + false + Apply custom theme + Apply customization to the dash appearance + + + false + TODO + TODO + + + false + Customize the style of the running application indicators. + Customize the style of the running application indicators. + + + "#ffffff" + Running application indicators color + Customize the color of the running application indicators. + + + "#ffffff" + Running application indicators border color. + Customize the border color of the running application indicators. + + + 0 + Running application indicators border width. + Customize the border width of the running application indicators. + + + true + Show running apps + Show or hide running applications icons in the dash + + + false + Provide workspace isolation + Dash shows only windows from the currentworkspace + + + false + Provide monitor isolation + Dash shows only windows from the monitor + + + true + Scroll to focused application + Ensure that the focused application icon is always visible in the dash + + + true + Show preview of the open windows + Replace open windows list with windows previews + + + true + Show favorites apps + Show or hide favorite applications icons in the dash + + + true + Show trash can + Show or hide the trash can icon in the dash + + + true + Show mounted volumes and devices + Show or hide mounted volume and device icons in the dash + + + true + Isolate volumes, devices and trash windows + Consider volume, devices and trash as different application windows and not part of the file manager + + + true + Show applications button + Show applications button in the dash + + + false + Show application button on the left + Show application button on the left of the dash + + + true + Animate Show Applications from the desktop + Animate Show Applications from the desktop + + + true + Basic compatibility with bolt extensions + Make the extension work properly when bolt extensions is enabled + + + 0.90 + Dock max height (fraction of available space) + + + false + Extend the dock container to all the available height + + + -1 + Monitor on which putting the dock + Set on which monitor to put the dock, use -1 for the primary one + + + false + Enable multi-monitor docks + Show a dock on every monitor + + + true + Minimize on shift+click + + + true + Activate only one window + + + 'cycle-windows' + Action when clicking on a running app + Set the action that is executed when clicking on the icon of a running application + + + 'do-nothing' + Action when scrolling app + Set the action that is executed when scrolling on the application icon + + + 'minimize' + Action when shift+clicking on a running app + Set the action that is executed when shift+clicking on the icon of a running application + + + 'launch' + Action when clicking on a running app + Set the action that is executed when middle-clicking on the icon of a running application + + + 'launch' + Action when clicking on a running app + Set the action that is executed when shift+middle-clicking on the icon of a running application + + + true + Super Hot-Keys + Launch and switch between dash items using Super+(0-9) + + + true + Show the dock when using the hotkeys + The dock will be quickly shown so that the number-overlay is visible and app activation is easier + + + "<Super>q" + Keybinding to show the dock and the number overlay. + Behavior depends on hotkeys-show-dock and hotkeys-overlay. + + + q']]]> + Keybinding to show the dock and the number overlay. + Behavior depends on hotkeys-show-dock and hotkeys-overlay. + + + 2 + Timeout to hide the dock + Sets the time duration before the dock is hidden again. + + + true + Show the dock when using the hotkeys + The dock will be quickly shown so that the number-overlay is visible and app activation is easier + + + 1']]]> + Keybinding to launch 1st dash app + + Keybinding to launch 1st app. + + + + 2']]]> + Keybinding to launch 2nd dash app + + Keybinding to launch 2nd app. + + + + 3']]]> + Keybinding to launch 3rd dash app + + Keybinding to launch 3rd app. + + + + 4']]]> + Keybinding to launch 4th dash app + + Keybinding to launch 4th app. + + + + 5']]]> + Keybinding to launch 5th dash app + + Keybinding to launch 5th app. + + + + 6']]]> + Keybinding to launch 6th dash app + + Keybinding to launch 6th app. + + + + 7']]]> + Keybinding to launch 7th dash app + + Keybinding to launch 7th app. + + + + 8']]]> + Keybinding to launch 8th dash app + + Keybinding to launch 8th app. + + + + 9']]]> + Keybinding to launch 9th dash app + + Keybinding to launch 9th app. + + + + 0']]]> + Keybinding to launch 10th dash app + + Keybinding to launch 10th app. + + + + 1']]]> + Keybinding to trigger 1st dash app with shift behavior + + Keybinding to trigger 1st app with shift behavior. + + + + 2']]]> + Keybinding to trigger 2nd dash app with shift behavior + + Keybinding to trigger 2nd app with shift behavior. + + + + 3']]]> + Keybinding to trigger 3rd dash app with shift behavior + + Keybinding to trigger 3rd app with shift behavior. + + + + 4']]]> + Keybinding to trigger 4th dash app with shift behavior + + Keybinding to trigger 4th app with shift behavior. + + + + 5']]]> + Keybinding to trigger 5th dash app with shift behavior + + Keybinding to trigger 5th app with shift behavior. + + + + 6']]]> + Keybinding to trigger 6th dash app with shift behavior + + Keybinding to trigger 6th app with shift behavior. + + + + 7']]]> + Keybinding to trigger 7th dash app with shift behavior + + Keybinding to trigger 7th app with shift behavior. + + + + 8']]]> + Keybinding to trigger 8th dash app with shift behavior + + Keybinding to trigger 8th app with shift behavior. + + + + 9']]]> + Keybinding to trigger 9th dash app with shift behavior + + Keybinding to trigger 9th app with shift behavior. + + + + 0']]]> + Keybinding to trigger 10th dash app with shift behavior + + Keybinding to trigger 10th app with shift behavior. + + + + 1']]]> + Keybinding to trigger 1st dash app + + Keybinding to either show or launch the 1st application in the dash. + + + + 2']]]> + Keybinding to trigger 2nd dash app + + Keybinding to either show or launch the 2nd application in the dash. + + + + 3']]]> + Keybinding to trigger 3rd dash app + + Keybinding to either show or launch the 3rd application in the dash. + + + + 4']]]> + Keybinding to trigger 4th dash app + + Keybinding to either show or launch the 4th application in the dash. + + + + 5']]]> + Keybinding to trigger 5th dash app + + Keybinding to either show or launch the 5th application in the dash. + + + + 6']]]> + Keybinding to trigger 6th dash app + + Keybinding to either show or launch the 6th application in the dash. + + + + 7']]]> + Keybinding to trigger 7th dash app + + Keybinding to either show or launch the 7th application in the dash. + + + + 8']]]> + Keybinding to trigger 8th dash app + + Keybinding to either show or launch the 8th application in the dash. + + + + 9']]]> + Keybinding to trigger 9th dash app + + Keybinding to either show or launch the 9th application in the dash. + + + + 0']]]> + Keybinding to trigger 10th dash app + + Keybinding to either show or launch the 10th application in the dash. + + + + false + Force straight corners in dash + Make the borders in the dash non rounded + + + false + Enable unity7 like glossy backlit items + Emulate the unity7 backlit glossy items behaviour + + + diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/stylesheet.css b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/stylesheet.css new file mode 100644 index 0000000..52ee72d --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/stylesheet.css @@ -0,0 +1,528 @@ +#dashtodockContainer.bottom #dash { + margin: 0px; + padding: 0px; } + #dashtodockContainer.bottom #dash .dash-background { + margin: 0; + margin-bottom: 4px; + padding: 0; } + #dashtodockContainer.bottom #dash .dash-separator { + margin-bottom: 0; } + #dashtodockContainer.bottom #dash #dashtodockDashContainer { + padding: 10px; + padding-bottom: 0; + padding-top: 0; } + #dashtodockContainer.bottom #dash .dash-item-container .app-well-app, + #dashtodockContainer.bottom #dash .dash-item-container .show-apps { + padding: 2px; + padding-bottom: 14px; + padding-top: 10px; } + +#dashtodockContainer.bottom.shrink #dash .dash-background { + margin-bottom: 1px; + padding: 3px; + border-radius: 12px; } + +#dashtodockContainer.bottom.shrink #dash #dashtodockDashContainer { + padding: 3px; } + +#dashtodockContainer.bottom.shrink #dash .dash-item-container .app-well-app, +#dashtodockContainer.bottom.shrink #dash .dash-item-container .show-apps { + padding: 1px; + padding-bottom: 4px; + padding-top: 3px; } + +#dashtodockContainer.bottom.shrink.fixed #dash .dash-background { + margin-top: 1px; } + +#dashtodockContainer.bottom.shrink.fixed #dash .dash-item-container .app-well-app, +#dashtodockContainer.bottom.shrink.fixed #dash .dash-item-container .show-apps { + padding-top: 4px; } + +#dashtodockContainer.bottom.fixed #dash .dash-background { + margin-top: 4px; } + +#dashtodockContainer.bottom.fixed #dash .dash-item-container .app-well-app, +#dashtodockContainer.bottom.fixed #dash .dash-item-container .show-apps { + padding-top: 14px; } + +#dashtodockContainer.top #dash { + margin: 0px; + padding: 0px; } + #dashtodockContainer.top #dash .dash-background { + margin: 0; + margin-top: 4px; + padding: 0; } + #dashtodockContainer.top #dash .dash-separator { + margin-bottom: 0; } + #dashtodockContainer.top #dash #dashtodockDashContainer { + padding: 10px; + padding-top: 0; + padding-bottom: 0; } + #dashtodockContainer.top #dash .dash-item-container .app-well-app, + #dashtodockContainer.top #dash .dash-item-container .show-apps { + padding: 2px; + padding-top: 14px; + padding-bottom: 10px; } + +#dashtodockContainer.top.shrink #dash .dash-background { + margin-top: 1px; + padding: 3px; + border-radius: 12px; } + +#dashtodockContainer.top.shrink #dash #dashtodockDashContainer { + padding: 3px; } + +#dashtodockContainer.top.shrink #dash .dash-item-container .app-well-app, +#dashtodockContainer.top.shrink #dash .dash-item-container .show-apps { + padding: 1px; + padding-top: 4px; + padding-bottom: 3px; } + +#dashtodockContainer.top.shrink.fixed #dash .dash-background { + margin-bottom: 1px; } + +#dashtodockContainer.top.shrink.fixed #dash .dash-item-container .app-well-app, +#dashtodockContainer.top.shrink.fixed #dash .dash-item-container .show-apps { + padding-bottom: 4px; } + +#dashtodockContainer.top.fixed #dash .dash-background { + margin-bottom: 4px; } + +#dashtodockContainer.top.fixed #dash .dash-item-container .app-well-app, +#dashtodockContainer.top.fixed #dash .dash-item-container .show-apps { + padding-bottom: 14px; } + +#dashtodockContainer.left #dash { + margin: 0px; + padding: 0px; } + #dashtodockContainer.left #dash .dash-background { + margin: 0; + margin-left: 4px; + padding: 0; } + #dashtodockContainer.left #dash .dash-separator { + height: 1px; + margin: 7px 0; + background-color: rgba(238, 238, 236, 0.3); } + #dashtodockContainer.left #dash #dashtodockDashContainer { + padding: 10px; + padding-left: 0; + padding-right: 0; } + #dashtodockContainer.left #dash .dash-item-container .app-well-app, + #dashtodockContainer.left #dash .dash-item-container .show-apps { + padding: 2px; + padding-left: 14px; + padding-right: 10px; } + +#dashtodockContainer.left.shrink #dash .dash-background { + margin-left: 1px; + padding: 3px; + border-radius: 12px; } + +#dashtodockContainer.left.shrink #dash #dashtodockDashContainer { + padding: 3px; } + +#dashtodockContainer.left.shrink #dash .dash-item-container .app-well-app, +#dashtodockContainer.left.shrink #dash .dash-item-container .show-apps { + padding: 1px; + padding-left: 4px; + padding-right: 3px; } + +#dashtodockContainer.left.shrink.fixed #dash .dash-background { + margin-right: 1px; } + +#dashtodockContainer.left.shrink.fixed #dash .dash-item-container .app-well-app, +#dashtodockContainer.left.shrink.fixed #dash .dash-item-container .show-apps { + padding-right: 4px; } + +#dashtodockContainer.left.fixed #dash .dash-background { + margin-right: 4px; } + +#dashtodockContainer.left.fixed #dash .dash-item-container .app-well-app, +#dashtodockContainer.left.fixed #dash .dash-item-container .show-apps { + padding-right: 14px; } + +#dashtodockContainer.right #dash { + margin: 0px; + padding: 0px; } + #dashtodockContainer.right #dash .dash-background { + margin: 0; + margin-right: 4px; + padding: 0; } + #dashtodockContainer.right #dash .dash-separator { + height: 1px; + margin: 7px 0; + background-color: rgba(238, 238, 236, 0.3); } + #dashtodockContainer.right #dash #dashtodockDashContainer { + padding: 10px; + padding-right: 0; + padding-left: 0; } + #dashtodockContainer.right #dash .dash-item-container .app-well-app, + #dashtodockContainer.right #dash .dash-item-container .show-apps { + padding: 2px; + padding-right: 14px; + padding-left: 10px; } + +#dashtodockContainer.right.shrink #dash .dash-background { + margin-right: 1px; + padding: 3px; + border-radius: 12px; } + +#dashtodockContainer.right.shrink #dash #dashtodockDashContainer { + padding: 3px; } + +#dashtodockContainer.right.shrink #dash .dash-item-container .app-well-app, +#dashtodockContainer.right.shrink #dash .dash-item-container .show-apps { + padding: 1px; + padding-right: 4px; + padding-left: 3px; } + +#dashtodockContainer.right.shrink.fixed #dash .dash-background { + margin-left: 1px; } + +#dashtodockContainer.right.shrink.fixed #dash .dash-item-container .app-well-app, +#dashtodockContainer.right.shrink.fixed #dash .dash-item-container .show-apps { + padding-left: 4px; } + +#dashtodockContainer.right.fixed #dash .dash-background { + margin-left: 4px; } + +#dashtodockContainer.right.fixed #dash .dash-item-container .app-well-app, +#dashtodockContainer.right.fixed #dash .dash-item-container .show-apps { + padding-left: 14px; } + +/* In extended mode we need to use the first and last .dash-item-container's + * to apply the padding on the dock, to ensure that the actual first or last + * child show-apps item will actually include the padding area so that it will + * be clickable up to the dock edge, and make Fitts happy. + * I don't think the same should happen for normal icons, so in the other side + * the padding will be applied via the scrolled area, given we can't get the + * parent of the first/last app-well-app icon to apply a rule there. + */ +#dashtodockContainer.extended.bottom #dash .dash-background { + margin: 0; + border-radius: 0; } + +#dashtodockContainer.extended.bottom #dash #dashtodockDashContainer { + padding: 0; + padding-bottom: 0; + padding-top: 0; } + #dashtodockContainer.extended.bottom #dash #dashtodockDashContainer > :first-child { + /* Use this instead of #dashtodockDashScrollview rule to apply the + * padding via the last app-icon item */ } + #dashtodockContainer.extended.bottom #dash #dashtodockDashContainer > :first-child .show-apps { + padding-left: 8px; } + #dashtodockContainer.extended.bottom #dash #dashtodockDashContainer #dashtodockDashScrollview:first-child { + padding-left: 8px; } + #dashtodockContainer.extended.bottom #dash #dashtodockDashContainer > :last-child { + /* Use this instead of #dashtodockDashScrollview rule to apply the + * padding via the last app-icon item */ } + #dashtodockContainer.extended.bottom #dash #dashtodockDashContainer > :last-child .show-apps { + padding-right: 8px; } + #dashtodockContainer.extended.bottom #dash #dashtodockDashContainer #dashtodockDashScrollview:last-child { + padding-right: 8px; } + +#dashtodockContainer.extended.bottom #dash .dash-item-container .app-well-app, +#dashtodockContainer.extended.bottom #dash .dash-item-container .show-apps { + padding-bottom: 10px; } + +#dashtodockContainer.extended.bottom.shrink #dash #dashtodockDashContainer { + padding: 0; } + #dashtodockContainer.extended.bottom.shrink #dash #dashtodockDashContainer > :first-child { + /* Use this instead of #dashtodockDashScrollview rule to apply the + * padding via the last app-icon item */ } + #dashtodockContainer.extended.bottom.shrink #dash #dashtodockDashContainer > :first-child .show-apps { + padding-left: 2px; } + #dashtodockContainer.extended.bottom.shrink #dash #dashtodockDashContainer #dashtodockDashScrollview:first-child { + padding-left: 2px; } + #dashtodockContainer.extended.bottom.shrink #dash #dashtodockDashContainer > :last-child { + /* Use this instead of #dashtodockDashScrollview rule to apply the + * padding via the last app-icon item */ } + #dashtodockContainer.extended.bottom.shrink #dash #dashtodockDashContainer > :last-child .show-apps { + padding-right: 2px; } + #dashtodockContainer.extended.bottom.shrink #dash #dashtodockDashContainer #dashtodockDashScrollview:last-child { + padding-right: 2px; } + +#dashtodockContainer.extended.bottom.shrink #dash .dash-item-container .app-well-app, +#dashtodockContainer.extended.bottom.shrink #dash .dash-item-container .show-apps { + padding-bottom: 3px; } + +#dashtodockContainer.extended.bottom.shrink.fixed #dash .dash-background { + margin-top: 0; } + +#dashtodockContainer.extended.top #dash .dash-background { + margin: 0; + border-radius: 0; } + +#dashtodockContainer.extended.top #dash #dashtodockDashContainer { + padding: 0; + padding-top: 0; + padding-bottom: 0; } + #dashtodockContainer.extended.top #dash #dashtodockDashContainer > :first-child { + /* Use this instead of #dashtodockDashScrollview rule to apply the + * padding via the last app-icon item */ } + #dashtodockContainer.extended.top #dash #dashtodockDashContainer > :first-child .show-apps { + padding-left: 8px; } + #dashtodockContainer.extended.top #dash #dashtodockDashContainer #dashtodockDashScrollview:first-child { + padding-left: 8px; } + #dashtodockContainer.extended.top #dash #dashtodockDashContainer > :last-child { + /* Use this instead of #dashtodockDashScrollview rule to apply the + * padding via the last app-icon item */ } + #dashtodockContainer.extended.top #dash #dashtodockDashContainer > :last-child .show-apps { + padding-right: 8px; } + #dashtodockContainer.extended.top #dash #dashtodockDashContainer #dashtodockDashScrollview:last-child { + padding-right: 8px; } + +#dashtodockContainer.extended.top #dash .dash-item-container .app-well-app, +#dashtodockContainer.extended.top #dash .dash-item-container .show-apps { + padding-top: 10px; } + +#dashtodockContainer.extended.top.shrink #dash #dashtodockDashContainer { + padding: 0; } + #dashtodockContainer.extended.top.shrink #dash #dashtodockDashContainer > :first-child { + /* Use this instead of #dashtodockDashScrollview rule to apply the + * padding via the last app-icon item */ } + #dashtodockContainer.extended.top.shrink #dash #dashtodockDashContainer > :first-child .show-apps { + padding-left: 2px; } + #dashtodockContainer.extended.top.shrink #dash #dashtodockDashContainer #dashtodockDashScrollview:first-child { + padding-left: 2px; } + #dashtodockContainer.extended.top.shrink #dash #dashtodockDashContainer > :last-child { + /* Use this instead of #dashtodockDashScrollview rule to apply the + * padding via the last app-icon item */ } + #dashtodockContainer.extended.top.shrink #dash #dashtodockDashContainer > :last-child .show-apps { + padding-right: 2px; } + #dashtodockContainer.extended.top.shrink #dash #dashtodockDashContainer #dashtodockDashScrollview:last-child { + padding-right: 2px; } + +#dashtodockContainer.extended.top.shrink #dash .dash-item-container .app-well-app, +#dashtodockContainer.extended.top.shrink #dash .dash-item-container .show-apps { + padding-top: 3px; } + +#dashtodockContainer.extended.top.shrink.fixed #dash .dash-background { + margin-bottom: 0; } + +#dashtodockContainer.extended.left #dash .dash-background { + margin: 0; + border-radius: 0; } + +#dashtodockContainer.extended.left #dash #dashtodockDashContainer { + padding: 0; + padding-left: 0; + padding-right: 0; } + #dashtodockContainer.extended.left #dash #dashtodockDashContainer > :first-child { + /* Use this instead of #dashtodockDashScrollview rule to apply the + * padding via the last app-icon item */ } + #dashtodockContainer.extended.left #dash #dashtodockDashContainer > :first-child .show-apps { + padding-top: 8px; } + #dashtodockContainer.extended.left #dash #dashtodockDashContainer #dashtodockDashScrollview:first-child { + padding-top: 8px; } + #dashtodockContainer.extended.left #dash #dashtodockDashContainer > :last-child { + /* Use this instead of #dashtodockDashScrollview rule to apply the + * padding via the last app-icon item */ } + #dashtodockContainer.extended.left #dash #dashtodockDashContainer > :last-child .show-apps { + padding-bottom: 8px; } + #dashtodockContainer.extended.left #dash #dashtodockDashContainer #dashtodockDashScrollview:last-child { + padding-bottom: 8px; } + +#dashtodockContainer.extended.left #dash .dash-item-container .app-well-app, +#dashtodockContainer.extended.left #dash .dash-item-container .show-apps { + padding-left: 10px; } + +#dashtodockContainer.extended.left.shrink #dash #dashtodockDashContainer { + padding: 0; } + #dashtodockContainer.extended.left.shrink #dash #dashtodockDashContainer > :first-child { + /* Use this instead of #dashtodockDashScrollview rule to apply the + * padding via the last app-icon item */ } + #dashtodockContainer.extended.left.shrink #dash #dashtodockDashContainer > :first-child .show-apps { + padding-top: 2px; } + #dashtodockContainer.extended.left.shrink #dash #dashtodockDashContainer #dashtodockDashScrollview:first-child { + padding-top: 2px; } + #dashtodockContainer.extended.left.shrink #dash #dashtodockDashContainer > :last-child { + /* Use this instead of #dashtodockDashScrollview rule to apply the + * padding via the last app-icon item */ } + #dashtodockContainer.extended.left.shrink #dash #dashtodockDashContainer > :last-child .show-apps { + padding-bottom: 2px; } + #dashtodockContainer.extended.left.shrink #dash #dashtodockDashContainer #dashtodockDashScrollview:last-child { + padding-bottom: 2px; } + +#dashtodockContainer.extended.left.shrink #dash .dash-item-container .app-well-app, +#dashtodockContainer.extended.left.shrink #dash .dash-item-container .show-apps { + padding-left: 3px; } + +#dashtodockContainer.extended.left.shrink.fixed #dash .dash-background { + margin-right: 0; } + +#dashtodockContainer.extended.right #dash .dash-background { + margin: 0; + border-radius: 0; } + +#dashtodockContainer.extended.right #dash #dashtodockDashContainer { + padding: 0; + padding-right: 0; + padding-left: 0; } + #dashtodockContainer.extended.right #dash #dashtodockDashContainer > :first-child { + /* Use this instead of #dashtodockDashScrollview rule to apply the + * padding via the last app-icon item */ } + #dashtodockContainer.extended.right #dash #dashtodockDashContainer > :first-child .show-apps { + padding-top: 8px; } + #dashtodockContainer.extended.right #dash #dashtodockDashContainer #dashtodockDashScrollview:first-child { + padding-top: 8px; } + #dashtodockContainer.extended.right #dash #dashtodockDashContainer > :last-child { + /* Use this instead of #dashtodockDashScrollview rule to apply the + * padding via the last app-icon item */ } + #dashtodockContainer.extended.right #dash #dashtodockDashContainer > :last-child .show-apps { + padding-bottom: 8px; } + #dashtodockContainer.extended.right #dash #dashtodockDashContainer #dashtodockDashScrollview:last-child { + padding-bottom: 8px; } + +#dashtodockContainer.extended.right #dash .dash-item-container .app-well-app, +#dashtodockContainer.extended.right #dash .dash-item-container .show-apps { + padding-right: 10px; } + +#dashtodockContainer.extended.right.shrink #dash #dashtodockDashContainer { + padding: 0; } + #dashtodockContainer.extended.right.shrink #dash #dashtodockDashContainer > :first-child { + /* Use this instead of #dashtodockDashScrollview rule to apply the + * padding via the last app-icon item */ } + #dashtodockContainer.extended.right.shrink #dash #dashtodockDashContainer > :first-child .show-apps { + padding-top: 2px; } + #dashtodockContainer.extended.right.shrink #dash #dashtodockDashContainer #dashtodockDashScrollview:first-child { + padding-top: 2px; } + #dashtodockContainer.extended.right.shrink #dash #dashtodockDashContainer > :last-child { + /* Use this instead of #dashtodockDashScrollview rule to apply the + * padding via the last app-icon item */ } + #dashtodockContainer.extended.right.shrink #dash #dashtodockDashContainer > :last-child .show-apps { + padding-bottom: 2px; } + #dashtodockContainer.extended.right.shrink #dash #dashtodockDashContainer #dashtodockDashScrollview:last-child { + padding-bottom: 2px; } + +#dashtodockContainer.extended.right.shrink #dash .dash-item-container .app-well-app, +#dashtodockContainer.extended.right.shrink #dash .dash-item-container .show-apps { + padding-right: 3px; } + +#dashtodockContainer.extended.right.shrink.fixed #dash .dash-background { + margin-left: 0; } + +#dashtodockContainer.top.shrink #dash .dash-background { + margin-top: 4px; + margin-bottom: 0; } + +#dashtodockContainer.straight-corner #dash .dash-background, +#dashtodockContainer.shrink.straight-corner #dash .dash-background { + border-radius: 0px; } + +/* Scrollview style */ +.bottom #dashtodockDashScrollview, +.top #dashtodockDashScrollview { + -st-hfade-offset: 24px; } + +.left #dashtodockDashScrollview, +.right #dashtodockDashScrollview { + -st-vfade-offset: 24px; } + +#dashtodockContainer.running-dots .dash-item-container > StButton, +#dashtodockContainer.dashtodock .dash-item-container > StButton { + transition-duration: 250; + background-size: contain; } + +/* Running and focused application style */ +#dashtodockContainer.running-dots .app-well-app.running > .overview-icon, +#dashtodockContainer.dashtodock .app-well-app.running > .overview-icon { + background-image: none; } + +#dashtodockContainer.running-dots .app-well-app.focused .overview-icon, +#dashtodockContainer.dashtodock .app-well-app.focused .overview-icon { + background-color: rgba(238, 238, 236, 0.2); } + +#dashtodockContainer.dashtodock #dash .dash-background { + background: #2e3436; } + +#dashtodockContainer.dashtodock .progress-bar { + /* Customization of the progress bar style, e.g.: + -progress-bar-background: rgba(0.8, 0.8, 0.8, 1); + -progress-bar-border: rgba(0.9, 0.9, 0.9, 1); + */ } + +#dashtodockContainer.top #dash .placeholder, +#dashtodockContainer.bottom #dash .placeholder { + width: 32px; + height: 1px; } + +/* + * This is applied to a dummy actor. Only the alpha value for the background and border color + * and the transition-duration are used + */ +#dashtodockContainer.dummy-opaque { + background-color: rgba(0, 0, 0, 0.8); + border-color: rgba(0, 0, 0, 0.4); + transition-duration: 300ms; } + +/* + * This is applied to a dummy actor. Only the alpha value for the background and border color + * and the transition-duration are used + */ +#dashtodockContainer.dummy-transparent { + background-color: rgba(0, 0, 0, 0.2); + border-color: rgba(0, 0, 0, 0.1); + transition-duration: 500ms; } + +#dashtodockContainer .number-overlay { + color: white; + background-color: rgba(0, 0, 0, 0.8); + text-align: center; } + +#dashtodockContainer .notification-badge { + color: white; + background-color: red; + padding: 0.2em 0.5em; + border-radius: 1em; + font-weight: bold; + text-align: center; + margin: 2px; } + +#dashtodockPreviewSeparator.popup-separator-menu-item-horizontal { + width: 1px; + height: auto; + border-right-width: 1px; + margin: 32px 0px; } + +.dashtodock-app-well-preview-menu-item { + padding: 1em 1em 0.5em 1em; } + +#dashtodockContainer .metro .overview-icon { + border-radius: 0px; } + +#dashtodockContainer.bottom .metro.running2.focused, +#dashtodockContainer.top .metro.running2.focused { + background-image: url("./media/highlight_stacked_bg.svg"); + background-position: 0px 0px; + background-size: contain; } + +#dashtodockContainer.left .metro.running2.focused, +#dashtodockContainer.right .metro.running2.focused { + background-image: url("./media/highlight_stacked_bg_h.svg"); + background-position: 0px 0px; + background-size: contain; } + +#dashtodockContainer.bottom .metro.running3.focused, +#dashtodockContainer.top .metro.running3.focused { + background-image: url("./media/highlight_stacked_bg.svg"); + background-position: 0px 0px; + background-size: contain; } + +#dashtodockContainer.left .metro.running3.focused, +#dashtodockContainer.right .metro.running3.focused { + background-image: url("./media/highlight_stacked_bg_h.svg"); + background-position: 0px 0px; + background-size: contain; } + +#dashtodockContainer.bottom .metro.running4.focused, +#dashtodockContainer.top .metro.running4.focused { + background-image: url("./media/highlight_stacked_bg.svg"); + background-position: 0px 0px; + background-size: contain; } + +#dashtodockContainer.left .metro.running4.focused, +#dashtodockContainer.right .metro.running4.focused { + background-image: url("./media/highlight_stacked_bg_h.svg"); + background-position: 0px 0px; + background-size: contain; } diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/theming.js b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/theming.js new file mode 100644 index 0000000..eba0370 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/theming.js @@ -0,0 +1,561 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Signals = imports.signals; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; +const St = imports.gi.St; +const Clutter = imports.gi.Clutter; + +const AppDisplay = imports.ui.appDisplay; +const AppFavorites = imports.ui.appFavorites; +const Dash = imports.ui.dash; +const DND = imports.ui.dnd; +const IconGrid = imports.ui.iconGrid; +const Main = imports.ui.main; +const PopupMenu = imports.ui.popupMenu; +const Util = imports.misc.util; +const Workspace = imports.ui.workspace; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Docking = Me.imports.docking; +const Utils = Me.imports.utils; + +/* + * DEFAULT: transparency given by theme + * FIXED: constant transparency chosen by user + * DYNAMIC: apply 'transparent' style when no windows are close to the dock + * */ +const TransparencyMode = { + DEFAULT: 0, + FIXED: 1, + DYNAMIC: 3 +}; + +/** + * Manage theme customization and custom theme support + */ +var ThemeManager = class DashToDock_ThemeManager { + + constructor(dock) { + this._signalsHandler = new Utils.GlobalSignalsHandler(this); + this._bindSettingsChanges(); + this._actor = dock; + this._dash = dock.dash; + + // initialize colors with generic values + this._customizedBackground = {red: 0, green: 0, blue: 0, alpha: 0}; + this._customizedBorder = {red: 0, green: 0, blue: 0, alpha: 0}; + this._transparency = new Transparency(dock); + + this._signalsHandler.add([ + // When theme changes re-obtain default background color + St.ThemeContext.get_for_stage (global.stage), + 'changed', + this.updateCustomTheme.bind(this) + ], [ + // update :overview pseudoclass + Main.overview, + 'showing', + this._onOverviewShowing.bind(this) + ], [ + Main.overview, + 'hiding', + this._onOverviewHiding.bind(this) + ]); + + this._updateCustomStyleClasses(); + + // destroy themeManager when the managed actor is destroyed (e.g. extension unload) + // in order to disconnect signals + this._signalsHandler.add(this._actor, 'destroy', () => this.destroy()); + } + + destroy() { + this.emit('destroy'); + this._transparency.destroy(); + this._destroyed = true; + } + + _onOverviewShowing() { + this._actor.add_style_pseudo_class('overview'); + } + + _onOverviewHiding() { + this._actor.remove_style_pseudo_class('overview'); + } + + _updateDashOpacity() { + let newAlpha = Docking.DockManager.settings.get_double('background-opacity'); + + let [backgroundColor, borderColor] = this._getDefaultColors(); + + if (backgroundColor==null) + return; + + // Get the background and border alphas. We check the background alpha + // for a minimum of .001 to prevent division by 0 errors + let backgroundAlpha = Math.max(Math.round(backgroundColor.alpha/2.55)/100, .001); + let borderAlpha = Math.round(borderColor.alpha/2.55)/100; + + // The border and background alphas should remain in sync + // We also limit the borderAlpha to a maximum of 1 (full opacity) + borderAlpha = Math.min((borderAlpha/backgroundAlpha)*newAlpha, 1); + + this._customizedBackground = 'rgba(' + + backgroundColor.red + ',' + + backgroundColor.green + ',' + + backgroundColor.blue + ',' + + newAlpha + ')'; + + this._customizedBorder = 'rgba(' + + borderColor.red + ',' + + borderColor.green + ',' + + borderColor.blue + ',' + + borderAlpha + ')'; + + } + + _getDefaultColors() { + // Prevent shell crash if the actor is not on the stage. + // It happens enabling/disabling repeatedly the extension + if (!this._dash._container.get_stage()) + return [null, null]; + + // Remove custom style + let oldStyle = this._dash._container.get_style(); + this._dash._container.set_style(null); + + let themeNode = this._dash._container.get_theme_node(); + this._dash._container.set_style(oldStyle); + + let backgroundColor = themeNode.get_background_color(); + + // Just in case the theme has different border colors .. + // We want to find the inside border-color of the dock because it is + // the side most visible to the user. We do this by finding the side + // opposite the position + let position = Utils.getPosition(); + let side = position + 2; + if (side > 3) + side = Math.abs(side - 4); + + let borderColor = themeNode.get_border_color(side); + + return [backgroundColor, borderColor]; + } + + _updateDashColor() { + // Retrieve the color. If needed we will adjust it before passing it to + // this._transparency. + let [backgroundColor, borderColor] = this._getDefaultColors(); + + if (backgroundColor==null) + return; + + let settings = Docking.DockManager.settings; + + if (settings.get_boolean('custom-background-color')) { + // When applying a custom color, we need to check the alpha value, + // if not the opacity will always be overridden by the color below. + // Note that if using 'dynamic' transparency modes, + // the opacity will be set by the opaque/transparent styles anyway. + let newAlpha = Math.round(backgroundColor.alpha/2.55)/100; + + backgroundColor = settings.get_string('background-color'); + // backgroundColor is a string like rgb(0,0,0) + const [ret, color] = Clutter.Color.from_string(backgroundColor); + if (!ret) { + logError(new Error(`${backgroundColor} is not a valid color string`)); + return; + } + + if (settings.get_enum('transparency-mode') == TransparencyMode.FIXED) { + newAlpha = settings.get_double('background-opacity'); + this._customizedBackground = + `rgba(${color.red}, ${color.blue}, ${color.green}, ${newAlpha})`; + } else { + this._customizedBackground = backgroundColor; + } + + this._customizedBorder = this._customizedBackground; + + color.alpha = newAlpha * 255; + this._transparency.setColor(color); + } else { + // backgroundColor is a Clutter.Color object + this._transparency.setColor(backgroundColor); + } + } + + _updateCustomStyleClasses() { + let settings = Docking.DockManager.settings; + + if (settings.get_boolean('apply-custom-theme')) + this._actor.add_style_class_name('dashtodock'); + else + this._actor.remove_style_class_name('dashtodock'); + + if (settings.get_boolean('custom-theme-shrink')) + this._actor.add_style_class_name('shrink'); + else + this._actor.remove_style_class_name('shrink'); + + if (settings.get_enum('running-indicator-style') !== 0) + this._actor.add_style_class_name('running-dots'); + else + this._actor.remove_style_class_name('running-dots'); + + // If not the built-in theme option is not selected + if (!settings.get_boolean('apply-custom-theme')) { + if (settings.get_boolean('force-straight-corner')) + this._actor.add_style_class_name('straight-corner'); + else + this._actor.remove_style_class_name('straight-corner'); + } else { + this._actor.remove_style_class_name('straight-corner'); + } + } + + updateCustomTheme() { + if (this._destroyed) + throw new Error(`Impossible to update a destroyed ${this.constructor.name}`); + this._updateCustomStyleClasses(); + this._updateDashOpacity(); + this._updateDashColor(); + this._adjustTheme(); + this.emit('updated'); + } + + /** + * Reimported back and adapted from atomdock + */ + _adjustTheme() { + // Prevent shell crash if the actor is not on the stage. + // It happens enabling/disabling repeatedly the extension + if (!this._dash._background.get_stage()) + return; + + let settings = Docking.DockManager.settings; + + // Remove prior style edits + this._dash._background.set_style(null); + this._transparency.disable(); + + // If built-in theme is enabled do nothing else + if (settings.get_boolean('apply-custom-theme')) + return; + + let newStyle = ''; + let position = Utils.getPosition(settings); + + // obtain theme border settings + let themeNode = this._dash._background.get_theme_node(); + let borderColor = themeNode.get_border_color(St.Side.TOP); + let borderWidth = themeNode.get_border_width(St.Side.TOP); + + // We're copying border and corner styles to left border and top-left + // corner, also removing bottom border and bottom-right corner styles + let borderInner = ''; + let borderMissingStyle = ''; + + if (this._rtl && (position != St.Side.RIGHT)) + borderMissingStyle = 'border-right: ' + borderWidth + 'px solid ' + + borderColor.to_string() + ';'; + else if (!this._rtl && (position != St.Side.LEFT)) + borderMissingStyle = 'border-left: ' + borderWidth + 'px solid ' + + borderColor.to_string() + ';'; + + newStyle = borderMissingStyle; + + if (newStyle) { + // I do call set_style possibly twice so that only the background gets the transition. + // The transition-property css rules seems to be unsupported + this._dash._background.set_style(newStyle); + } + + // Customize background + let fixedTransparency = settings.get_enum('transparency-mode') == TransparencyMode.FIXED; + let defaultTransparency = settings.get_enum('transparency-mode') == TransparencyMode.DEFAULT; + if (!defaultTransparency && !fixedTransparency) { + this._transparency.enable(); + } + else if (!defaultTransparency || settings.get_boolean('custom-background-color')) { + newStyle = newStyle + 'background-color:'+ this._customizedBackground + '; ' + + 'border-color:'+ this._customizedBorder + '; ' + + 'transition-delay: 0s; transition-duration: 0.250s;'; + this._dash._background.set_style(newStyle); + } + } + + _bindSettingsChanges() { + let keys = ['transparency-mode', + 'customize-alphas', + 'min-alpha', + 'max-alpha', + 'background-opacity', + 'custom-background-color', + 'background-color', + 'apply-custom-theme', + 'custom-theme-shrink', + 'custom-theme-running-dots', + 'extend-height', + 'force-straight-corner']; + + this._signalsHandler.add(...keys.map(key => [ + Docking.DockManager.settings, + `changed::${key}`, + () => this.updateCustomTheme(), + ])); + } +}; +Signals.addSignalMethods(ThemeManager.prototype); + +/** + * The following class is based on the following upstream commit: + * https://git.gnome.org/browse/gnome-shell/commit/?id=447bf55e45b00426ed908b1b1035f472c2466956 + * Transparency when free-floating + */ +var Transparency = class DashToDock_Transparency { + + constructor(dock) { + this._dash = dock.dash; + this._actor = this._dash._container; + this._backgroundActor = this._dash._background; + this._dockActor = dock; + this._dock = dock; + this._panel = Main.panel; + this._position = Utils.getPosition(); + + // All these properties are replaced with the ones in the .dummy-opaque and .dummy-transparent css classes + this._backgroundColor = '0,0,0'; + this._transparentAlpha = '0.2'; + this._opaqueAlpha = '1'; + this._transparentAlphaBorder = '0.1'; + this._opaqueAlphaBorder = '0.5'; + this._transparentTransition = '0ms'; + this._opaqueTransition = '0ms'; + this._base_actor_style = ""; + + this._signalsHandler = new Utils.GlobalSignalsHandler(); + this._trackedWindows = new Map(); + } + + enable() { + // ensure I never double-register/inject + // although it should never happen + this.disable(); + + this._base_actor_style = this._actor.get_style(); + if (this._base_actor_style == null) { + this._base_actor_style = ""; + } + + this._signalsHandler.addWithLabel('transparency', [ + global.window_group, + 'actor-added', + this._onWindowActorAdded.bind(this) + ], [ + global.window_group, + 'actor-removed', + this._onWindowActorRemoved.bind(this) + ], [ + global.window_manager, + 'switch-workspace', + this._updateSolidStyle.bind(this) + ], [ + Main.overview, + 'hiding', + this._updateSolidStyle.bind(this) + ], [ + Main.overview, + 'showing', + this._updateSolidStyle.bind(this) + ]); + + // Window signals + global.window_group.get_children().filter(function(child) { + // An irrelevant window actor ('Gnome-shell') produces an error when the signals are + // disconnected, therefore do not add signals to it. + return child instanceof Meta.WindowActor && + child.get_meta_window().get_wm_class() !== 'Gnome-shell'; + }).forEach(function(win) { + this._onWindowActorAdded(null, win); + }, this); + + if (this._actor.get_stage()) + this._updateSolidStyle(); + + this._updateStyles(); + this._updateSolidStyle(); + + this.emit('transparency-enabled'); + } + + disable() { + // ensure I never double-register/inject + // although it should never happen + this._signalsHandler.removeWithLabel('transparency'); + + for (let key of this._trackedWindows.keys()) + this._trackedWindows.get(key).forEach(id => { + key.disconnect(id); + }); + this._trackedWindows.clear(); + + this.emit('transparency-disabled'); + } + + destroy() { + this.disable(); + this._signalsHandler.destroy(); + } + + _onWindowActorAdded(container, metaWindowActor) { + let signalIds = []; + ['notify::allocation', 'notify::visible'].forEach(s => { + signalIds.push(metaWindowActor.connect(s, this._updateSolidStyle.bind(this))); + }); + this._trackedWindows.set(metaWindowActor, signalIds); + } + + _onWindowActorRemoved(container, metaWindowActor) { + if (!this._trackedWindows.get(metaWindowActor)) + return; + + this._trackedWindows.get(metaWindowActor).forEach(id => { + metaWindowActor.disconnect(id); + }); + this._trackedWindows.delete(metaWindowActor); + this._updateSolidStyle(); + } + + _updateSolidStyle() { + let isNear = this._dockIsNear(); + if (isNear) { + this._backgroundActor.set_style(this._opaque_style); + this._dockActor.remove_style_class_name('transparent'); + this._dockActor.add_style_class_name('opaque'); + } + else { + this._backgroundActor.set_style(this._transparent_style); + this._dockActor.remove_style_class_name('opaque'); + this._dockActor.add_style_class_name('transparent'); + } + + this.emit('solid-style-updated', isNear); + } + + _dockIsNear() { + if (this._dockActor.has_style_pseudo_class('overview')) + return false; + /* Get all the windows in the active workspace that are in the primary monitor and visible */ + let activeWorkspace = global.workspace_manager.get_active_workspace(); + let dash = this._dash; + let windows = activeWorkspace.list_windows().filter(function(metaWindow) { + return metaWindow.get_monitor() === dash._monitorIndex && + metaWindow.showing_on_its_workspace() && + metaWindow.get_window_type() != Meta.WindowType.DESKTOP; + }); + + /* Check if at least one window is near enough to the panel. + * If the dock is hidden, we need to account for the space it would take + * up when it slides out. This is avoid an ugly transition. + * */ + let factor = 0; + if (!Docking.DockManager.settings.dockFixed && + this._dock.getDockState() == Docking.State.HIDDEN) + factor = 1; + let [leftCoord, topCoord] = this._actor.get_transformed_position(); + let threshold; + if (this._position === St.Side.LEFT) + threshold = leftCoord + this._actor.get_width() * (factor + 1); + else if (this._position === St.Side.RIGHT) + threshold = leftCoord - this._actor.get_width() * factor; + else if (this._position === St.Side.TOP) + threshold = topCoord + this._actor.get_height() * (factor + 1); + else + threshold = topCoord - this._actor.get_height() * factor; + + let scale = St.ThemeContext.get_for_stage(global.stage).scale_factor; + let isNearEnough = windows.some((metaWindow) => { + let coord; + if (this._position === St.Side.LEFT) { + coord = metaWindow.get_frame_rect().x; + return coord < threshold + 5 * scale; + } + else if (this._position === St.Side.RIGHT) { + coord = metaWindow.get_frame_rect().x + metaWindow.get_frame_rect().width; + return coord > threshold - 5 * scale; + } + else if (this._position === St.Side.TOP) { + coord = metaWindow.get_frame_rect().y; + return coord < threshold + 5 * scale; + } + else { + coord = metaWindow.get_frame_rect().y + metaWindow.get_frame_rect().height; + return coord > threshold - 5 * scale; + } + }); + + return isNearEnough; + } + + _updateStyles() { + this._getAlphas(); + + this._transparent_style = this._base_actor_style + + 'background-color: rgba(' + + this._backgroundColor + ', ' + this._transparentAlpha + ');' + + 'border-color: rgba(' + + this._backgroundColor + ', ' + this._transparentAlphaBorder + ');' + + 'transition-duration: ' + this._transparentTransition + 'ms;'; + + this._opaque_style = this._base_actor_style + + 'background-color: rgba(' + + this._backgroundColor + ', ' + this._opaqueAlpha + ');' + + 'border-color: rgba(' + + this._backgroundColor + ',' + this._opaqueAlphaBorder + ');' + + 'transition-duration: ' + this._opaqueTransition + 'ms;'; + + this.emit('styles-updated'); + } + + setColor(color) { + this._backgroundColor = color.red + ',' + color.green + ',' + color.blue; + this._updateStyles(); + } + + _getAlphas() { + // Create dummy object and add to the uiGroup to get it to the stage + let dummyObject = new St.Bin({ + name: 'dashtodockContainer', + }); + Main.uiGroup.add_child(dummyObject); + + dummyObject.add_style_class_name('dummy-opaque'); + let themeNode = dummyObject.get_theme_node(); + this._opaqueAlpha = themeNode.get_background_color().alpha / 255; + this._opaqueAlphaBorder = themeNode.get_border_color(0).alpha / 255; + this._opaqueTransition = themeNode.get_transition_duration(); + + dummyObject.add_style_class_name('dummy-transparent'); + themeNode = dummyObject.get_theme_node(); + this._transparentAlpha = themeNode.get_background_color().alpha / 255; + this._transparentAlphaBorder = themeNode.get_border_color(0).alpha / 255; + this._transparentTransition = themeNode.get_transition_duration(); + + Main.uiGroup.remove_child(dummyObject); + + let settings = Docking.DockManager.settings; + + if (settings.get_boolean('customize-alphas')) { + this._opaqueAlpha = settings.get_double('max-alpha'); + this._opaqueAlphaBorder = this._opaqueAlpha / 2; + this._transparentAlpha = settings.get_double('min-alpha'); + this._transparentAlphaBorder = this._transparentAlpha / 2; + } + } +}; +Signals.addSignalMethods(Transparency.prototype); diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/utils.js b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/utils.js new file mode 100644 index 0000000..0beaa75 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/utils.js @@ -0,0 +1,416 @@ + +const Gi = imports._gi; + +const Clutter = imports.gi.Clutter; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; +const Meta = imports.gi.Meta; +const St = imports.gi.St; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Docking = Me.imports.docking; + +var SignalsHandlerFlags = { + NONE: 0, + CONNECT_AFTER: 1 +}; + +/** + * Simplify global signals and function injections handling + * abstract class + */ +const BasicHandler = class DashToDock_BasicHandler { + + constructor(parentObject) { + this._storage = new Object(); + + if (parentObject) { + if (!(parentObject.connect instanceof Function)) + throw new TypeError('Not a valid parent object'); + + if (!(parentObject instanceof GObject.Object) || + GObject.signal_lookup('destroy', parentObject.constructor.$gtype)) { + this._parentObject = parentObject; + this._destroyId = parentObject.connect('destroy', () => this.destroy()); + } + } + } + + add(...args) { + // Convert arguments object to array, concatenate with generic + // Call addWithLabel with ags as if they were passed arguments + this.addWithLabel('generic', ...args); + } + + destroy() { + this._parentObject?.disconnect(this._destroyId); + this._parentObject = null; + + for( let label in this._storage ) + this.removeWithLabel(label); + } + + addWithLabel(label, ...args) { + let argsArray = [...args]; + if (argsArray.every(arg => !Array.isArray(arg))) + argsArray = [argsArray]; + + if (this._storage[label] == undefined) + this._storage[label] = new Array(); + + // Skip first element of the arguments + for (const argArray of argsArray) { + if (argArray.length < 3) + throw new Error('Unexpected number of arguments'); + let item = this._storage[label]; + try { + item.push(this._create(...argArray)); + } catch (e) { + logError(e); + } + } + } + + removeWithLabel(label) { + if (this._storage[label]) { + for (let i = 0; i < this._storage[label].length; i++) + this._remove(this._storage[label][i]); + + delete this._storage[label]; + } + } + + // Virtual methods to be implemented by subclass + + /** + * Create single element to be stored in the storage structure + */ + _create(_object, _element, _callback) { + throw new GObject.NotImplementedError(`_create in ${this.constructor.name}`); + } + + /** + * Correctly delete single element + */ + _remove(_item) { + throw new GObject.NotImplementedError(`_remove in ${this.constructor.name}`); + } +}; + +/** + * Manage global signals + */ +var GlobalSignalsHandler = class DashToDock_GlobalSignalHandler extends BasicHandler { + + _create(object, event, callback, flags = SignalsHandlerFlags.NONE) { + if (!object) + throw new Error('Impossible to connect to an invalid object'); + + let after = flags == SignalsHandlerFlags.CONNECT_AFTER; + let connector = after ? object.connect_after : object.connect; + + if (!connector) { + throw new Error(`Requested to connect to signal '${event}', ` + + `but no implementation for 'connect${after ? '_after' : ''}' `+ + `found in ${object.constructor.name}`); + } + + let id = connector.call(object, event, callback); + + return [object, id]; + } + + _remove(item) { + const [object, id] = item; + object.disconnect(id); + } +}; + +/** + * Color manipulation utilities + */ +var ColorUtils = class DashToDock_ColorUtils { + + // Darken or brigthen color by a fraction dlum + // Each rgb value is modified by the same fraction. + // Return "#rrggbb" string + static ColorLuminance(r, g, b, dlum) { + let rgbString = '#'; + + rgbString += ColorUtils._decimalToHex(Math.round(Math.min(Math.max(r*(1+dlum), 0), 255)), 2); + rgbString += ColorUtils._decimalToHex(Math.round(Math.min(Math.max(g*(1+dlum), 0), 255)), 2); + rgbString += ColorUtils._decimalToHex(Math.round(Math.min(Math.max(b*(1+dlum), 0), 255)), 2); + + return rgbString; + } + + // Convert decimal to an hexadecimal string adding the desired padding + static _decimalToHex(d, padding) { + let hex = d.toString(16); + while (hex.length < padding) + hex = '0'+ hex; + return hex; + } + + // Convert hsv ([0-1, 0-1, 0-1]) to rgb ([0-255, 0-255, 0-255]). + // Following algorithm in https://en.wikipedia.org/wiki/HSL_and_HSV + // here with h = [0,1] instead of [0, 360] + // Accept either (h,s,v) independently or {h:h, s:s, v:v} object. + // Return {r:r, g:g, b:b} object. + static HSVtoRGB(h, s, v) { + if (arguments.length === 1) { + s = h.s; + v = h.v; + h = h.h; + } + + let r,g,b; + let c = v*s; + let h1 = h*6; + let x = c*(1 - Math.abs(h1 % 2 - 1)); + let m = v - c; + + if (h1 <=1) + r = c + m, g = x + m, b = m; + else if (h1 <=2) + r = x + m, g = c + m, b = m; + else if (h1 <=3) + r = m, g = c + m, b = x + m; + else if (h1 <=4) + r = m, g = x + m, b = c + m; + else if (h1 <=5) + r = x + m, g = m, b = c + m; + else + r = c + m, g = m, b = x + m; + + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255) + }; + } + + // Convert rgb ([0-255, 0-255, 0-255]) to hsv ([0-1, 0-1, 0-1]). + // Following algorithm in https://en.wikipedia.org/wiki/HSL_and_HSV + // here with h = [0,1] instead of [0, 360] + // Accept either (r,g,b) independently or {r:r, g:g, b:b} object. + // Return {h:h, s:s, v:v} object. + static RGBtoHSV(r, g, b) { + if (arguments.length === 1) { + r = r.r; + g = r.g; + b = r.b; + } + + let h,s,v; + + let M = Math.max(r, g, b); + let m = Math.min(r, g, b); + let c = M - m; + + if (c == 0) + h = 0; + else if (M == r) + h = ((g-b)/c) % 6; + else if (M == g) + h = (b-r)/c + 2; + else + h = (r-g)/c + 4; + + h = h/6; + v = M/255; + if (M !== 0) + s = c/M; + else + s = 0; + + return { + h: h, + s: s, + v: v + }; + } +}; + +/** + * Manage function injection: both instances and prototype can be overridden + * and restored + */ +var InjectionsHandler = class DashToDock_InjectionsHandler extends BasicHandler { + + _create(object, name, injectedFunction) { + let original = object[name]; + + if (!(original instanceof Function)) + throw new Error(`Virtual function ${name} is not available for ${prototype}`); + + object[name] = function(...args) { return injectedFunction.call(this, original, ...args) }; + return [object, name, original]; + } + + _remove(item) { + const [object, name, original] = item; + object[name] = original; + } +}; + +/** + * Manage vfunction injection: both instances and prototype can be overridden + * and restored + */ +var VFuncInjectionsHandler = class DashToDock_VFuncInjectionsHandler extends BasicHandler { + + _create(prototype, name, injectedFunction) { + const original = prototype[`vfunc_${name}`]; + if (!(original instanceof Function)) + throw new Error(`Virtual function ${name} is not available for ${prototype}`); + prototype[Gi.hook_up_vfunc_symbol](name, injectedFunction); + return [prototype, name]; + } + + _remove(item) { + const [prototype, name] = item; + const originalVFunc = prototype[`vfunc_${name}`]; + try { + // This may fail if trying to reset to a never-overridden vfunc + // as gjs doesn't consider it a function, even if it's true that + // originalVFunc instanceof Function. + prototype[Gi.hook_up_vfunc_symbol](name, originalVFunc); + } catch { + try { + prototype[Gi.hook_up_vfunc_symbol](name, function (...args) { + return originalVFunc.call(this, ...args); + }); + } catch (e) { + logError(e, `Removing vfunc_${name}`); + } + } + } +}; + +/** + * Manage properties injection: both instances and prototype can be overridden + * and restored + */ +var PropertyInjectionsHandler = class DashToDock_PropertyInjectionsHandler extends BasicHandler { + + _create(instance, name, injectedPropertyDescriptor) { + if (!(name in instance)) + throw new Error(`Object ${instance} has no '${name}' property`); + + const prototype = instance.constructor.prototype; + const originalPropertyDescriptor = Object.getOwnPropertyDescriptor(prototype, name) ?? + Object.getOwnPropertyDescriptor(instance, name); + + Object.defineProperty(instance, name, { + ...originalPropertyDescriptor, + ...injectedPropertyDescriptor, + ...{ configurable: true }, + }); + return [instance, name, originalPropertyDescriptor]; + } + + _remove(item) { + const [instance, name, originalPropertyDescriptor] = item; + if (originalPropertyDescriptor) + Object.defineProperty(instance, name, originalPropertyDescriptor); + else + delete instance[name]; + } +}; + +/** + * Return the actual position reverseing left and right in rtl + */ +function getPosition() { + let position = Docking.DockManager.settings.get_enum('dock-position'); + if (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL) { + if (position == St.Side.LEFT) + position = St.Side.RIGHT; + else if (position == St.Side.RIGHT) + position = St.Side.LEFT; + } + return position; +} + +function getPreviewScale() { + return Docking.DockManager.settings.get_double('preview-size-scale'); +} + +function drawRoundedLine(cr, x, y, width, height, isRoundLeft, isRoundRight, stroke, fill) { + if (height > width) { + y += Math.floor((height - width) / 2.0); + height = width; + } + + height = 2.0 * Math.floor(height / 2.0); + + var leftRadius = isRoundLeft ? height / 2.0 : 0.0; + var rightRadius = isRoundRight ? height / 2.0 : 0.0; + + cr.moveTo(x + width - rightRadius, y); + cr.lineTo(x + leftRadius, y); + if (isRoundLeft) + cr.arcNegative(x + leftRadius, y + leftRadius, leftRadius, -Math.PI/2, Math.PI/2); + else + cr.lineTo(x, y + height); + cr.lineTo(x + width - rightRadius, y + height); + if (isRoundRight) + cr.arcNegative(x + width - rightRadius, y + rightRadius, rightRadius, Math.PI/2, -Math.PI/2); + else + cr.lineTo(x + width, y); + cr.closePath(); + + if (fill != null) { + cr.setSource(fill); + cr.fillPreserve(); + } + if (stroke != null) + cr.setSource(stroke); + cr.stroke(); +} + +/** + * Convert a signal handler with n value parameters (that is, excluding the + * signal source parameter) to an array of n handlers that are each responsible + * for receiving one of the n values and calling the original handler with the + * most up-to-date arguments. + */ +function splitHandler(handler) { + if (handler.length > 30) { + throw new Error("too many parameters"); + } + const count = handler.length - 1; + let missingValueBits = (1 << count) - 1; + const values = Array.from({ length: count }); + return values.map((_ignored, i) => { + const mask = ~(1 << i); + return (obj, value) => { + values[i] = value; + missingValueBits &= mask; + if (missingValueBits === 0) { + handler(obj, ...values); + } + }; + }); +} + +var IconTheme = class DashToDockIconTheme { + constructor() { + const settings = St.Settings.get(); + this._iconTheme = new Gtk.IconTheme(); + this._iconTheme.set_custom_theme(settings.gtkIconTheme); + this._changesId = settings.connect('notify::gtk-icon-theme', () => { + this._iconTheme.set_custom_theme(settings.gtkIconTheme); + }); + } + + get iconTheme() { + return this._iconTheme; + } + + destroy() { + St.Settings.get().disconnect(this._changesId); + this._iconTheme = null; + } +} diff --git a/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/windowPreview.js b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/windowPreview.js new file mode 100644 index 0000000..e33f5df --- /dev/null +++ b/.local/share/gnome-shell/extensions/dash-to-dock@micxgx.gmail.com/windowPreview.js @@ -0,0 +1,607 @@ +/* + * Credits: + * This file is based on code from the Dash to Panel extension by Jason DeRose + * and code from the Taskbar extension by Zorin OS + * Some code was also adapted from the upstream Gnome Shell source code. + */ +const Clutter = imports.gi.Clutter; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const St = imports.gi.St; +const Main = imports.ui.main; + +const BoxPointer = imports.ui.boxpointer; +const Params = imports.misc.params; +const PopupMenu = imports.ui.popupMenu; +const Workspace = imports.ui.workspace; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Utils = Me.imports.utils; + +const PREVIEW_MAX_WIDTH = 250; +const PREVIEW_MAX_HEIGHT = 150; + +const PREVIEW_ANIMATION_DURATION = 250; + +var WindowPreviewMenu = class DashToDock_WindowPreviewMenu extends PopupMenu.PopupMenu { + + constructor(source) { + super(source, 0.5, Utils.getPosition()); + + // We want to keep the item hovered while the menu is up + this.blockSourceEvents = true; + + this._source = source; + this._app = this._source.app; + let monitorIndex = this._source.monitorIndex; + + this.actor.add_style_class_name('app-well-menu'); + this.actor.add_style_class_name('app-menu'); + this.actor.set_style('max-width: ' + (Main.layoutManager.monitors[monitorIndex].width - 22) + 'px; ' + + 'max-height: ' + (Main.layoutManager.monitors[monitorIndex].height - 22) + 'px;'); + this.actor.hide(); + + // Chain our visibility and lifecycle to that of the source + this._mappedId = this._source.connect('notify::mapped', () => { + if (!this._source.mapped) + this.close(); + }); + this._destroyId = this._source.connect('destroy', this.destroy.bind(this)); + + Main.uiGroup.add_actor(this.actor); + + this.connect('destroy', this._onDestroy.bind(this)); + } + + _redisplay() { + if (this._previewBox) + this._previewBox.destroy(); + this._previewBox = new WindowPreviewList(this._source); + this.addMenuItem(this._previewBox); + this._previewBox._redisplay(); + } + + popup() { + let windows = this._source.getInterestingWindows(); + if (windows.length > 0) { + this._redisplay(); + this.open(BoxPointer.PopupAnimation.FULL); + this.actor.navigate_focus(null, St.DirectionType.TAB_FORWARD, false); + this._source.emit('sync-tooltip'); + } + } + + _onDestroy() { + if (this._mappedId) + this._source.disconnect(this._mappedId); + + if (this._destroyId) + this._source.disconnect(this._destroyId); + } +}; + +var WindowPreviewList = class DashToDock_WindowPreviewList extends PopupMenu.PopupMenuSection { + + constructor(source) { + super(); + this.actor = new St.ScrollView({ + name: 'dashtodockWindowScrollview', + hscrollbar_policy: St.PolicyType.NEVER, + vscrollbar_policy: St.PolicyType.NEVER, + enable_mouse_scrolling: true + }); + + this.actor.connect('scroll-event', this._onScrollEvent.bind(this)); + + let position = Utils.getPosition(); + this.isHorizontal = position == St.Side.BOTTOM || position == St.Side.TOP; + this.box.set_vertical(!this.isHorizontal); + this.box.set_name('dashtodockWindowList'); + this.actor.add_actor(this.box); + this.actor._delegate = this; + + this._shownInitially = false; + + this._source = source; + this.app = source.app; + + this._redisplayId = Main.initializeDeferredWork(this.actor, this._redisplay.bind(this)); + + this.actor.connect('destroy', this._onDestroy.bind(this)); + this._stateChangedId = this.app.connect('windows-changed', + this._queueRedisplay.bind(this)); + } + + _queueRedisplay () { + Main.queueDeferredWork(this._redisplayId); + } + + _onScrollEvent(actor, event) { + // Event coordinates are relative to the stage but can be transformed + // as the actor will only receive events within his bounds. + let stage_x, stage_y, ok, event_x, event_y, actor_w, actor_h; + [stage_x, stage_y] = event.get_coords(); + [ok, event_x, event_y] = actor.transform_stage_point(stage_x, stage_y); + [actor_w, actor_h] = actor.get_size(); + + // If the scroll event is within a 1px margin from + // the relevant edge of the actor, let the event propagate. + if (event_y >= actor_h - 2) + return Clutter.EVENT_PROPAGATE; + + // Skip to avoid double events mouse + if (event.is_pointer_emulated()) + return Clutter.EVENT_STOP; + + let adjustment, delta; + + if (this.isHorizontal) + adjustment = this.actor.get_hscroll_bar().get_adjustment(); + else + adjustment = this.actor.get_vscroll_bar().get_adjustment(); + + let increment = adjustment.step_increment; + + switch ( event.get_scroll_direction() ) { + case Clutter.ScrollDirection.UP: + delta = -increment; + break; + case Clutter.ScrollDirection.DOWN: + delta = +increment; + break; + case Clutter.ScrollDirection.SMOOTH: + let [dx, dy] = event.get_scroll_delta(); + delta = dy*increment; + delta += dx*increment; + break; + + } + + adjustment.set_value(adjustment.get_value() + delta); + + return Clutter.EVENT_STOP; + } + + _onDestroy() { + this.app.disconnect(this._stateChangedId); + this._stateChangedId = 0; + } + + _createPreviewItem(window) { + let preview = new WindowPreviewMenuItem(window); + return preview; + } + + _redisplay () { + let children = this._getMenuItems().filter(function(actor) { + return actor._window; + }); + + // Windows currently on the menu + let oldWin = children.map(function(actor) { + return actor._window; + }); + + // All app windows with a static order + let newWin = this._source.getInterestingWindows().sort(function(a, b) { + return a.get_stable_sequence() > b.get_stable_sequence(); + }); + + let addedItems = []; + let removedActors = []; + + let newIndex = 0; + let oldIndex = 0; + + while (newIndex < newWin.length || oldIndex < oldWin.length) { + // No change at oldIndex/newIndex + if (oldWin[oldIndex] && + oldWin[oldIndex] == newWin[newIndex]) { + oldIndex++; + newIndex++; + continue; + } + + // Window removed at oldIndex + if (oldWin[oldIndex] && + newWin.indexOf(oldWin[oldIndex]) == -1) { + removedActors.push(children[oldIndex]); + oldIndex++; + continue; + } + + // Window added at newIndex + if (newWin[newIndex] && + oldWin.indexOf(newWin[newIndex]) == -1) { + addedItems.push({ item: this._createPreviewItem(newWin[newIndex]), + pos: newIndex }); + newIndex++; + continue; + } + + // Window moved + let insertHere = newWin[newIndex + 1] && + newWin[newIndex + 1] == oldWin[oldIndex]; + let alreadyRemoved = removedActors.reduce(function(result, actor) { + let removedWin = actor._window; + return result || removedWin == newWin[newIndex]; + }, false); + + if (insertHere || alreadyRemoved) { + addedItems.push({ item: this._createPreviewItem(newWin[newIndex]), + pos: newIndex + removedActors.length }); + newIndex++; + } else { + removedActors.push(children[oldIndex]); + oldIndex++; + } + } + + for (let i = 0; i < addedItems.length; i++) + this.addMenuItem(addedItems[i].item, + addedItems[i].pos); + + for (let i = 0; i < removedActors.length; i++) { + let item = removedActors[i]; + if (this._shownInitially) + item._animateOutAndDestroy(); + else + item.actor.destroy(); + } + + // Skip animations on first run when adding the initial set + // of items, to avoid all items zooming in at once + let animate = this._shownInitially; + + if (!this._shownInitially) + this._shownInitially = true; + + for (let i = 0; i < addedItems.length; i++) + addedItems[i].item.show(animate); + + // Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=692744 + // Without it, StBoxLayout may use a stale size cache + this.box.queue_relayout(); + + if (newWin.length < 1) + this._getTopMenu().close(~0); + + // As for upstream: + // St.ScrollView always requests space horizontally for a possible vertical + // scrollbar if in AUTOMATIC mode. Doing better would require implementation + // of width-for-height in St.BoxLayout and St.ScrollView. This looks bad + // when we *don't* need it, so turn off the scrollbar when that's true. + // Dynamic changes in whether we need it aren't handled properly. + let needsScrollbar = this._needsScrollbar(); + let scrollbar_policy = needsScrollbar ? + St.PolicyType.AUTOMATIC : St.PolicyType.NEVER; + if (this.isHorizontal) + this.actor.hscrollbar_policy = scrollbar_policy; + else + this.actor.vscrollbar_policy = scrollbar_policy; + + if (needsScrollbar) + this.actor.add_style_pseudo_class('scrolled'); + else + this.actor.remove_style_pseudo_class('scrolled'); + } + + _needsScrollbar() { + let topMenu = this._getTopMenu(); + let topThemeNode = topMenu.actor.get_theme_node(); + if (this.isHorizontal) { + let [topMinWidth, topNaturalWidth] = topMenu.actor.get_preferred_width(-1); + let topMaxWidth = topThemeNode.get_max_width(); + return topMaxWidth >= 0 && topNaturalWidth >= topMaxWidth; + } else { + let [topMinHeight, topNaturalHeight] = topMenu.actor.get_preferred_height(-1); + let topMaxHeight = topThemeNode.get_max_height(); + return topMaxHeight >= 0 && topNaturalHeight >= topMaxHeight; + } + + } + + isAnimatingOut() { + return this.actor.get_children().reduce(function(result, actor) { + return result || actor.animatingOut; + }, false); + } +}; + +var WindowPreviewMenuItem = GObject.registerClass( +class WindowPreviewMenuItem extends PopupMenu.PopupBaseMenuItem { + _init(window, params) { + super._init(params); + + this._window = window; + this._destroyId = 0; + this._windowAddedId = 0; + [this._width, this._height, this._scale] = this._getWindowPreviewSize(); // This gets the actual windows size for the preview + + // We don't want this: it adds spacing on the left of the item. + this.remove_child(this._ornamentLabel); + this.add_style_class_name('dashtodock-app-well-preview-menu-item'); + + // Now we don't have to set PREVIEW_MAX_WIDTH and PREVIEW_MAX_HEIGHT as preview size - that made all kinds of windows either stretched or squished (aspect ratio problem) + this._cloneBin = new St.Bin(); + this._cloneBin.set_size(this._width*this._scale, this._height*this._scale); + + // TODO: improve the way the closebutton is layout. Just use some padding + // for the moment. + this._cloneBin.set_style('padding-bottom: 0.5em'); + + this.closeButton = new St.Button({ style_class: 'window-close', + x_expand: true, + y_expand: true}); + this.closeButton.add_actor(new St.Icon({ icon_name: 'window-close-symbolic' })); + this.closeButton.set_x_align(Clutter.ActorAlign.END); + this.closeButton.set_y_align(Clutter.ActorAlign.START); + + + this.closeButton.opacity = 0; + this.closeButton.connect('clicked', this._closeWindow.bind(this)); + + let overlayGroup = new Clutter.Actor({layout_manager: new Clutter.BinLayout(), y_expand: true }); + + overlayGroup.add_actor(this._cloneBin); + overlayGroup.add_actor(this.closeButton); + + let label = new St.Label({ text: window.get_title()}); + label.set_style('max-width: '+PREVIEW_MAX_WIDTH +'px'); + let labelBin = new St.Bin({ child: label, + x_align: Clutter.ActorAlign.CENTER, + }); + + this._windowTitleId = this._window.connect('notify::title', () => { + label.set_text(this._window.get_title()); + }); + + let box = new St.BoxLayout({ vertical: true, + reactive:true, + x_expand:true }); + box.add(overlayGroup); + box.add(labelBin); + this.add_actor(box); + + this._cloneTexture(window); + + this.connect('destroy', this._onDestroy.bind(this)); + } + + _getWindowPreviewSize() { + let mutterWindow = this._window.get_compositor_private(); + let [width, height] = mutterWindow.get_size(); + + let scale; + + if (Utils.getPreviewScale()) { + scale = Utils.getPreviewScale(); + } else { + // a simple example with 1680x1050: + // * 250/1680 = 0,1488 + // * 150/1050 = 0,1429 + // => scale is 0,1429 + scale = Math.min(1.0, PREVIEW_MAX_WIDTH/width, PREVIEW_MAX_HEIGHT/height) + } + + // width and height that we wanna multiply by scale + return [width, height, scale]; + } + + _cloneTexture(metaWin){ + + let mutterWindow = metaWin.get_compositor_private(); + + // Newly-created windows are added to a workspace before + // the compositor finds out about them... + // Moreover sometimes they return an empty texture, thus as a workarounf also check for it size + if (!mutterWindow || !mutterWindow.get_texture() || !mutterWindow.get_size()[0]) { + this._cloneTextureId = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { + // Check if there's still a point in getting the texture, + // otherwise this could go on indefinitely + if (metaWin.get_workspace()) + this._cloneTexture(metaWin); + this._cloneTextureId = 0; + return GLib.SOURCE_REMOVE; + }); + GLib.Source.set_name_by_id(this._cloneTextureId, '[dash-to-dock] this._cloneTexture'); + return; + } + + let clone = new Clutter.Clone ({ source: mutterWindow, + reactive: true, + width: this._width * this._scale, + height: this._height * this._scale }); + + // when the source actor is destroyed, i.e. the window closed, first destroy the clone + // and then destroy the menu item (do this animating out) + this._destroyId = mutterWindow.connect('destroy', () => { + clone.destroy(); + this._destroyId = 0; // avoid to try to disconnect this signal from mutterWindow in _onDestroy(), + // as the object was just destroyed + this._animateOutAndDestroy(); + }); + + this._clone = clone; + this._mutterWindow = mutterWindow; + this._cloneBin.set_child(this._clone); + + this._clone.connect('destroy', () => { + if (this._destroyId) { + mutterWindow.disconnect(this._destroyId); + this._destroyId = 0; + } + this._clone = null; + }) + } + + _windowCanClose() { + return this._window.can_close() && + !this._hasAttachedDialogs(); + } + + _closeWindow(actor) { + this._workspace = this._window.get_workspace(); + + // This mechanism is copied from the workspace.js upstream code + // It forces window activation if the windows don't get closed, + // for instance because asking user confirmation, by monitoring the opening of + // such additional confirmation window + this._windowAddedId = this._workspace.connect('window-added', + this._onWindowAdded.bind(this)); + + this.deleteAllWindows(); + } + + deleteAllWindows() { + // Delete all windows, starting from the bottom-most (most-modal) one + //let windows = this._window.get_compositor_private().get_children(); + let windows = this._clone.get_children(); + for (let i = windows.length - 1; i >= 1; i--) { + let realWindow = windows[i].source; + let metaWindow = realWindow.meta_window; + + metaWindow.delete(global.get_current_time()); + } + + this._window.delete(global.get_current_time()); + } + + _onWindowAdded(workspace, win) { + let metaWindow = this._window; + + if (win.get_transient_for() == metaWindow) { + workspace.disconnect(this._windowAddedId); + this._windowAddedId = 0; + + // use an idle handler to avoid mapping problems - + // see comment in Workspace._windowAdded + let activationEvent = Clutter.get_current_event(); + let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { + this.emit('activate', activationEvent); + return GLib.SOURCE_REMOVE; + }); + GLib.Source.set_name_by_id(id, '[dash-to-dock] this.emit'); + } + } + + _hasAttachedDialogs() { + // count trasient windows + let n=0; + this._window.foreach_transient(function(){n++;}); + return n>0; + } + + vfunc_key_focus_in() { + super.vfunc_key_focus_in(); + this._showCloseButton(); + } + + vfunc_key_focus_out() { + super.vfunc_key_focus_out(); + this._hideCloseButton(); + } + + vfunc_enter_event(crossingEvent) { + this._showCloseButton(); + return super.vfunc_enter_event(crossingEvent); + } + + vfunc_leave_event(crossingEvent) { + this._hideCloseButton(); + return super.vfunc_leave_event(crossingEvent); + } + + _idleToggleCloseButton() { + this._idleToggleCloseId = 0; + + this._hideCloseButton(); + + return GLib.SOURCE_REMOVE; + } + + _showCloseButton() { + + if (this._windowCanClose()) { + this.closeButton.show(); + this.closeButton.remove_all_transitions(); + this.closeButton.ease({ + opacity: 255, + duration: Workspace.WINDOW_OVERLAY_FADE_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD + }); + } + } + + _hideCloseButton() { + if (this.closeButton.has_pointer || + this.get_children().some(a => a.has_pointer)) + return; + + this.closeButton.remove_all_transitions(); + this.closeButton.ease({ + opacity: 0, + duration: Workspace.WINDOW_OVERLAY_FADE_TIME, + mode: Clutter.AnimationMode.EASE_IN_QUAD + }); + } + + show(animate) { + let fullWidth = this.get_width(); + + this.opacity = 0; + this.set_width(0); + + let time = animate ? PREVIEW_ANIMATION_DURATION : 0; + this.remove_all_transitions(); + this.ease({ + opacity: 255, + width: fullWidth, + duration: time, + mode: Clutter.AnimationMode.EASE_IN_OUT_QUAD, + }); + } + + _animateOutAndDestroy() { + this.remove_all_transitions(); + this.ease({ + opacity: 0, + duration: PREVIEW_ANIMATION_DURATION, + }); + + this.ease({ + width: 0, + height: 0, + duration: PREVIEW_ANIMATION_DURATION, + delay: PREVIEW_ANIMATION_DURATION, + onComplete: () => this.destroy() + }); + } + + activate() { + this._getTopMenu().close(); + Main.activateWindow(this._window); + } + + _onDestroy() { + if (this._cloneTextureId) { + GLib.source_remove(this._cloneTextureId); + this._cloneTextureId = 0; + } + + if (this._windowAddedId > 0) { + this._workspace.disconnect(this._windowAddedId); + this._windowAddedId = 0; + } + + if (this._destroyId > 0) { + this._mutterWindow.disconnect(this._destroyId); + this._destroyId = 0; + } + + if (this._windowTitleId > 0) { + this._window.disconnect(this._windowTitleId); + this._windowTitleId = 0; + } + } +}); diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/COPYING b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/COPYING new file mode 100644 index 0000000..6183b77 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/COPYING @@ -0,0 +1,13 @@ +GPL v2; Copyright (c) 2017 Evan Welsh + +Dynamic Panel Transparency is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 2 only. + +Dynamic Panel Transparency is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Dynamic Panel Transparency. If not, see . diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/LICENSE b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/LICENSE new file mode 100644 index 0000000..6ccd897 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/LICENSE @@ -0,0 +1,130 @@ +GNU GENERAL PUBLIC LICENSE + +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. +Preamble + +The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. + +1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: + +a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. +b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. +c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) +These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. + +3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: + +a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, +b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, +c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) +The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. + +If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. + +This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + +one line to give the program's name and an idea of what it does. +Copyright (C) yyyy name of author + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when it starts in an interactive mode: + +Gnomovision version 69, Copyright (C) year name of author +Gnomovision comes with ABSOLUTELY NO WARRANTY; for details +type `show w'. This is free software, and you are welcome +to redistribute it under certain conditions; type `show c' +for details. +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright +interest in the program `Gnomovision' +(which makes passes at compilers) written +by James Hacker. + +signature of Ty Coon, 1 April 1989 +Ty Coon, President of Vice +This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/convenience.js b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/convenience.js new file mode 100644 index 0000000..24b851b --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/convenience.js @@ -0,0 +1,110 @@ +/* exported initTranslations, getSettings, getSchemaObj */ + +/* + Copyright (c) 2011-2012, Giovanni Campagna + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the GNOME nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +const Gettext = imports.gettext; + +const Gio = imports.gi.Gio; + +const Config = imports.misc.config; +const ExtensionUtils = imports.misc.extensionUtils; + +/** + * initTranslations: + * @domain: (optional): the gettext domain to use + * + * Initialize Gettext to load translations from extensionsdir/locale. + * If @domain is not provided, it will be taken from metadata['gettext-domain'] + */ +function initTranslations(domain) { + let extension = ExtensionUtils.getCurrentExtension(); + + domain = domain || extension.metadata['gettext-domain']; + + // check if this extension was built with "make zip-file", and thus + // has the locale files in a subfolder + // otherwise assume that extension has been installed in the + // same prefix as gnome-shell + let localeDir = extension.dir.get_child('locale'); + + if (localeDir.query_exists(null)) { + Gettext.bindtextdomain(domain, localeDir.get_path()); + } else { + Gettext.bindtextdomain(domain, Config.LOCALEDIR); + } +} + +/** + * getSettings: + * @schema: (optional): the GSettings schema id + * + * Builds and return a GSettings schema for @schema, using schema files + * in extensionsdir/schemas. If @schema is not provided, it is taken from + * metadata['settings-schema']. + */ +function getSettings(schema) { + let schemaObj = getSchemaObj(schema); + + return new Gio.Settings({ settings_schema: schemaObj }); +} + +/** + * Seperated from getSettings to allow for custom paths. + * + * @param {string} schema - the GSettings schema id (default from extension.metadata) + * + * @returns {Object} A GSettingsSchema found based on the given schema path. + */ +function getSchemaObj(schema, defaultSource = false) { + let extension = ExtensionUtils.getCurrentExtension(); + + schema = schema || extension.metadata['settings-schema']; + + const GioSSS = Gio.SettingsSchemaSource; + + // check if this extension was built with "make zip-file", and thus + // has the schema files in a subfolder + // otherwise assume that extension has been installed in the + // same prefix as gnome-shell (and therefore schemas are available + // in the standard folders) + let schemaDir = extension.dir.get_child('schemas'); + let schemaSource = null; + + if (schemaDir.query_exists(null) && !defaultSource) { + schemaSource = GioSSS.new_from_directory(schemaDir.get_path(), GioSSS.get_default(), false); + } else { + schemaSource = GioSSS.get_default(); + } + + let schemaObj = schemaSource.lookup(schema, true); + + if (!schemaObj) { + throw new Error('Schema ' + schema + ' could not be found for extension ' + extension.metadata.uuid + '. Please check your installation.'); + } + + return schemaObj; +} \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/dynamic-panel-transparency.pot b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/dynamic-panel-transparency.pot new file mode 100644 index 0000000..7a3ef00 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/dynamic-panel-transparency.pot @@ -0,0 +1,162 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-03 11:22-0700\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: prefs.js:89 +msgid "default" +msgstr "" + +#: prefs.ui:7 +msgid "Classy transparency for your panel." +msgstr "" + +#: prefs.ui:9 +msgid "Website" +msgstr "" + +#: prefs.ui:72 +msgid "Shell Restart Required." +msgstr "" + +#. Meant to ask the user if they would like to restart right now or later in a yes/no format. +#: prefs.ui:73 +msgid "" +"A shell restart is required to view some of your changes. Do you want to " +"restart now?" +msgstr "" + +#. How long the transition will take. +#: prefs.ui:134 +msgid "Transition Speed" +msgstr "" + +#: prefs.ui:160 prefs.ui:183 +msgid "Overrides 'gtk-enable-animations'." +msgstr "" + +#. Whether the panel transitions should happen with the overview transitions or after. +#: prefs.ui:171 +msgid "Transition with the overview" +msgstr "" + +#. Whether the panel should transition when a window touches it or only when a window is actually maximized. +#: prefs.ui:194 +msgid "Transition when windows touch the panel" +msgstr "" + +#: prefs.ui:210 +msgid "Transitions" +msgstr "" + +#. Allows the user to set their own text color. +#: prefs.ui:257 +msgid "Enable custom text coloring" +msgstr "" + +#. The main color to be used. +#: prefs.ui:281 prefs.ui:294 +msgid "Primary Color" +msgstr "" + +#. Another color for special functions. +#: prefs.ui:307 prefs.ui:319 +msgid "Secondary Color" +msgstr "" + +#: prefs.ui:334 +msgid "Use when a window is maximized" +msgstr "" + +#: prefs.ui:352 +msgid "Use when the overview is visible" +msgstr "" + +#: prefs.ui:409 +msgid "Enable text shadowing" +msgstr "" + +#: prefs.ui:442 prefs.ui:496 prefs.ui:609 prefs.ui:663 +msgid "Shadow Color" +msgstr "" + +#: prefs.ui:508 prefs.ui:675 +msgid "Radius" +msgstr "" + +#: prefs.ui:520 prefs.ui:687 +msgid "Horizontal Offset" +msgstr "" + +#: prefs.ui:532 prefs.ui:699 +msgid "Vertical Offset" +msgstr "" + +#: prefs.ui:576 +msgid "Enable icon shadowing" +msgstr "" + +#: prefs.ui:734 +msgid "Foreground" +msgstr "" + +#. Opacity of the panel when no window is maximized. +#: prefs.ui:785 +msgid "Unmaximized Opacity" +msgstr "" + +#. Opacity of the panel when a window is maximized. +#: prefs.ui:829 +msgid "Maximized Opacity" +msgstr "" + +#. Allows the user to set their own opacity. +#: prefs.ui:850 +msgid "Enable custom opacity" +msgstr "" + +#. The panel's background color. +#: prefs.ui:902 prefs.ui:915 +msgid "Panel Color" +msgstr "" + +#. Allows the user to set their own background color for the panel. +#: prefs.ui:935 +msgid "Enable custom panel color" +msgstr "" + +#. Removes any extra styling certain themes may have. Fixes theme issues. +#: prefs.ui:983 +msgid "Remove excess panel styling" +msgstr "" + +#: prefs.ui:990 +msgid "(Fixes theme incompatibilities.)" +msgstr "" + +#. Hides the panel corners. +#: prefs.ui:1008 +msgid "Hide corners" +msgstr "" + +#: prefs.ui:1027 +msgid "Background" +msgstr "" + +#: prefs.ui:1037 +msgid "About" +msgstr "" diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/events.js b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/events.js new file mode 100644 index 0000000..bacb080 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/events.js @@ -0,0 +1,231 @@ +/* exported init, cleanup, get_current_maximized_window */ + +const Mainloop = imports.mainloop; + +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; +const Shell = imports.gi.Shell; + +const Main = imports.ui.main; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); + +const Convenience = Me.imports.convenience; +const Extension = Me.imports.extension; +const Intellifade = Me.imports.intellifade; +const Settings = Me.imports.settings; +const Util = Me.imports.util; + +const Theming = Me.imports.theming; +const Transitions = Me.imports.transitions; + +/** + * Signal Connections + * js/ui/overview.js/hidden: occurs after the overview is hidden + * js/ui/overview.js/shown: occurs after the overview is open + * MetaScreen/restacked: occurs when the window Z-ordering changes + * window_group/actor-added: occurs when a window actor is added + * window_group/actor-removed: occurs when a window actor is removed + * wm/switch-workspace: occurs after a workspace is switched + */ + +/** + * Intialize. + * + */ +function init() { + this._wm_tracker = Shell.WindowTracker.get_default(); + + this._overviewHidingSig = Main.overview.connect('hiding', Util.strip_args(Intellifade.syncCheck).bind(this)); + + if (Settings.transition_with_overview()) { + this._overviewShownSig = Main.overview.connect('showing', _overviewShown.bind(this)); + } else { + this._overviewShownSig = Main.overview.connect('shown', _overviewShown.bind(this)); + } + + let windows = global.get_window_actors(); + + for (let window_actor of windows) { + /* Simulate window creation event, null container because _windowActorAdded doesn't utilize containers */ + _windowActorAdded(null, window_actor, false); + } + + this._workspaceSwitchSig = global.window_manager.connect_after('switch-workspace', _workspaceSwitched.bind(this)); + + const screen = global.screen || global.display; + + if (screen) { + this._windowRestackedSig = screen.connect_after('restacked', _windowRestacked.bind(this)); + } else { + log('[Dynamic Panel Transparency] Error could not register \'restacked\' event.'); + } + + this._windowActorAddedSig = global.window_group.connect('actor-added', _windowActorAdded.bind(this)); + this._windowActorRemovedSig = global.window_group.connect('actor-removed', _windowActorRemoved.bind(this)); + + this._appFocusedSig = this._wm_tracker.connect_after('notify::focus-app', _windowRestacked.bind(this)); +} + +function disconnect(obj, sig) { + try { + if (sig != null && obj) { + obj.disconnect(sig); + } + } catch (error) { + log('[Dynamic Panel Transparency] Failed to disconnect signal: ' + error); + } +} + +/** + * Don't want to hold onto anything that isn't ours. + * + */ +function cleanup() { + /* Disconnect Signals */ + if (this._windowUnminimizeSig) { + disconnect(global.window_manager, this._windowUnminimizeSig); + } + + disconnect(Main.overview, this._overviewShownSig); + disconnect(Main.overview, this._overviewHidingSig); + + disconnect(global.window_manager, this._workspaceSwitchSig); + + disconnect(global.window_group, this._windowActorAddedSig); + disconnect(global.window_group, this._windowActorRemovedSig); + + const screen = global.screen || global.display; + + if (screen) { + disconnect(screen, this._windowRestackedSig); + } else { + log('[Dynamic Panel Transparency] Error could not disconnect \'restacked\' event.'); + } + + disconnect(this._wm_tracker, this._appFocusedSig); + + let windows = global.get_window_actors(); + + for (let window_actor of windows) { + if (typeof (window_actor._dpt_signals) !== 'undefined') { + for (let signalId of window_actor._dpt_signals) { + disconnect(window_actor, signalId); + } + } + + delete window_actor._dpt_signals; + delete window_actor._dpt_tracking; + } + + /* Cleanup Signals */ + this._windowRestackedSig = null; + this._overviewShownSig = null; + this._overviewHidingSig = null; + this._windowActorRemovedSig = null; + this._workspaceSwitchSig = null; + this._windowActorAddedSig = null; + + this._wm_tracker = null; +} + +/* Event Handlers */ + +/** + * Called whenever the overview is shown. + * + */ +function _overviewShown() { + if (!Transitions.get_transparency_status().is_blank()) { + Transitions.blank_fade_out(); + } + + if (Settings.get_enable_text_color() && (Settings.get_enable_maximized_text_color() || Settings.get_enable_overview_text_color())) { + if (Settings.get_enable_overview_text_color()) { + Theming.remove_text_color(); + Theming.set_text_color('maximized'); + } else { + Theming.remove_text_color('maximized'); + Theming.set_text_color(); + } + } +} + +/** + * Called whenever a window actor is removed. + * + */ +function _windowActorRemoved(container, window_actor) { + if (typeof (window_actor._dpt_tracking) === 'undefined') { + return; + } + + /* Remove our tracking variable. */ + delete window_actor._dpt_tracking; + + if (typeof (window_actor._dpt_signals) !== 'undefined') { + for (let signalId of window_actor._dpt_signals) { + window_actor.disconnect(signalId); + } + } + + delete window_actor._dpt_signals; + + Intellifade.asyncCheck(); +} + +const { major, minor } = Util.get_shell_version(); + +/** + * Called whenever a window is created in the shell. + * + */ +function _windowActorAdded(window_group, window_actor, force = true) { + if (window_actor && (force || typeof (window_actor._dpt_tracking) === 'undefined')) { + window_actor._dpt_tracking = true; + + let ac_wId = -1; + + if (major >= 40 || minor > 36) { + ac_wId = window_actor.connect('notify::allocation', (function() { + Intellifade.asyncCheck(); + }).bind(this)); + } else { + ac_wId = window_actor.connect('allocation-changed', (function() { + Intellifade.asyncCheck(); + }).bind(this)); + } + + const v_wId = window_actor.connect('notify::visible', (function() { + Intellifade.asyncCheck(); + }).bind(this)); + window_actor._dpt_signals = [ac_wId, v_wId]; + + Intellifade.asyncCheck(); + } +} + +/** + * SPECIAL_CASE: Only update if we're using per-app settings or is desktop icons are enabled. + * + */ +function _windowRestacked() { + /* Don't allow restacks while the overview is transitioning. */ + if (!Main.overview.visible) { + /* Detect if desktop icons are enabled. */ + if (Settings.gs_show_desktop()) { + Intellifade.asyncCheck(); + } + } +} + +/** + * SPECIAL_CASE: Update logic requires the workspace that we'll be switching to. + * + */ +function _workspaceSwitched(wm, from, to, direction) { + /* Detect if desktop icons are enabled. */ + if (!Settings.gs_show_desktop()) { + Intellifade.syncCheck(); + } +} \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/extension.js b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/extension.js new file mode 100644 index 0000000..7a285a8 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/extension.js @@ -0,0 +1,571 @@ +/* exported init, enable, disable */ + +const Mainloop = imports.mainloop; + +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; +const St = imports.gi.St; + +const ExtensionSystem = imports.ui.extensionSystem; +const Main = imports.ui.main; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); + +const Convenience = Me.imports.convenience; +const Events = Me.imports.events; +const Intellifade = Me.imports.intellifade; +const Settings = Me.imports.settings; +const Util = Me.imports.util; + +const Theming = Me.imports.theming; +const Transitions = Me.imports.transitions; + +const SETTINGS_DELAY = 3000; + +/* Only way to prevent multiple runs apparently. Hacky-ness. */ +let modified = false; + +/* Initialize */ +function init() { } + +function enable() { + /* Initialize Settings */ + initialize_settings(); + + /* Initialize Utilities */ + Transitions.init(); + Theming.init(); + Intellifade.init(); + + /* Modify the panel. */ + modify_panel(); + + /* Start the event loop. */ + Events.init(); + + /* Simulate window changes. */ + Intellifade.forceSyncCheck(); + +} + +function disable() { + /* Do this first in case any of the upcoming methods fail. */ + unmodify_panel(); + + try { + /* Disconnect & Null Signals */ + Events.cleanup(); + + /* Cleanup Settings */ + Settings.unbind(); + Settings.cleanup(); + + /* Cleanup Transitions */ + Transitions.cleanup(); + + /* Cleanup Theming */ + Theming.cleanup(); + + /* Cleanup Intellifade */ + Intellifade.cleanup(); + } catch (error) { + log('[DPT] Encountered an error cleaning up extension: ' + error); + } + + /* Shouldn't be an issue, but let's make sure it isn't. */ + modified = false; + + return false; +} + +function modify_panel() { + /* Get Rid of the Panel's CSS Background */ + Theming.initialize_background_styles(); + + let text_shadow = Theming.register_text_shadow(Settings.get_text_shadow_color(), Settings.get_text_shadow_position()); + let icon_shadow = Theming.register_icon_shadow(Settings.get_icon_shadow_color(), Settings.get_icon_shadow_position()); + + /* Add Text Shadowing */ + if (Settings.add_text_shadow()) { + if (text_shadow !== null) { + Theming.add_text_shadow(); + } else { + log('[Dynamic Panel Transparency] Failed to enabled text shadowing.'); + } + } + + /* Add Icon Shadowing */ + if (Settings.add_icon_shadow()) { + if (icon_shadow !== null) { + Theming.add_icon_shadow(); + } else { + log('[Dynamic Panel Transparency] Failed to enabled icon shadowing.'); + } + } + + /* Register text color styling. */ + let [text, icon, arrow] = Theming.register_text_color(Settings.get_text_color()); // eslint-disable-line no-unused-vars + let [maximized_text, maximized_icon, maximized_arrow] = Theming.register_text_color(Settings.get_maximized_text_color(), 'maximized'); // eslint-disable-line no-unused-vars + + if (Settings.get_enable_text_color()) { + if (text !== null) { + Theming.set_text_color(); + } else { + log('[Dynamic Panel Transparency] Failed to enabled text coloring.'); + } + } +} + +function unmodify_panel() { + + /* Remove corner styling */ + Theming.clear_corner_color(); + + /* Remove Our Styling */ + Theming.reapply_panel_styling(); + Theming.reapply_panel_background_image(); + + Theming.remove_panel_transparency(); + + /* Remove shadowing */ + if (Theming.has_text_shadow()) { + Theming.remove_text_shadow(); + } + + if (Theming.has_icon_shadow()) { + Theming.remove_icon_shadow(); + } + + /* Remove text coloring */ + Theming.remove_text_color(); + + /* Remove maximized text coloring */ + Theming.remove_text_color('maximized'); +} + +// TODO: Merge handler code or hide it behind the backend. +function initialize_settings() { + /* Setup settings... */ + Settings.init(); + + /* Register settings... */ + Settings.add({ + key: 'hide-corners', + name: 'hide_corners', + type: 'b', + handler: (function() { + Transitions.update_corner_alpha(); + }.bind(this)) + }); + Settings.add({ + key: 'transition-speed', + name: 'transition_speed', + type: 'i', + handler: ( + /* Update the backend24 transition CSS. */ + function() { + Main.panel.remove_style_class_name('dpt-panel-transition-duration'); + + let theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); + + for (let i = Theming.stylesheets.length - 1; i >= 0; i--) { + let stylesheet = Theming.stylesheets[i]; + if (stylesheet.indexOf('transitions') !== -1 && stylesheet.endsWith('panel-transition-duration.dpt.css')) { + theme.unload_stylesheet(Util.get_file(stylesheet)); + Util.remove_file(stylesheet); + Theming.stylesheets.splice(i, 1); + } + } + + const id = this.panel_transition_update_id = Mainloop.timeout_add(SETTINGS_DELAY, (function() { // eslint-disable-line no-magic-numbers + if (id !== this.panel_transition_update_id) { + return false; + } + + /* Get Rid of the Panel's CSS */ + Theming.update_transition_css(); + + Intellifade.forceSyncCheck(); + + return false; + }).bind(this)); + }).bind(this) + }); + Settings.add({ + key: 'unmaximized-opacity', + name: 'unmaximized_opacity', + type: 'i', + getter: 'get_unmaximized_opacity', + handler: (function() { + const super_id = this.opacity_update_id = Mainloop.timeout_add(SETTINGS_DELAY, (function() { + if (super_id !== this.opacity_update_id) { + return false; + } + + let theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); + + for (let i = Theming.stylesheets.length - 1; i >= 0; i--) { + let stylesheet = Theming.stylesheets[i]; + if (stylesheet.indexOf('background') !== -1 && stylesheet.indexOf('panel-') !== -1) { + theme.unload_stylesheet(Util.get_file(stylesheet)); + Util.remove_file(stylesheet); + Theming.stylesheets.splice(i, 1); + } + } + + Theming.initialize_background_styles(); + + const id = this.panel_color_update_id = Mainloop.timeout_add(SETTINGS_DELAY, (function() { // eslint-disable-line no-magic-numbers + if (id !== this.panel_color_update_id) { + return false; + } + + /* Get Rid of the Panel's CSS Background */ + Theming.remove_background_color(); + + Intellifade.forceSyncCheck(); + + return false; + }).bind(this)); + + return false; + }).bind(this)); + }).bind(this) + }); + Settings.add({ + key: 'maximized-opacity', + name: 'maximized_opacity', + type: 'i', + getter: 'get_maximized_opacity', + handler: (function() { + const super_id = this.opacity_update_id = Mainloop.timeout_add(SETTINGS_DELAY, (function() { + if (super_id !== this.opacity_update_id) { + return false; + } + + let theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); + + for (let i = Theming.stylesheets.length - 1; i >= 0; i--) { + let stylesheet = Theming.stylesheets[i]; + if (stylesheet.indexOf('background') !== -1 && stylesheet.indexOf('panel-') !== -1) { + theme.unload_stylesheet(Util.get_file(stylesheet)); + Util.remove_file(stylesheet); + Theming.stylesheets.splice(i, 1); + } + } + + Theming.initialize_background_styles(); + + const id = this.panel_color_update_id = Mainloop.timeout_add(SETTINGS_DELAY, (function() { // eslint-disable-line no-magic-numbers + if (id !== this.panel_color_update_id) { + return false; + } + + /* Get Rid of the Panel's CSS Background */ + Intellifade.forceSyncCheck(); + + return false; + }).bind(this)); + + return false; + }).bind(this)); + }).bind(this) + }); + Settings.add({ + key: 'panel-color', + name: 'panel_color', + type: 'ai', + parser: Util.tuple_to_native_color, + handler: ( + /* Handler for 3.24+ */ + function() { + Theming.remove_background_color(); + + let theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); + + for (let i = Theming.stylesheets.length - 1; i >= 0; i--) { + let stylesheet = Theming.stylesheets[i]; + if (stylesheet.indexOf('background') !== -1 && stylesheet.indexOf('panel.dpt.css') !== -1) { + theme.unload_stylesheet(Util.get_file(stylesheet)); + Util.remove_file(stylesheet); + Theming.stylesheets.splice(i, 1); + } + } + Theming.register_background_color(Settings.get_panel_color(), 'custom'); + const id = this.panel_color_update_id = Mainloop.timeout_add(SETTINGS_DELAY, (function() { // eslint-disable-line no-magic-numbers + if (id !== this.panel_color_update_id) { + return false; + } + + /* Get Rid of the Panel's CSS Background */ + Theming.remove_background_color(); + + Intellifade.forceSyncCheck(); + + return false; + }).bind(this)); + /* Legacy Handler */ + }).bind(this) + }); + Settings.add({ + key: 'trigger-apps', + name: 'trigger_apps', + type: 'as' + }); + Settings.add({ + key: 'trigger-windows', + name: 'trigger_windows', + type: 'as' + }); + Settings.add({ + key: 'text-shadow', + name: 'text_shadow', + type: 'b', + getter: 'add_text_shadow', + handler: (function() { + if (Settings.add_text_shadow()) { + Theming.add_text_shadow(); + } else { + Theming.remove_text_shadow(); + } + }).bind(this) + }); + Settings.add({ + key: 'icon-shadow', + name: 'icon_shadow', + type: 'b', + getter: 'add_icon_shadow', + handler: (function() { + if (Settings.add_icon_shadow()) { + Theming.add_icon_shadow(); + } else { + Theming.remove_icon_shadow(); + } + }).bind(this) + }); + Settings.add({ + key: 'text-shadow-position', + name: 'text_shadow_position', + type: '(iii)', + handler: (function() { + Theming.remove_text_shadow(); + + let theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); + + for (let i = Theming.stylesheets.length - 1; i >= 0; i--) { + let stylesheet = Theming.stylesheets[i]; + if (stylesheet.indexOf('shadow') !== -1 && stylesheet.indexOf('text') !== -1) { + theme.unload_stylesheet(Util.get_file(stylesheet)); + Util.remove_file(stylesheet); + Theming.stylesheets.splice(i, 1); + } + } + let text_shadow = Theming.register_text_shadow(Settings.get_text_shadow_color(), Settings.get_text_shadow_position()); + const id = this.text_shadow_update_id = Mainloop.timeout_add(SETTINGS_DELAY, (function() { // eslint-disable-line no-magic-numbers + if (id !== this.text_shadow_update_id) { + return false; + } + + /* Add Text Shadowing */ + if (Settings.add_text_shadow()) { + if (text_shadow !== null) { + Theming.add_text_shadow(); + } else { + log('[Dynamic Panel Transparency] Failed to enabled text shadowing.'); + } + } + + Intellifade.forceSyncCheck(); + + return false; + }).bind(this)); + }).bind(this) + }); + Settings.add({ + key: 'icon-shadow-position', + name: 'icon_shadow_position', + type: '(iii)', + handler: (function() { + Theming.remove_icon_shadow(); + + let theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); + + for (let i = Theming.stylesheets.length - 1; i >= 0; i--) { + let stylesheet = Theming.stylesheets[i]; + if (stylesheet.indexOf('shadow') !== -1 && stylesheet.indexOf('icon') !== -1) { + theme.unload_stylesheet(Util.get_file(stylesheet)); + Util.remove_file(stylesheet); + Theming.stylesheets.splice(i, 1); + } + } + let icon_shadow = Theming.register_icon_shadow(Settings.get_icon_shadow_color(), Settings.get_icon_shadow_position()); + const id = this.icon_shadow_update_id = Mainloop.timeout_add(SETTINGS_DELAY, (function() { // eslint-disable-line no-magic-numbers + if (id !== this.icon_shadow_update_id) { + return false; + } + + /* Add Icon Shadowing */ + if (Settings.add_icon_shadow()) { + if (icon_shadow !== null) { + Theming.add_icon_shadow(); + } else { + log('[Dynamic Panel Transparency] Failed to enabled icon shadowing.'); + } + } + + Intellifade.forceSyncCheck(); + + return false; + }).bind(this)); + }).bind(this) + }); + Settings.add({ + key: 'icon-shadow-color', + name: 'icon_shadow_color', + type: '(iiid)', + parser: Util.tuple_to_native_color, + handler: (function() { + Theming.remove_icon_shadow(); + + let theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); + + for (let i = Theming.stylesheets.length - 1; i >= 0; i--) { + let stylesheet = Theming.stylesheets[i]; + if (stylesheet.indexOf('shadow') !== -1 && stylesheet.indexOf('icon') !== -1) { + theme.unload_stylesheet(Util.get_file(stylesheet)); + Util.remove_file(stylesheet); + Theming.stylesheets.splice(i, 1); + } + } + let icon_shadow = Theming.register_icon_shadow(Settings.get_icon_shadow_color(), Settings.get_icon_shadow_position()); + const id = this.icon_shadow_update_id = Mainloop.timeout_add(SETTINGS_DELAY, (function() { // eslint-disable-line no-magic-numbers + if (id !== this.icon_shadow_update_id) { + return false; + } + + /* Add Icon Shadowing */ + if (Settings.add_icon_shadow()) { + if (icon_shadow !== null) { + Theming.add_icon_shadow(); + } else { + log('[Dynamic Panel Transparency] Failed to enabled icon shadowing.'); + } + } + + Intellifade.forceSyncCheck(); + + return false; + }).bind(this)); + }).bind(this) + }); + Settings.add({ + key: 'text-shadow-color', + name: 'text_shadow_color', + type: '(iiid)', + parser: Util.tuple_to_native_color, + handler: (function() { + Theming.remove_text_shadow(); + + let theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); + + for (let i = Theming.stylesheets.length - 1; i >= 0; i--) { + let stylesheet = Theming.stylesheets[i]; + if (stylesheet.indexOf('shadow') !== -1 && stylesheet.indexOf('text') !== -1) { + theme.unload_stylesheet(Util.get_file(stylesheet)); + Util.remove_file(stylesheet); + Theming.stylesheets.splice(i, 1); + } + } + let text_shadow = Theming.register_text_shadow(Settings.get_text_shadow_color(), Settings.get_text_shadow_position()); + const id = this.text_shadow_update_id = Mainloop.timeout_add(SETTINGS_DELAY, (function() { // eslint-disable-line no-magic-numbers + if (id !== this.text_shadow_update_id) { + return false; + } + + /* Add Text Shadowing */ + if (Settings.add_text_shadow()) { + if (text_shadow !== null) { + Theming.add_text_shadow(); + } else { + log('[Dynamic Panel Transparency] Failed to enabled text shadowing.'); + } + } + + Intellifade.forceSyncCheck(); + + return false; + }).bind(this)); + }).bind(this) + }); + Settings.add({ + key: 'text-color', + name: 'text_color', + type: '(iii)', + parser: Util.tuple_to_native_color + }); + Settings.add({ + key: 'maximized-text-color', + name: 'maximized_text_color', + type: '(iii)', + parser: Util.tuple_to_native_color + }); + Settings.add({ + key: 'enable-maximized-text-color', + name: 'enable_maximized_text_color', + type: 'b', + handler: (function() { + Intellifade.forceSyncCheck(); + }).bind(this) + }); + Settings.add({ + key: 'remove-panel-styling', + name: 'remove_panel_styling', + getter: 'remove_panel_styling', + type: 'b' + }); + Settings.add({ + key: 'enable-overview-text-color', + name: 'enable_overview_text_color', + type: 'b' + }); + Settings.add({ + key: 'enable-text-color', + name: 'enable_text_color', + type: 'b', + handler: (function() { + if (Settings.get_enable_text_color()) { + Intellifade.forceSyncCheck(); + } else { + Theming.remove_text_color(); + Theming.remove_text_color('maximized'); + } + }).bind(this) + }); + Settings.add({ + key: 'enable-opacity', + name: 'enable_custom_opacity', + getter: 'enable_custom_opacity', + type: 'b' + }); + Settings.add({ + key: 'enable-background-color', + name: 'enable_custom_background_color', + getter: 'enable_custom_background_color', + type: 'b' + }); + Settings.add({ + key: 'transition-with-overview', + name: 'transition_with_overview', + getter: 'transition_with_overview', + type: 'b' + }); + Settings.add({ + key: 'transition-windows-touch', + name: 'transition_windows_touch', + getter: 'transition_when_windows_touch_panel', + type: 'b' + }); + + /* After we've given Settings the necessary information... let's bind it. */ + Settings.bind(); +} \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/intellifade.js b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/intellifade.js new file mode 100644 index 0000000..a2ff64a --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/intellifade.js @@ -0,0 +1,252 @@ +/* exported init, cleanup, asyncCheck, syncCheck, forceAsyncCheck, forceSyncCheck, get_current_maximized_window */ + +const GLib = imports.gi.GLib; +const Mainloop = imports.mainloop; + +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; +const St = imports.gi.St; + +const Main = imports.ui.main; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Settings = Me.imports.settings; +const Util = Me.imports.util; + +const Transitions = Me.imports.transitions; +const Theming = Me.imports.theming; + +/* Determines whether to continue the async loop checks. */ +let continueCheck = false; +/* Variable for current detected maximized window... */ +let maximized_window = null; +/* Run the next change regardless of the whether it is the same as the current status. */ +// TODO: Find a nicer way to override optimization code. +let override_optimization = false; +/* Current ID of the async loop (0 if no loop is running) */ +let timeoutId = 0; + +/* How often the asynchronous loop should run in milliseconds... */ +const ASYNC_UPDATE_FREQUENCY = 200; // ms + +function init() { + this._wm_tracker = Shell.WindowTracker.get_default(); + + _updateBounds(); +} + +function cleanup() { + this._wm_tracker = null; + + if (timeoutId > 0) { + Mainloop.source_remove(timeoutId); + } +} + +function forceSyncCheck() { + override_optimization = true; + syncCheck(); +} + +function syncCheck() { + /* Prevent any asynchronous checks from occuring in the loop. */ + continueCheck = false; + /* Stop the asynchronous loop... */ + if (timeoutId > 0) + Mainloop.source_remove(timeoutId); + /* Remove the old loop ID */ + timeoutId = 0; + /* Update bounds when a check is done in sync. */ + _updateBounds(); + /* Run a check. */ + _check(); +} + +function forceAsyncCheck() { + override_optimization = true; + asyncCheck(); +} + +function asyncCheck() { + if (timeoutId <= 0) { + _check(); + + timeoutId = Mainloop.timeout_add(ASYNC_UPDATE_FREQUENCY, (function() { + _check(); + + if (continueCheck) { + continueCheck = false; + return GLib.SOURCE_CONTINUE; + } else { + timeoutId = 0; + return GLib.SOURCE_REMOVE; + } + }).bind(this)); + } else { + continueCheck = true; + } + +} + + +function _updateBounds() { + const panel = Main.panel; + + this.panel_bounds = { + x: panel.get_x(), + y: panel.get_y(), + height: panel.get_height(), + width: panel.get_width(), + is_top: true + }; + + this.scale_factor = St.ThemeContext.get_for_stage(global.stage).scale_factor; + + let [, pivot_y] = Main.layoutManager.panelBox.get_pivot_point(); + + // Adjust for bottom panel. + if (pivot_y < 0) { + this.panel_bounds.y = -pivot_y; + this.panel_bounds.is_top = false; + } +} + +/* Main extension logic. Modified to fit Gnome Shell 3.26 design patterns. */ + +// TODO: Cleanup use of variable flags. +function _check() { + if (Main.overview._shown) { + return; + } + + let workspace = null; + + const manager = global.screen || global.workspace_manager; + + if (manager) { + workspace = manager.get_active_workspace(); + } else { + log('[Dynamic Panel Transparency] Error could not get active workspace.'); + } + + let windows = workspace.list_windows(); + windows = global.display.sort_windows_by_stacking(windows); + + let focused_window = global.display.get_focus_window(); + + maximized_window = null; + + let add_transparency = true; + let force_transparency = false; + + /* Handle desktop icons (they're a window too) */ + if (focused_window && focused_window.get_window_type() === Meta.WindowType.DESKTOP) { + add_transparency = true; + maximized_window = focused_window; + } else { + // TODO: Always negative? Is pivot negative? + for (let i = windows.length - 1; i >= 0; i--) { + + let current_window = windows[i]; + + if (!current_window.showing_on_its_workspace() || !current_window.is_on_primary_monitor()) { + continue; + } + + /* Make sure the window is on the correct monitor, isn't minimized, isn't supposed to be excluded, and is actually maximized. */ + if (!Util.is_valid(current_window)) { + continue; + } + + if (current_window.maximized_vertically) { + /* Make sure the top-most window is selected */ + if (maximized_window === null && !force_transparency) { + maximized_window = current_window; + } + + add_transparency = false; + + break; + } + + let frame = current_window.get_frame_rect(); + + if (Main.layoutManager._rightPanelBarrier) { + let overlap = this.panel_bounds.x < frame.x + frame.width && + this.panel_bounds.x + this.panel_bounds.width > frame.x && + this.panel_bounds.y < frame.y + frame.height && + this.panel_bounds.height + this.panel_bounds.y > frame.y; + + if (overlap) { + force_transparency = true; + maximized_window = null; + + break; + } + } + + if (Settings.transition_when_windows_touch_panel()) { + let touching_panel = false; + + if (this.panel_bounds.is_top) { + touching_panel = frame.y >= (this.panel_bounds.y + this.panel_bounds.height) && + frame.y <= (this.panel_bounds.y + this.panel_bounds.height); + } else { + touching_panel = (frame.y + frame.height) >= (this.panel_bounds.y) && + (frame.y + frame.height) <= (this.panel_bounds.y); + } + + if (!force_transparency && touching_panel) { + add_transparency = false; + + if (maximized_window === null && !force_transparency) { + maximized_window = current_window; + } + + break; + } + } + } + } + + if (force_transparency) { + Transitions.fade_out(); + force_transparency = false; + /* Only change if the transparency isn't already correct or if override_optimization has been called */ + } else if (Transitions.get_transparency_status().is_blank()) { + if (add_transparency) { + Transitions.minimum_fade_in(); + } else { + Transitions.fade_in(); + } + } else if (override_optimization || (Transitions.get_transparency_status().is_transparent() !== add_transparency)) { + override_optimization = false; + + if (add_transparency) { + Transitions.fade_out(); + } else { + Transitions.fade_in(); + } + } + + /* Reset text coloring. */ + if (Settings.get_enable_text_color() && (Settings.get_enable_maximized_text_color() || Settings.get_enable_overview_text_color())) { + if (!add_transparency && Settings.get_enable_maximized_text_color()) { + Theming.remove_text_color(); + Theming.set_text_color('maximized'); + } else { + Theming.remove_text_color('maximized'); + Theming.set_text_color(); + } + } +} + +/** + * Returns the current visible maximized window as understood by the events' logic. + * The maximized window is not necessarily the highest window in the z-order. + * + * @returns {Object} The current visible maximized window. + */ +function get_current_maximized_window() { + return maximized_window; +} \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/ar/LC_MESSAGES/dynamic-panel-transparency.mo b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/ar/LC_MESSAGES/dynamic-panel-transparency.mo new file mode 100644 index 0000000000000000000000000000000000000000..482bb5d2070493880772d9ba27470a006b797c79 GIT binary patch literal 2715 zcmb7^+iz4w9LER6+lnHH;stfk7$A6-wt&`!%B2*+P}}q(VnWF5?sR+P?3v}v*|v)h zrqGtAQQv&_Qlaf~>780$eDDu2m>B207+-w!RUh&Dn>|an5KWw9`#ERk_xsKFcRBrS z5>08IXgsU^nGx!Dg z6nF(B|0VEg@JH}v@K=z7e}Uw`8Ob~XZi~4K>_rXU0+ZPP8kcJDCy?%MhVhf&d*JKf zDR3wFY5aZ_+=chs;0xfIM;Ut#+yau{UXbc^1f)DZ0vo|Ta4R?slHS`O)$tFIblw5! zzV(k)_V4Hh91AL$a&}@Kyf-b||fiAv-KGXLErn`dQZO z39U;!41})>B9MNj#Cudw30V-n^pdQ_7imxOOi_m_&s9NW+^}@dt~KQu<*DGFlTZ$X zl>^t$RnOdv@{yp(s{Wc?Yr3*~R0T4YdCbj1fC_vWXm(5m?s?^h!sBf{Jz9pWRSdX! z_dH6|7OQLPlR*F{&9~*kvyQYWIKp@HD6{hQwj^t(?0B=4+#cW{D8+9bBI_14>yUZX zC%GKRNUeD#LmiepQrsz#L3YKZ)8-QUciPT%#GUJi+m>WqRAn?6xpx+%%+_pZ*6ru- z%c5IL-rp;IE;#ChV$m4fHDh;=F-9Cx13MKk>2tM5vpy`OXm$$`o&?tAEp} zZ*Uswd3{4_Pvg$I-F0<|6GDfM&D0a23R1jPX(7`5kau1c(=OkiR~ab^2YXeR7p|AY z=)VIeKUWkv>2yhvPw|#M;q#Ny)4hB@>Kuwpcu-=mpSD(eq__yqbe>AI9&c?~NuoYk zmuQBg^r59xfLNNXT?J3L{yzFt0*p;lch^y;am9|h(<1|?1)pOUO-}K~v>PVcy<#9d z2h*6>Def2St8Qx8$19IbKHu8J>-Qx-bl#O1&`Q=P)LG$SBzXg*uX)MS&;HOf(c-toVH!jhL^PxsISi%<8*=!$F9c zVrC&4#Fl;GW*j$f*hzR2u`Su~5awzy?9 z8sRvYFpCJe613Ke6jd$hF%D$yp?onKT^-eYPpMLwXJI?HDpd7|{A{Xd!UZ!8JEV$I zMuQd4IRu?Rb`zC)llpjr+~}UrE18#b)=9e zfhlA9hUo&1hY{qvQf%vkJq)p#p*G*|V|!Iwp*W=dl5SRouNnbdhm5D=wCDxJUyg(0 MCi>DQ%uSsC15n;9V*mgE literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/ar/LC_MESSAGES/dynamic-panel-transparency.po b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/ar/LC_MESSAGES/dynamic-panel-transparency.po new file mode 100644 index 0000000..a254229 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/ar/LC_MESSAGES/dynamic-panel-transparency.po @@ -0,0 +1,231 @@ +# Dynamic Panel Transparency Translation File +# Copyright (c) 2017 +# This file is distributed under the same license as the dynamic-panel-transparency package. +# +# Evan Welsh , 2017. +# Mosaab Alzoubi , 2016. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-03 11:20-0700\n" +"PO-Revision-Date: 2016-12-21 12:48+0300\n" +"Last-Translator: Mosaab Alzoubi \n" +"Language-Team: Evan Welsh \n" +"Language: ar_SY\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 2.0\n" + +#: prefs.js:89 +msgid "default" +msgstr "الافتراضي" + +#: prefs.ui:7 +msgid "Classy transparency for your panel." +msgstr "شفافية أنيقة لشريطك." + +#: prefs.ui:9 +msgid "Website" +msgstr "الموقع" + +#: prefs.ui:72 +msgid "Shell Restart Required." +msgstr "إعادة تشغيل غنوم ضرورية." + +#. Meant to ask the user if they would like to restart right now or later in a yes/no format. +#: prefs.ui:73 +msgid "" +"A shell restart is required to view some of your changes. Do you want to " +"restart now?" +msgstr "يجب إعادة تشغيل غنوم لتأخذ التغييرات مجراها. هل ترغب بذلك الآن؟" + +#. How long the transition will take. +#: prefs.ui:134 +msgid "Transition Speed" +msgstr "سرعة التلاشي" + +#: prefs.ui:160 prefs.ui:183 +msgid "Overrides 'gtk-enable-animations'." +msgstr "أبطل 'gtk-enable-animations'." + +#. Whether the panel transitions should happen with the overview transitions or after. +#: prefs.ui:171 +msgid "Transition with the overview" +msgstr "" + +#. Whether the panel should transition when a window touches it or only when a window is actually maximized. +#: prefs.ui:194 +msgid "Transition when windows touch the panel" +msgstr "" + +#: prefs.ui:210 +msgid "Transitions" +msgstr "التلاشيات" + +#. Allows the user to set their own text color. +#: prefs.ui:257 +msgid "Enable custom text coloring" +msgstr "تفعيل اللون المخصص للنصوص" + +#. The main color to be used. +#: prefs.ui:281 prefs.ui:294 +msgid "Primary Color" +msgstr "اللون الأساسي" + +#. Another color for special functions. +#: prefs.ui:307 prefs.ui:319 +msgid "Secondary Color" +msgstr "اللون الثانوي" + +#: prefs.ui:334 +msgid "Use when a window is maximized" +msgstr "الاستعمال عند تكبير النافذة" + +#: prefs.ui:352 +msgid "Use when the overview is visible" +msgstr "الاستعمال في نمط الاستعراض" + +#: prefs.ui:409 +msgid "Enable text shadowing" +msgstr "تفعيل ظل النص" + +#: prefs.ui:442 prefs.ui:496 prefs.ui:609 prefs.ui:663 +msgid "Shadow Color" +msgstr "لون الظل" + +#: prefs.ui:508 prefs.ui:675 +msgid "Radius" +msgstr "نصف قطري" + +#: prefs.ui:520 prefs.ui:687 +msgid "Horizontal Offset" +msgstr "التوازن الأفقي" + +#: prefs.ui:532 prefs.ui:699 +msgid "Vertical Offset" +msgstr "التوازن العمودي" + +#: prefs.ui:576 +msgid "Enable icon shadowing" +msgstr "تفعيل ظل الرمز" + +#: prefs.ui:734 +msgid "Foreground" +msgstr "المقدمة" + +#. Opacity of the panel when no window is maximized. +#: prefs.ui:785 +msgid "Unmaximized Opacity" +msgstr "إلغاء الشفافية في حالة عدم التكبير" + +#. Opacity of the panel when a window is maximized. +#: prefs.ui:829 +msgid "Maximized Opacity" +msgstr "إلغاء الشفافية في حالة التكبير" + +#. Allows the user to set their own opacity. +#: prefs.ui:850 +msgid "Enable custom opacity" +msgstr "تفعيل إلغاء شفافية مخصص" + +#. The panel's background color. +#: prefs.ui:902 prefs.ui:915 +msgid "Panel Color" +msgstr "لون الشريط" + +#. Allows the user to set their own background color for the panel. +#: prefs.ui:935 +msgid "Enable custom panel color" +msgstr "تفعيل لون خاص للشريط" + +#. Removes any extra styling certain themes may have. Fixes theme issues. +#: prefs.ui:983 +msgid "Remove excess panel styling" +msgstr "أزل زوائد نمط الشريط" + +#: prefs.ui:990 +msgid "(Fixes theme incompatibilities.)" +msgstr "(أصلح التكاملات مع السمة.)" + +#. Hides the panel corners. +#: prefs.ui:1008 +msgid "Hide corners" +msgstr "أخف الزوايا" + +#: prefs.ui:1027 +msgid "Background" +msgstr "الخلفية" + +#: prefs.ui:1037 +msgid "About" +msgstr "حول" + +#~ msgid "App Tweaks" +#~ msgstr "تطويعات التطبيق" + +#~ msgid "Add a Custom WM_CLASS" +#~ msgstr "أضف WM_CLASS مخصصة" + +#~ msgid "Advanced..." +#~ msgstr "متقدم..." + +#~ msgid "" +#~ "A WM_CLASS is a code that identifies a window. Use this setting if the " +#~ "window you want custom settings for is not part of an application. You " +#~ "can find a window's WM_CLASS with the command 'xprop WM_CLASS'. Enter the " +#~ "second result from 'xprop WM_CLASS' below without quotation marks." +#~ msgstr "" +#~ "إن WM_CLASS هو شيفرة تعرّف النافذة. استعمل هذه الوحدات إذا أردت تنفيذ " +#~ "إعدادات مخصصة ليست جزءًا من التطبيق. بإمكانك العثور عليها من خلال تنفيذ " +#~ "الأمر xprop WM_CLASS وأدخل هنا النتيجة الثانية للأمر بدون علامات التنصيص." + +#~ msgid "WM_CLASS cannot be blank." +#~ msgstr "لا يمكن أن يكون WM_CLASS فارغًا." + +#~ msgid "Enable background app tweaks" +#~ msgstr "فعّل تطويعات الخلفية" + +#~ msgid "Theme Source" +#~ msgstr "مصدر السمة" + +#~ msgid "Always trigger the panel." +#~ msgstr "دومًا علّم الشريط." + +#~ msgid "Force animation" +#~ msgstr "إجبار التحريك" + +#~ msgid "Transition Style" +#~ msgstr "نمط التلاشي" + +#~ msgid "Linear (default)" +#~ msgstr "خطي (الافتراضي)" + +#~ msgid "Sine" +#~ msgstr "جيبي" + +#~ msgid "Quadratic" +#~ msgstr "تربيعي" + +#~ msgid "Cubic" +#~ msgstr "تكعيبي" + +#~ msgid "Quartic" +#~ msgstr "رباعي" + +#~ msgid "Quintic" +#~ msgstr "خماسي" + +#~ msgid "Exponential" +#~ msgstr "أسي" + +#~ msgid "Circle" +#~ msgstr "دائري" + +#~ msgid "Elastic" +#~ msgstr "متمدد" + +#~ msgid "Bounce" +#~ msgstr "ارتدادي" diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/de/LC_MESSAGES/dynamic-panel-transparency.mo b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/de/LC_MESSAGES/dynamic-panel-transparency.mo new file mode 100644 index 0000000000000000000000000000000000000000..f40fff86b97daee955d95839f8404ca00c5443a4 GIT binary patch literal 2542 zcmaKsJ&YSg6vqb$UnYDcB!q89M36+uI=+BN&IIyt=gTF^edv6bKqQ35yU)If*E7z{ z_p5juzj6FMvN4 z^QC+9xaV`BI|#Qk9UNaCoKK`9 zvzvACJ|1k(qj)%;PvPPG@Zfl|P9B^S&IkL*apQp_#N&AQe2y9W{Rkc&D64EB?AHyB z1;>GFlj}t6ozd&$Wmu&SNo``=>8hX`I@LjwFYgm4q+g{pm5zK+E=cY1_fk(gN@TEd zO;cZb3t`)o1KV>lUR9M8o!iIH}@x%h{eu#8j%hAIRX8 z@m=M}!~@y1E?3!AhSIXA8P!Nh#yuZwN7}B6br{?*n{Ab`O|83OCD3}v7qn?@uIbns zIH(faADhj#j%H`9qr%KtoxlL=3^^}mtew}sv>uc9GWXDjLCO}_ z2_7UPk$UD!Az#ljw^U?qsYqK{EOV5RVEA6|5+!33UaXkTOawc9mNo4)QYOxk3pyUf={z-XK8b{ys+R__={UF$+L=UY07PWDhi0!Z@ znpT0T@=#^srKmC)O-$l%wKBQCa;Q=%omDY*hYgAB1&f?vsf$AuEfpn6R zlSeP+Yi|zoiQ}!#dO4w2p~SR$szuQ$P|IoIXEZmP6-nXj2c$-T0&JQE!m?c>Zg$wf6>bSl!P34NKGS@8jo}^W31i8#D!f4YT zYY`ilVI7)8O&d8+xR=}^5=3Y`McM~R4wo4Vsg73<$X-X1Ho8UG-i>z+R(fa}s4p|@ z^$Fhw{X^4rdM;8J25E|8$Q%==P1A~5jS#sO6eOaSQVZ>dJ)eaiPi~cjdK6XS?xdz>0+cMH~(|Uc-Y1q^wT>-Hg zm*nmLHGeLPRN+wEhEPIdc8$6Y;>@>2g`pUEkl#w&H?i?C{KWx|G}3p=_H(0iVpjFK zxgVlvKs6IkQ)uHGSD`XEO$}6&JjIPA4ai)U$1J?2h_ATNCtYZ%7tUv~5x1<9@fs=T z)+3oMk4siK!=GcW8Wd9~+7LTD-wQ)B2P6u}aDmAgv, 2017. +# Jonatan Hatakeyama Zeidler , 2016. +msgid "" +msgstr "" +"Project-Id-Version: Dynamic Panel Transparany GNOME Shell Extension\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-03 11:20-0700\n" +"PO-Revision-Date: 2016-09-19 20:09+0200\n" +"Last-Translator: Jonatan Hatakeyama Zeidler \n" +"Language-Team: Evan Welsh \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.8.7.1\n" +"X-Poedit-SourceCharset: UTF-8\n" + +#: prefs.js:89 +msgid "default" +msgstr "Vorgabe" + +#: prefs.ui:7 +msgid "Classy transparency for your panel." +msgstr "Elegante Transparenz für die obere Leiste." + +#: prefs.ui:9 +msgid "Website" +msgstr "Webseite" + +#: prefs.ui:72 +msgid "Shell Restart Required." +msgstr "Neustart der GNOME Shell erforderlich." + +#. Meant to ask the user if they would like to restart right now or later in a yes/no format. +#: prefs.ui:73 +msgid "" +"A shell restart is required to view some of your changes. Do you want to " +"restart now?" +msgstr "" +"Ein Neustart der GNOME Shell ist erforderlich, um einige der Änderungen " +"anzuwenden. Die GNOME Shell jetzt neustarten?" + +#. How long the transition will take. +#: prefs.ui:134 +msgid "Transition Speed" +msgstr "Übergangszeit" + +#: prefs.ui:160 prefs.ui:183 +msgid "Overrides 'gtk-enable-animations'." +msgstr "Überschreibt 'gtk-enable-animations'." + +#. Whether the panel transitions should happen with the overview transitions or after. +#: prefs.ui:171 +msgid "Transition with the overview" +msgstr "" + +#. Whether the panel should transition when a window touches it or only when a window is actually maximized. +#: prefs.ui:194 +msgid "Transition when windows touch the panel" +msgstr "" + +#: prefs.ui:210 +msgid "Transitions" +msgstr "Übergang" + +#. Allows the user to set their own text color. +#: prefs.ui:257 +msgid "Enable custom text coloring" +msgstr "Angepasste Textfarbe aktivieren" + +#. The main color to be used. +#: prefs.ui:281 prefs.ui:294 +msgid "Primary Color" +msgstr "Hauptfarbe" + +#. Another color for special functions. +#: prefs.ui:307 prefs.ui:319 +msgid "Secondary Color" +msgstr "Zweitfarbe" + +#: prefs.ui:334 +msgid "Use when a window is maximized" +msgstr "Nutzen, wenn ein Fenster maximiert ist" + +#: prefs.ui:352 +msgid "Use when the overview is visible" +msgstr "Nutzen, wenn ein Fenster sichtbar ist" + +#: prefs.ui:409 +msgid "Enable text shadowing" +msgstr "Textschatten aktivieren" + +#: prefs.ui:442 prefs.ui:496 prefs.ui:609 prefs.ui:663 +msgid "Shadow Color" +msgstr "Schattenfarbe" + +#: prefs.ui:508 prefs.ui:675 +msgid "Radius" +msgstr "Radius" + +#: prefs.ui:520 prefs.ui:687 +msgid "Horizontal Offset" +msgstr "Horizontaler Versatz" + +#: prefs.ui:532 prefs.ui:699 +msgid "Vertical Offset" +msgstr "Vertikaler Versatz" + +#: prefs.ui:576 +msgid "Enable icon shadowing" +msgstr "Symbolschatten aktivieren" + +#: prefs.ui:734 +msgid "Foreground" +msgstr "Vordergrund" + +#. Opacity of the panel when no window is maximized. +#: prefs.ui:785 +msgid "Unmaximized Opacity" +msgstr "Deckkraft nicht maximiert" + +#. Opacity of the panel when a window is maximized. +#: prefs.ui:829 +msgid "Maximized Opacity" +msgstr "Deckkraft maximiert" + +#. Allows the user to set their own opacity. +#: prefs.ui:850 +msgid "Enable custom opacity" +msgstr "Angepasste Deckkraft aktivieren" + +#. The panel's background color. +#: prefs.ui:902 prefs.ui:915 +msgid "Panel Color" +msgstr "Hintergrundfarbe" + +#. Allows the user to set their own background color for the panel. +#: prefs.ui:935 +msgid "Enable custom panel color" +msgstr "Angepasste Hintergrundfarbe aktivieren" + +#. Removes any extra styling certain themes may have. Fixes theme issues. +#: prefs.ui:983 +msgid "Remove excess panel styling" +msgstr "Entfernt übertriebene Gestaltung der oberen Leiste" + +#: prefs.ui:990 +msgid "(Fixes theme incompatibilities.)" +msgstr "(Behebt Themeninkompatibilitäten.)" + +#. Hides the panel corners. +#: prefs.ui:1008 +msgid "Hide corners" +msgstr "Ecken deaktivieren" + +#: prefs.ui:1027 +msgid "Background" +msgstr "Hintergrund" + +#: prefs.ui:1037 +msgid "About" +msgstr "Info" + +#~ msgid "App Tweaks" +#~ msgstr "Anwendungsoptimierung" + +#~ msgid "Add a Custom WM_CLASS" +#~ msgstr "Angepasste WM_CLASS hinzufügen\t" + +#~ msgid "Advanced..." +#~ msgstr "Fortgeschritten …" + +#~ msgid "" +#~ "A WM_CLASS is a code that identifies a window. Use this setting if the " +#~ "window you want custom settings for is not part of an application. You " +#~ "can find a window's WM_CLASS with the command 'xprop WM_CLASS'. Enter the " +#~ "second result from 'xprop WM_CLASS' below without quotation marks." +#~ msgstr "" +#~ "Eine WM_CLASS ist ein Code zur Identifizierung von Fenstern. Diese " +#~ "Einstellung ist nützlich, wenn das Fenster nicht Teil einer gelisteten " +#~ "Anwendung ist. Die WM_CLASS eines Fensters kann mit dem Befehl 'xprop " +#~ "WM_CLASS' herausgefunden werden. Bitte den zweiten Treffer des Befehls " +#~ "hier eintragen (Ohne Anführungsstriche)." + +#~ msgid "WM_CLASS cannot be blank." +#~ msgstr "WM_CLASS darf nicht leer sein." + +#~ msgid "Enable background app tweaks" +#~ msgstr "Anwendungsoptimierung aktivieren" + +#~ msgid "Theme Source" +#~ msgstr "Themenquelle" + +#~ msgid "Always trigger the panel." +#~ msgstr "Obere Leiste immer anpassen" + +#~ msgid "Force animation" +#~ msgstr "Animation erzwingen" + +#~ msgid "Transition Style" +#~ msgstr "Übergangsstil" + +#~ msgid "Linear (default)" +#~ msgstr "Linear (Vorgabe)" + +#~ msgid "Sine" +#~ msgstr "Sinus\t" + +#~ msgid "Quadratic" +#~ msgstr "Quadratisch" + +#~ msgid "Cubic" +#~ msgstr "Kubisch" + +#~ msgid "Quartic" +#~ msgstr "Biquadratisch" + +#~ msgid "Quintic" +#~ msgstr "Polynom fünften Grades" + +#~ msgid "Exponential" +#~ msgstr "Exponentiell" + +#~ msgid "Circle" +#~ msgstr "Kreisförmig" + +#~ msgid "Elastic" +#~ msgstr "Elastisch" + +#~ msgid "Bounce" +#~ msgstr "Prellend" diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/es/LC_MESSAGES/dynamic-panel-transparency.mo b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/es/LC_MESSAGES/dynamic-panel-transparency.mo new file mode 100644 index 0000000000000000000000000000000000000000..3e6b1e501d9fc4bb610b1cf9579f384706134922 GIT binary patch literal 2464 zcmb7^O>Z1E7{?8imM$&OQYf#*ye0)^yP=U7P(dIj7M^1YMb z%U~OP2^@gzhcCe=z#l=@^JkFtyH)M~0kYnE9~jC%53*k8z=yyqAiviI*)KWB@}GhS z!LPvA!RsKua~tG${sQ^l-B5ZQJOaK7o&$N^f%p?2;fMY783?oEo9tg698ZKJ?z1gW zH4eBSzQuMwi^=if!FuD_I9LaM=MW}b9S6Vn6ejD<`Nna19FqsqDh{@F`(Rr+UpO~8 zPsI3)UL!BVG8H7XNo~4Pn#?Wa^Wwa9dfl3!ayi%SdJ1Azt?5Fq!!>i2c5^G_9E7|a>xHWc zMH3QbbOAS8YuqMSkc~vz z(UwZS9V53;MQ))ATU{)&ml0st-tH4++ZMc7GR0k1EP0Zv%VeaI@DrTHX|Q!nY;7Zr zBvf@VD{y2*dySBZ%jAMiM}E3O9lTE>qpm9RAm*L@fYOj$$Pyg&P^>BEr>5VVI@gky zTMO+AbC+rh)VD4qv%aUZUt1$ypVsDc@*JoJbnSlg6Z+INqE* z`C4P5(WqTiJ|xlbTm|czaw>=Wt-Pq5lBZPssOR{o_j6^`>0Y69UPnE=15wi}RgaQI zQbkj?R+N!fDEG_q6r3Ma8cq||R(a`8iKZl9o4qjG8ksdwZ`7tCMF#2&`p}oN?HC@_ z<}EHBheUdNX>lewIXcJj>XJ)ZD9{XHZ_1M$9cp5H+9JDp<&M(L6enDUtO`^p6mgq| zMN5wV(MG3QIhiY&Z+vG+;V6>2P>3w&7aW2ZN|pCYg9U8*(#up89c@w9Kixb_9zvsw z)UtI7MV*pM`7$wsoV3HN8Wyuwc)-XMRZhbfLOIOjI>drrfjY6Ep}OoN1*}oIURRmg z+Kx3HjT%VN^!~NspwUE$5q2a{1W#EQQrR5At||pxMzn3m5&6{V4*hpah|OSRs~CRw zV{_0}a9^;Zm7~{~s7}zig;g*LiFKEprxYz#Imm2Y%&{NZ`hRkxjzZ3Ds@X1NMQqkc zrH$*R>L4kqko?GaukSE^$wS+;CM)%BLqm?*l9$Y`y0x%BhWUWZ5RE;ihluSv#8Jj literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/es/LC_MESSAGES/dynamic-panel-transparency.po b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/es/LC_MESSAGES/dynamic-panel-transparency.po new file mode 100644 index 0000000..7050698 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/es/LC_MESSAGES/dynamic-panel-transparency.po @@ -0,0 +1,233 @@ +# Dynamic Panel Transparency Translation File +# Copyright (c) 2017 +# This file is distributed under the same license as the dynamic-panel-transparency package. +# +# Evan Welsh , 2017. +# Alonso Lara , 2017. +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-03 11:20-0700\n" +"PO-Revision-Date: 2017-03-04 13:48+0100\n" +"Last-Translator: Alonso Lara \n" +"Language-Team: Evan Welsh \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: prefs.js:89 +msgid "default" +msgstr "por defecto" + +#: prefs.ui:7 +msgid "Classy transparency for your panel." +msgstr "Transparencia con clase para tu panel." + +#: prefs.ui:9 +msgid "Website" +msgstr "Sitio Web" + +#: prefs.ui:72 +msgid "Shell Restart Required." +msgstr "Se requiere reiniciar la línea de comandos." + +#. Meant to ask the user if they would like to restart right now or later in a yes/no format. +#: prefs.ui:73 +msgid "" +"A shell restart is required to view some of your changes. Do you want to " +"restart now?" +msgstr "" +"Se requiere reiniciar la línea de comandos para ver algunos de sus cambios. " +"¿Desea reiniciar ahora?" + +#. How long the transition will take. +#: prefs.ui:134 +msgid "Transition Speed" +msgstr "Velocidad de transición" + +#: prefs.ui:160 prefs.ui:183 +msgid "Overrides 'gtk-enable-animations'." +msgstr "Sobreescribe 'gtk-enable-animations'." + +#. Whether the panel transitions should happen with the overview transitions or after. +#: prefs.ui:171 +msgid "Transition with the overview" +msgstr "" + +#. Whether the panel should transition when a window touches it or only when a window is actually maximized. +#: prefs.ui:194 +msgid "Transition when windows touch the panel" +msgstr "" + +#: prefs.ui:210 +msgid "Transitions" +msgstr "Transiciones" + +#. Allows the user to set their own text color. +#: prefs.ui:257 +msgid "Enable custom text coloring" +msgstr "Acticar color de texto personalizado" + +#. The main color to be used. +#: prefs.ui:281 prefs.ui:294 +msgid "Primary Color" +msgstr "Color primario" + +#. Another color for special functions. +#: prefs.ui:307 prefs.ui:319 +msgid "Secondary Color" +msgstr "Color secundario" + +#: prefs.ui:334 +msgid "Use when a window is maximized" +msgstr "Usar cuando una ventana está maximizada" + +#: prefs.ui:352 +msgid "Use when the overview is visible" +msgstr "Usar cuando la vista general está visible" + +#: prefs.ui:409 +msgid "Enable text shadowing" +msgstr "Activar sombras de texto" + +#: prefs.ui:442 prefs.ui:496 prefs.ui:609 prefs.ui:663 +msgid "Shadow Color" +msgstr "Color de la sombra" + +#: prefs.ui:508 prefs.ui:675 +msgid "Radius" +msgstr "Radio" + +#: prefs.ui:520 prefs.ui:687 +msgid "Horizontal Offset" +msgstr "Desplazamiento horizontal" + +#: prefs.ui:532 prefs.ui:699 +msgid "Vertical Offset" +msgstr "Desplazamiento vertical" + +#: prefs.ui:576 +msgid "Enable icon shadowing" +msgstr "Activar sombras de iconos" + +#: prefs.ui:734 +msgid "Foreground" +msgstr "Primer plano" + +#. Opacity of the panel when no window is maximized. +#: prefs.ui:785 +msgid "Unmaximized Opacity" +msgstr "Opacidad sin maximizado" + +#. Opacity of the panel when a window is maximized. +#: prefs.ui:829 +msgid "Maximized Opacity" +msgstr "Opacidad con maximizado" + +#. Allows the user to set their own opacity. +#: prefs.ui:850 +msgid "Enable custom opacity" +msgstr "Activar opacidad personalizada" + +#. The panel's background color. +#: prefs.ui:902 prefs.ui:915 +msgid "Panel Color" +msgstr "Color del panel" + +#. Allows the user to set their own background color for the panel. +#: prefs.ui:935 +msgid "Enable custom panel color" +msgstr "Activar color del panel personalizado" + +#. Removes any extra styling certain themes may have. Fixes theme issues. +#: prefs.ui:983 +msgid "Remove excess panel styling" +msgstr "Eliminar estilo sobrante del panel" + +#: prefs.ui:990 +msgid "(Fixes theme incompatibilities.)" +msgstr "(Corrigue incompatibilidades del tema.)" + +#. Hides the panel corners. +#: prefs.ui:1008 +msgid "Hide corners" +msgstr "Ocultar esquinas" + +#: prefs.ui:1027 +msgid "Background" +msgstr "Fondo" + +#: prefs.ui:1037 +msgid "About" +msgstr "Acerca de" + +#~ msgid "App Tweaks" +#~ msgstr "Ajustes" + +#~ msgid "Add a Custom WM_CLASS" +#~ msgstr "Añadir un WM_CLASS personalizado" + +#~ msgid "Advanced..." +#~ msgstr "Avanzado..." + +#~ msgid "" +#~ "A WM_CLASS is a code that identifies a window. Use this setting if the " +#~ "window you want custom settings for is not part of an application. You " +#~ "can find a window's WM_CLASS with the command 'xprop WM_CLASS'. Enter the " +#~ "second result from 'xprop WM_CLASS' below without quotation marks." +#~ msgstr "" +#~ "Un WM_CLASS es un código que identifica una ventana. Use este ajuste si " +#~ "la ventana que desea personalizar no es parte de una aplicación. Puede " +#~ "localizar el WM_CLASS de una ventana mediante el comando 'xprop " +#~ "WM_CLASS'. Introduzca el segundo resultado de 'xprop WM_CLASS' sin " +#~ "comillas." + +#~ msgid "WM_CLASS cannot be blank." +#~ msgstr "WM_CLASS no puede estar en blanco." + +#~ msgid "Enable background app tweaks" +#~ msgstr "Activar ajustes de la aplicación de fondo" + +#~ msgid "Theme Source" +#~ msgstr "Fuente del tema" + +#~ msgid "Always trigger the panel." +#~ msgstr "Desencadenar siempre el panel" + +#~ msgid "Force animation" +#~ msgstr "Forzar animación" + +#~ msgid "Transition Style" +#~ msgstr "Estilo de transición" + +#~ msgid "Linear (default)" +#~ msgstr "Lineal (por defecto)" + +#~ msgid "Sine" +#~ msgstr "Senoidal" + +#~ msgid "Quadratic" +#~ msgstr "Cuadrática" + +#~ msgid "Cubic" +#~ msgstr "Cúbica" + +#~ msgid "Quartic" +#~ msgstr "Cuártica" + +#~ msgid "Quintic" +#~ msgstr "Quíntica" + +#~ msgid "Exponential" +#~ msgstr "Exponencial" + +#~ msgid "Circle" +#~ msgstr "Circular" + +#~ msgid "Elastic" +#~ msgstr "Elástica" + +#~ msgid "Bounce" +#~ msgstr "Rebotar" diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/fr/LC_MESSAGES/dynamic-panel-transparency.mo b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/fr/LC_MESSAGES/dynamic-panel-transparency.mo new file mode 100644 index 0000000000000000000000000000000000000000..cb94a389646dce33305eb732ea6037c58e10dad0 GIT binary patch literal 2483 zcmai#%WoS+9LJ}myxdaS@+z+~f=X$CHBKs^bzh`u(pDnGa^e&rBsAU~+e6ngo0-|T zj)Vj!4jeckgt!3WP*Vvm{R>zjBo2r(68r&h;0l5R-`{#|ho%)Htv;Uj_xBqAdT{qg z4DB$!C-Cjv#n=q^4@m8^I~e2O=U@Z;7Q7QYG{sm6JPbYq&VUbr%iyD62tE$JQ;dI7 z@Eef!eFyFZe*zDHzZCofd>Z{-yBXU99s%zIE8tz=8Ss8^5v2VM@DZ>H?gzI(+V=tY z0QfOTwp|A~_yb7$e+Tad|1LNMqh5o}d%)*0{su1F0N)4i1^)s`#}qcb2p#~*PZz*B zaHHsd36kAEf``E0K+-vN&(JUX!3WWQ79@Y41&@Gl7W40c2hsl&BtL&$^ludW9()qx zKZE4&KMU@INRIvykaR7APk{!c^F9Qzlzjn061xt$!6HxsfU>Bkm2~RI11@t zSXs!ts3Hh3R!i&HMPt>L(O$%SrPXrMvt@Brb=4MJvr?#A*_75|ljGBEerpv5G*71d0a?N7G_KRL!L@IS`RYGMn8D7ox7Hd}X=+uko)QixR*&2BgL4{=q;1CM+co`c{BUQM8Re9vGWK%hXu&{M$y$UD3xGbBv zU$RKHL>hZmv*v9X`ruL&V5x(!DlgS#Vyq9AU0X%LxwP$qHG|>WN>G<{u+hPJ;boQ2 zRc2obW-Gzm%Y5!w_1KB$Dsz=e=~dx;kXsrHZ)}xoVYiz6)EUg^b}HI3Sd*e#<%^p_ z^UE@J9exVV^dj`HNR0JpsEn+i?5OgVeQT*+TE4WrI8i!Vu9OzAQEFV+PoRjB7$yT;j=R*OYCSp8&IKG9wWK_Te;c5apbA*WY@ZR5jQl- zFUkhUEeoe`WDD2XAe7DtR5DMfaN+M%#LD5w5V3?|`6-*m6*b+3@u=%{lg4r@*fc3U z1;we@)Wr_?hsymd%iiUQHHmR-9{HbLvoe6M^e_vD1ul6uh_gW(8%7F@hnRB^DY;9N zY+#j4tt_+o&?}@Ij|ESVJ4R~~npY4R8cz=&gKRKfxno&Ks$u1h)Bk@g+3W&afa|Hjr#3#Xy2*69?Y*aap*n*`paeIXYB{Knd4H-)J_PaKmQ@`7s_E`kHKK z^-<(WGCVZ;m%v|xE3, 2017. +# narzb <>, 2016. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-03 11:20-0700\n" +"PO-Revision-Date: 2016-10-27 23:39+0200\n" +"Last-Translator: narzb <>\n" +"Language-Team: Evan Welsh \n" +"Language: fr_FR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.8.9\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: prefs.js:89 +msgid "default" +msgstr "défaut" + +#: prefs.ui:7 +msgid "Classy transparency for your panel." +msgstr "Transparence élégante pour votre Barre supérieure." + +#: prefs.ui:9 +msgid "Website" +msgstr "Site web" + +#: prefs.ui:72 +msgid "Shell Restart Required." +msgstr "Redémarrage du shell requis." + +#. Meant to ask the user if they would like to restart right now or later in a yes/no format. +#: prefs.ui:73 +msgid "" +"A shell restart is required to view some of your changes. Do you want to " +"restart now?" +msgstr "" +"Un redémarrage est nécessaire pour visualiser certains de vos changements. " +"Voulez-vous redémarrer maintenant ?" + +#. How long the transition will take. +#: prefs.ui:134 +msgid "Transition Speed" +msgstr "Vitesse de transition" + +#: prefs.ui:160 prefs.ui:183 +msgid "Overrides 'gtk-enable-animations'." +msgstr "Outrepasser 'gtk-enable-animations'." + +#. Whether the panel transitions should happen with the overview transitions or after. +#: prefs.ui:171 +msgid "Transition with the overview" +msgstr "" + +#. Whether the panel should transition when a window touches it or only when a window is actually maximized. +#: prefs.ui:194 +msgid "Transition when windows touch the panel" +msgstr "" + +#: prefs.ui:210 +msgid "Transitions" +msgstr "Vitesse de transition" + +#. Allows the user to set their own text color. +#: prefs.ui:257 +msgid "Enable custom text coloring" +msgstr "Activer personnalisation couleur du texte" + +#. The main color to be used. +#: prefs.ui:281 prefs.ui:294 +msgid "Primary Color" +msgstr "Couleur Principale" + +#. Another color for special functions. +#: prefs.ui:307 prefs.ui:319 +msgid "Secondary Color" +msgstr "Couleur secondaire" + +#: prefs.ui:334 +msgid "Use when a window is maximized" +msgstr "Utiliser quand la fenêtre est maximisée" + +#: prefs.ui:352 +msgid "Use when the overview is visible" +msgstr "Utiliser dans la Vue d'ensemble" + +#: prefs.ui:409 +msgid "Enable text shadowing" +msgstr "Activer l'ombre du texte" + +#: prefs.ui:442 prefs.ui:496 prefs.ui:609 prefs.ui:663 +msgid "Shadow Color" +msgstr "Couleur de l'ombre" + +#: prefs.ui:508 prefs.ui:675 +msgid "Radius" +msgstr "Rayon" + +#: prefs.ui:520 prefs.ui:687 +msgid "Horizontal Offset" +msgstr "Décalage horizontal" + +#: prefs.ui:532 prefs.ui:699 +msgid "Vertical Offset" +msgstr "Décalage vertical" + +#: prefs.ui:576 +msgid "Enable icon shadowing" +msgstr "Activer l'ombre de l'icône" + +#: prefs.ui:734 +msgid "Foreground" +msgstr "Premier plan" + +#. Opacity of the panel when no window is maximized. +#: prefs.ui:785 +msgid "Unmaximized Opacity" +msgstr "Opacité fenêtre non maximisée" + +#. Opacity of the panel when a window is maximized. +#: prefs.ui:829 +msgid "Maximized Opacity" +msgstr "Opacité fenêtre maximisée" + +#. Allows the user to set their own opacity. +#: prefs.ui:850 +msgid "Enable custom opacity" +msgstr "Activer la personnalisation de l'opacité" + +#. The panel's background color. +#: prefs.ui:902 prefs.ui:915 +msgid "Panel Color" +msgstr "Couleur Barre supérieure" + +#. Allows the user to set their own background color for the panel. +#: prefs.ui:935 +msgid "Enable custom panel color" +msgstr "Activer personnalisation couleur Barre supérieure" + +#. Removes any extra styling certain themes may have. Fixes theme issues. +#: prefs.ui:983 +msgid "Remove excess panel styling" +msgstr "" + +#: prefs.ui:990 +msgid "(Fixes theme incompatibilities.)" +msgstr "(Réparer les incompatibilités de thèmes.)" + +#. Hides the panel corners. +#: prefs.ui:1008 +msgid "Hide corners" +msgstr "Cacher les coins arrondis" + +#: prefs.ui:1027 +msgid "Background" +msgstr "Arrière-plan" + +#: prefs.ui:1037 +msgid "About" +msgstr "À propos" + +#~ msgid "App Tweaks" +#~ msgstr "Réglages applications" + +#~ msgid "Add a Custom WM_CLASS" +#~ msgstr "Ajouter un WM_CLASS personnalisé" + +#~ msgid "Advanced..." +#~ msgstr "Fonctionnalités avancées" + +#~ msgid "" +#~ "A WM_CLASS is a code that identifies a window. Use this setting if the " +#~ "window you want custom settings for is not part of an application. You " +#~ "can find a window's WM_CLASS with the command 'xprop WM_CLASS'. Enter the " +#~ "second result from 'xprop WM_CLASS' below without quotation marks." +#~ msgstr "" +#~ "WM_CLASS est un code qui identifie une fenêtre. Utilisez ce paramètre si " +#~ "la fenêtre que vous voulez personnaliser ne fait pas partie d'une " +#~ "application. Vous pouvez trouver le WM_CLASS d'une fenêtre avec la " +#~ "commande «xprop WM_CLASS». Entrez le deuxième résultat de «xprop " +#~ "WM_CLASS» ci-dessous sans guillemets." + +#~ msgid "WM_CLASS cannot be blank." +#~ msgstr "WM_CLASS ne peut pas être vide." + +#~ msgid "Enable background app tweaks" +#~ msgstr "Activer les réglages d'applications en arrière plan" + +#~ msgid "Theme Source" +#~ msgstr "Source du Thème" + +#~ msgid "Always trigger the panel." +#~ msgstr "Toujours déclencher la coloration de la Barre supérieure" + +#~ msgid "Force animation" +#~ msgstr "Forcer l'animation" + +#~ msgid "Transition Style" +#~ msgstr "Style de transition" + +#~ msgid "Linear (default)" +#~ msgstr "Linéaire (défaut)" diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/pt_BR/LC_MESSAGES/dynamic-panel-transparency.mo b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/pt_BR/LC_MESSAGES/dynamic-panel-transparency.mo new file mode 100644 index 0000000000000000000000000000000000000000..1f5c3f9f602d9d96781cd2d286ce0ccfa65df7de GIT binary patch literal 2510 zcmai#$!{D*6vhh(+Ypw7Kp zJdfkqcZ(1s;O<-TgZ2d|!7sr@@JH}=aQHSMYG56F790m30^a~11wHr}_+Hh2wc@uR zulpI?4gL!50e`P}1AGSGdv*zN5BL&zFL)BX13VA%zKh`f;3~-bybbOL-v=K6KL&aI zH{gR{4}22*1(e{QAg|vKW9|fpDjoz^;Df{9)9Al~Ls!7>L3D{ROlCcY!56@3kbTz$ zp9enxhrq8v30?;efH%PVzyo*n?HvJG??m<80-wV-fvo>skk9!Dgj8`ANj*$b0iV zZXAb4@UTChz{5JZu`bAnjbp;+@596AaKjPN7kfeW8OP;eJlv30v9XVLHueSQ3+E>1 zi5Q;I8{}nJrVdGMQrqdOpxe68L6fhKh*Q!pQ&C7qJ}4KY_IO<_wWCZ1E7vrwOK-8* zF3C+>I+-pjlVhozvfLrpl?gm^YXf7~Pl!`(TLy7jr7O9!rOCu(p}gOe!71ar%8^Mo z<&t&%ExXE4Q5VxjwF{DI>4WV^+f}I!n>Y2wsWP>NbvKO!+6estZSt)#omv9}Rc6V(taT;t-6WhZ` zBMGT4W(B6KX|EA7@fx|H(}ABZP#foy$Y@EGMG*7OUZgZ6=duJ-J%lx6tw~+$LNe=f zoh7Hs+$Reb-ScxvlXzomk|`CaArFm?y^@TMCx^!6*vk#PjJ`NJT6 zncn5Lw(`8qOVUosqf4fGJ((M1aNWB6IF^~bR5>LJq&f{by{3%3K!snHN8#z9(r}W{ zx6WO4T$jO$hU|tnPd96`=Vqq|W{lNGYm-<=25L6DkjvS1uv;i?Cit>AB+jwLg_-2Y zU>?V3iCi*`3z;Fz4SA%kL+$nC3?cLS=FD5lbdY29BlYpxd{H`8B*@f`Z%EUPpZ?gP z3E6)=Hf-c^IW{s;6T?vB5f9~{q@(=yt?yl1Bh6R7_l`8gxz@Yv;4nwp=u~6At!e=- zsN&d`s?e9vuZq0nop3kM=kXn&viC{vE3N})ELRS#czCdv%B@o;M9a1vA&YYq0SmFp z*nT$RjNYdP$4V+FM-3rlK8}S3D)FjgNjR%wsbXz>;WEY7Gu8(P%aj<5_gCx~w5$hL znY=g^^cuE^RfA<;u{-8Lw~aBgJ9b0GgKpJ9RI!hhy=zDgx_JV-VrwnmY^2?mjdLmT zo@INP!Z_>26#2rY)t1ZKrA5R_5)JG0E~C6C(JI?M=um1n<6o=Fv|lX|-hq*msHRR literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/pt_BR/LC_MESSAGES/dynamic-panel-transparency.po b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/pt_BR/LC_MESSAGES/dynamic-panel-transparency.po new file mode 100644 index 0000000..71efd2a --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/pt_BR/LC_MESSAGES/dynamic-panel-transparency.po @@ -0,0 +1,236 @@ +# Dynamic Panel Transparency Translation File +# Copyright (c) 2017 +# This file is distributed under the same license as the dynamic-panel-transparency package. +# +# Evan Welsh , 2017. +# Fábio Nogueira , 2016. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-03 11:20-0700\n" +"PO-Revision-Date: 2016-09-29 15:09-0300\n" +"Last-Translator: Fábio Nogueira \n" +"Language-Team: Evan Welsh \n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.8.9\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: prefs.js:89 +msgid "default" +msgstr "padrão" + +#: prefs.ui:7 +msgid "Classy transparency for your panel." +msgstr "Transparência elegante para o seu painel." + +#: prefs.ui:9 +msgid "Website" +msgstr "Site" + +#: prefs.ui:72 +msgid "Shell Restart Required." +msgstr "É necessário reiniciar o shell." + +#. Meant to ask the user if they would like to restart right now or later in a yes/no format. +#: prefs.ui:73 +msgid "" +"A shell restart is required to view some of your changes. Do you want to " +"restart now?" +msgstr "" +"É necessário reiniciar o shell para visualizar algumas de suas alterações. " +"Gostaria de reiniciar agora?" + +#. How long the transition will take. +#: prefs.ui:134 +msgid "Transition Speed" +msgstr "Velocidade de transição" + +#: prefs.ui:160 prefs.ui:183 +msgid "Overrides 'gtk-enable-animations'." +msgstr "Substitui o 'gtk-enable-animations'." + +#. Whether the panel transitions should happen with the overview transitions or after. +#: prefs.ui:171 +msgid "Transition with the overview" +msgstr "" + +#. Whether the panel should transition when a window touches it or only when a window is actually maximized. +#: prefs.ui:194 +msgid "Transition when windows touch the panel" +msgstr "" + +#: prefs.ui:210 +msgid "Transitions" +msgstr "Transição" + +#. Allows the user to set their own text color. +#: prefs.ui:257 +msgid "Enable custom text coloring" +msgstr "Ativar coloração customizada do texto" + +#. The main color to be used. +#: prefs.ui:281 prefs.ui:294 +msgid "Primary Color" +msgstr "Cor primária" + +#. Another color for special functions. +#: prefs.ui:307 prefs.ui:319 +msgid "Secondary Color" +msgstr "Cor secundária" + +#: prefs.ui:334 +msgid "Use when a window is maximized" +msgstr "Utilizar quando um janela for maximizada" + +#: prefs.ui:352 +msgid "Use when the overview is visible" +msgstr "Utilizar quando a visão geral estiver visível" + +#: prefs.ui:409 +msgid "Enable text shadowing" +msgstr "Habilitar sombras ao texto" + +#: prefs.ui:442 prefs.ui:496 prefs.ui:609 prefs.ui:663 +msgid "Shadow Color" +msgstr "Cor da sombra" + +#: prefs.ui:508 prefs.ui:675 +msgid "Radius" +msgstr "Raio" + +#: prefs.ui:520 prefs.ui:687 +msgid "Horizontal Offset" +msgstr "Deslocamento horizontal" + +#: prefs.ui:532 prefs.ui:699 +msgid "Vertical Offset" +msgstr "Deslocamento vertical" + +#: prefs.ui:576 +msgid "Enable icon shadowing" +msgstr "Habilitar sombreamento do ícone" + +#: prefs.ui:734 +msgid "Foreground" +msgstr "Primeiro plano" + +#. Opacity of the panel when no window is maximized. +#: prefs.ui:785 +msgid "Unmaximized Opacity" +msgstr "Opacidade desmaximizada" + +#. Opacity of the panel when a window is maximized. +#: prefs.ui:829 +msgid "Maximized Opacity" +msgstr "Opacidade máxima" + +#. Allows the user to set their own opacity. +#: prefs.ui:850 +msgid "Enable custom opacity" +msgstr "Habilitar opacidade customizada" + +#. The panel's background color. +#: prefs.ui:902 prefs.ui:915 +msgid "Panel Color" +msgstr "Esquema de cores" + +#. Allows the user to set their own background color for the panel. +#: prefs.ui:935 +msgid "Enable custom panel color" +msgstr "Habilitar painel customizado de cores" + +#. Removes any extra styling certain themes may have. Fixes theme issues. +#: prefs.ui:983 +msgid "Remove excess panel styling" +msgstr "Retirar o excesso de estilo do painel" + +#: prefs.ui:990 +msgid "(Fixes theme incompatibilities.)" +msgstr "(Consertar temas incompatíveis.)" + +#. Hides the panel corners. +#: prefs.ui:1008 +msgid "Hide corners" +msgstr "Ocultar os cantos" + +#: prefs.ui:1027 +msgid "Background" +msgstr "Segundo plano" + +#: prefs.ui:1037 +msgid "About" +msgstr "Sobre" + +#~ msgid "App Tweaks" +#~ msgstr "Aplicativo de ajustes" + +#~ msgid "Add a Custom WM_CLASS" +#~ msgstr "Adicionar uma WM_CLASS customizada" + +#~ msgid "Advanced..." +#~ msgstr "Avançado..." + +#~ msgid "" +#~ "A WM_CLASS is a code that identifies a window. Use this setting if the " +#~ "window you want custom settings for is not part of an application. You " +#~ "can find a window's WM_CLASS with the command 'xprop WM_CLASS'. Enter the " +#~ "second result from 'xprop WM_CLASS' below without quotation marks." +#~ msgstr "" +#~ "Uma WM_CLASS é um código que identiifica uma janela. Utilize esta " +#~ "configuração se a janela que deseja configurar de forma personalizada não " +#~ "for parte de uma aplicação. Você pode encontrar uma WM_CLASS da janela " +#~ "com o comando 'xprop WM_CLASS'. Digite o segundo resultado de 'xprop " +#~ "WM_CLASS' abaixo sem as aspas." + +#~ msgid "WM_CLASS cannot be blank." +#~ msgstr "WM_CLASS não pode ser em branco." + +#~ msgid "Enable background app tweaks" +#~ msgstr "Habilitar o aplicativo de ajustes em segundo plano" + +#~ msgid "Theme Source" +#~ msgstr "Origem do tema" + +#~ msgid "Always trigger the panel." +#~ msgstr "Sempre acionar o painel." + +#~ msgid "Force animation" +#~ msgstr "Forçar animação" + +#~ msgid "Transition Style" +#~ msgstr "Estilo de transição" + +#~ msgid "Linear (default)" +#~ msgstr "Linear (padrão)" + +#~ msgid "Sine" +#~ msgstr "Seno" + +#~ msgid "Quadratic" +#~ msgstr "Ao quadrado" + +#~ msgid "Cubic" +#~ msgstr "Ao cubo" + +#~ msgid "Quartic" +#~ msgstr "Quarta potência" + +#~ msgid "Quintic" +#~ msgstr "Quinta potência" + +#~ msgid "Exponential" +#~ msgstr "Exponencial" + +#~ msgid "Circle" +#~ msgstr "Círculo" + +#~ msgid "Elastic" +#~ msgstr "Elástico" + +#~ msgid "Bounce" +#~ msgstr "Pulo" diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/ru/LC_MESSAGES/dynamic-panel-transparency.mo b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/ru/LC_MESSAGES/dynamic-panel-transparency.mo new file mode 100644 index 0000000000000000000000000000000000000000..e9b790d215994bab73625e0f803ca353f311e5a7 GIT binary patch literal 1324 zcmb7C&u`pB6dp?XWkBMmxNvwuq9uY^>K$l+zJ_lR}J`5ZK9|c|qy1LJRj{(1)=5GO?guf4b z09XQlf&B>M@DJ{-=llkog@5cgW&vLUy7}=GuT1e%;0)q7r}zWV@qPt91AOW}#?boD z^B`_WRkNKwq^olb&xc_5*KYN6`*<)&VXUphrBsrUn}&+Mi1W*d>ajPISW=X0C3V3% zVl&B-D>9~)frt__W-BA9HA)IP*)#8RS@qyTC7Ccuu8NZl))h*obS6)8&AJ*1J)U~2 z)s(5TriCgJN21k%lrcLB6zrnP{x&+68)BFm^vvIrk>TfKz7COS5YoA~S2}H4?aMT! z_NI~OjK0fbC9))`DYTQvnW{wz2FIL+BbkJ-=dfxTsS?6-Ee+%7+&a@$VqJ}&mQ z-77z-{X69ci0?4_jlEM|EpOD7*X(yFxxwt0;G@Lu+Pmd7dzb8Cc@=HH$8)Fr(C*uV zioeV3S9XHn1W8vi!HkLBa|6Mh{PVAFZ$jUs+(u`N+OJ6H_J5*K4jGW;q`37CYboV* eh`Bx8?O!VX`<{pPfV)?L-g|fhPV;X0G5Z}e_~F6; literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/ru/LC_MESSAGES/dynamic-panel-transparency.po b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/ru/LC_MESSAGES/dynamic-panel-transparency.po new file mode 100644 index 0000000..1bdccaa --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/ru/LC_MESSAGES/dynamic-panel-transparency.po @@ -0,0 +1,177 @@ +# Dynamic Panel Transparency Translation File +# Copyright (c) 2017 +# This file is distributed under the same license as the dynamic-panel-transparency package. +# +# Evan Welsh , 2017. +# Alexey Varfolomeev , 2016. +msgid "" +msgstr "" +"Project-Id-Version: GNOME Shell Extension Dynamic Panel Transparency\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-03 11:20-0700\n" +"PO-Revision-Date: 2016-03-07 12:21+0300\n" +"Last-Translator: Alexey Varfolomeev \n" +"Language-Team: Evan Welsh \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.8.7\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#: prefs.js:89 +msgid "default" +msgstr "по-умолчанию" + +#: prefs.ui:7 +msgid "Classy transparency for your panel." +msgstr "" + +#: prefs.ui:9 +msgid "Website" +msgstr "" + +#: prefs.ui:72 +msgid "Shell Restart Required." +msgstr "" + +#. Meant to ask the user if they would like to restart right now or later in a yes/no format. +#: prefs.ui:73 +msgid "" +"A shell restart is required to view some of your changes. Do you want to " +"restart now?" +msgstr "" + +#. How long the transition will take. +#: prefs.ui:134 +msgid "Transition Speed" +msgstr "Скорость перехода" + +#: prefs.ui:160 prefs.ui:183 +msgid "Overrides 'gtk-enable-animations'." +msgstr "Переопределение 'gtk-enable-animations'." + +#. Whether the panel transitions should happen with the overview transitions or after. +#: prefs.ui:171 +msgid "Transition with the overview" +msgstr "" + +#. Whether the panel should transition when a window touches it or only when a window is actually maximized. +#: prefs.ui:194 +msgid "Transition when windows touch the panel" +msgstr "" + +#: prefs.ui:210 +msgid "Transitions" +msgstr "Скорость перехода" + +#. Allows the user to set their own text color. +#: prefs.ui:257 +msgid "Enable custom text coloring" +msgstr "" + +#. The main color to be used. +#: prefs.ui:281 prefs.ui:294 +msgid "Primary Color" +msgstr "Цвет панели" + +#. Another color for special functions. +#: prefs.ui:307 prefs.ui:319 +msgid "Secondary Color" +msgstr "" + +#: prefs.ui:334 +msgid "Use when a window is maximized" +msgstr "" + +#: prefs.ui:352 +msgid "Use when the overview is visible" +msgstr "" + +#: prefs.ui:409 +msgid "Enable text shadowing" +msgstr "Добавить тень для текста" + +#: prefs.ui:442 prefs.ui:496 prefs.ui:609 prefs.ui:663 +msgid "Shadow Color" +msgstr "Цвет панели" + +#: prefs.ui:508 prefs.ui:675 +msgid "Radius" +msgstr "" + +#: prefs.ui:520 prefs.ui:687 +msgid "Horizontal Offset" +msgstr "" + +#: prefs.ui:532 prefs.ui:699 +msgid "Vertical Offset" +msgstr "" + +#: prefs.ui:576 +msgid "Enable icon shadowing" +msgstr "" + +#: prefs.ui:734 +msgid "Foreground" +msgstr "" + +#. Opacity of the panel when no window is maximized. +#: prefs.ui:785 +msgid "Unmaximized Opacity" +msgstr "Максимум прозрачности" + +#. Opacity of the panel when a window is maximized. +#: prefs.ui:829 +msgid "Maximized Opacity" +msgstr "Максимум прозрачности" + +#. Allows the user to set their own opacity. +#: prefs.ui:850 +msgid "Enable custom opacity" +msgstr "" + +#. The panel's background color. +#: prefs.ui:902 prefs.ui:915 +msgid "Panel Color" +msgstr "Цвет панели" + +#. Allows the user to set their own background color for the panel. +#: prefs.ui:935 +msgid "Enable custom panel color" +msgstr "" + +#. Removes any extra styling certain themes may have. Fixes theme issues. +#: prefs.ui:983 +msgid "Remove excess panel styling" +msgstr "" + +#: prefs.ui:990 +msgid "(Fixes theme incompatibilities.)" +msgstr "" + +#. Hides the panel corners. +#: prefs.ui:1008 +msgid "Hide corners" +msgstr "Скрыть углы" + +#: prefs.ui:1027 +msgid "Background" +msgstr "" + +#: prefs.ui:1037 +msgid "About" +msgstr "" + +#~ msgid "Theme Source" +#~ msgstr "Исходная тема" + +#~ msgid "Force animation" +#~ msgstr "Принудительная анимация" + +#~ msgid "Transition Style" +#~ msgstr "Скорость перехода" + +#~ msgid "Linear (default)" +#~ msgstr "по-умолчанию" diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/sr/LC_MESSAGES/dynamic-panel-transparency.mo b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/sr/LC_MESSAGES/dynamic-panel-transparency.mo new file mode 100644 index 0000000000000000000000000000000000000000..e5bbec16d490ea4ab10d4c7816163f9944828084 GIT binary patch literal 3246 zcmaKs%X1V}6vnTLuTgy91AJeUSA-sV0v0%Acqam7Dyclgg$p|~cP1@8eTVLzykrqf zfoxo05h@TxL9No##SoH840&vnt+)LHs1i52vRql^M!$2rCodpfHS_EHIOm@8opbx< zsuhC_&szLm#Bbd_jBN$C+>1Xve}EkP6Kn?)_c3-q_&%5b^WbaXaqt;%5PS~&9()1( zIiCM5=070Wtz5y_qu?6wNpOA49pI}N9|j)*9dISs13mzr2FdRy;N#$TAo=+bTn*j; zp8#)y8^OOo%4;=(CV$((r@+17%U~PGK_4W4=fDTSuVelI-hd{f;G3BL5Q}}_7)be= z4@KuSfJZPs3X*PB|XpL2h+LddAy+{x)#+*ssrUn55m^{g*KUc zC=aUr)A&)296xGLsyFopJ(Lrq(zT{|SK&vxl4knZ)L{1rp9h_yD7furWwB%hc8BfQ zfi3)G4co{4PT@G*6MkTM0k?g;9xvOT$nik(E?abSU&2`Cd9N&cJlkox1(@=Kk|ub! zcExVd)U4XSq|8KD|@Wq$+DYc2OP`ydwJklu3xe|;bwbzUV4$slI02~$?9FJ z!x21N_5)euvSelLpm)iv-paGmk=~M(K=cIBg6$TnYj#$;NYKj3?gg`Y>-=nk^h6xy zAv=cvq~{9HXNRO`_enRf9Nv`A`yybCR*zk@`yfqI99>hF@H`m#d}ARvW(b{vVYzk@ zGE3Lrm}JeA9Y3H$ZuYR?^~R$XD`%H|)*_0sOK{PX6~51-4E>@rGRV!FwCY@9 z`&M1Kmbh{)aoLirjZ{W~k$Y=Nh}^=7?oQ!yhyg{=7P6e}q~?fDU9`^*q7{VZQ5P&& z^jW)GyiJOBUvTZ+iv3eB&23|I%XD1TDQ)SpeH(RW?+7ojv-4sf6&*-Iu$;(SWhY?G zp8P;$1LJVcz*ZmGrFo)7l%y9Jjefz-8T-ox-)NJVZf-JK1UZ(janK4xn%A1C9Y!i; zq;}#pZJL|SlxZg3w*0`*ayeEYy);*s)pa$auHrYUDr%amD{3t4SL13T9Ok^vcVvgm zS#B~AUZ0)a+b26^Qj~YYlUpcT1!1%atC;5XU6#v_3dir{b*NxqWrMu}bKNwR#5J6^ zNORvyG#+lOpHC~5G!qA4C0s<;TSEBMvRg}zWxKoR272hmOnX~{v2%VO6+JIJqaL?B zhu%r^ogH?Nc-MGKpq-=inx)9u0k2J($?b_|r|emdf%Y%@Y3`Qv(9dk!#iQ5ECUT=Gsgu#rJTNV>`yXGqu&Fy3BTZ)fQ%5%H$_bL?+l{*1#qOo7$RP3ARbi zLflzsT2Uk61y$i{8WNwx@EWwQ;B8vXgeSv~)C3O)I5?)J)I<$apW}^8PwDDl7Y~Qj z7}pLlQX`nrgGqQFQ`6x<@ zC@xykDRRVIflt^_s8}9ed_t)dg5wF)9H17hce, 2017. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-03 11:20-0700\n" +"PO-Revision-Date: 2017-11-18 20:00+0100\n" +"Last-Translator: Слободан Терзић \n" +"Language-Team: Evan Welsh \n" +"Language: sr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.0.4\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#: prefs.js:89 +msgid "default" +msgstr "поразумевана" + +#: prefs.ui:7 +msgid "Classy transparency for your panel." +msgstr "Елегантна транспарентност вашег панела." + +#: prefs.ui:9 +msgid "Website" +msgstr "Домаћа страница" + +#: prefs.ui:72 +msgid "Shell Restart Required." +msgstr "Неопходно је поновно покретање шкољке" + +#. Meant to ask the user if they would like to restart right now or later in a yes/no format. +#: prefs.ui:73 +msgid "" +"A shell restart is required to view some of your changes. Do you want to " +"restart now?" +msgstr "Неопходно је поново покренути шкољку. Желите ли да то учиним одмах?" + +#. How long the transition will take. +#: prefs.ui:134 +msgid "Transition Speed" +msgstr "Брзина прелаза" + +#: prefs.ui:160 prefs.ui:183 +msgid "Overrides 'gtk-enable-animations'." +msgstr "Премошћује „gtk-enable-animations“." + +#. Whether the panel transitions should happen with the overview transitions or after. +#: prefs.ui:171 +msgid "Transition with the overview" +msgstr "Прелаз при прегледу" + +#. Whether the panel should transition when a window touches it or only when a window is actually maximized. +#: prefs.ui:194 +msgid "Transition when windows touch the panel" +msgstr "Прелаз када прозор дотакне панел" + +#: prefs.ui:210 +msgid "Transitions" +msgstr "Прелази" + +#. Allows the user to set their own text color. +#: prefs.ui:257 +msgid "Enable custom text coloring" +msgstr "Прилагођено бојење текста" + +#. The main color to be used. +#: prefs.ui:281 prefs.ui:294 +msgid "Primary Color" +msgstr "Примарна боја" + +#. Another color for special functions. +#: prefs.ui:307 prefs.ui:319 +msgid "Secondary Color" +msgstr "Секундарна боја" + +#: prefs.ui:334 +msgid "Use when a window is maximized" +msgstr "Када је прозор максимизован" + +#: prefs.ui:352 +msgid "Use when the overview is visible" +msgstr "Када је видљив преглед" + +#: prefs.ui:409 +msgid "Enable text shadowing" +msgstr "Сенчење текста" + +#: prefs.ui:442 prefs.ui:496 prefs.ui:609 prefs.ui:663 +msgid "Shadow Color" +msgstr "Боја сенке" + +#: prefs.ui:508 prefs.ui:675 +msgid "Radius" +msgstr "Радијус" + +#: prefs.ui:520 prefs.ui:687 +msgid "Horizontal Offset" +msgstr "Хоризонтално одступање" + +#: prefs.ui:532 prefs.ui:699 +msgid "Vertical Offset" +msgstr "Вертикално одступање" + +#: prefs.ui:576 +msgid "Enable icon shadowing" +msgstr "Сенчење иконе" + +#: prefs.ui:734 +msgid "Foreground" +msgstr "Први план" + +#. Opacity of the panel when no window is maximized. +#: prefs.ui:785 +msgid "Unmaximized Opacity" +msgstr "Непрозирност немаксимизованог" + +#. Opacity of the panel when a window is maximized. +#: prefs.ui:829 +msgid "Maximized Opacity" +msgstr "Непрозирност максимизованог" + +#. Allows the user to set their own opacity. +#: prefs.ui:850 +msgid "Enable custom opacity" +msgstr "Прилагођена непрозирност" + +#. The panel's background color. +#: prefs.ui:902 prefs.ui:915 +msgid "Panel Color" +msgstr "Боја панела" + +#. Allows the user to set their own background color for the panel. +#: prefs.ui:935 +msgid "Enable custom panel color" +msgstr "Прилагођена боја панела" + +#. Removes any extra styling certain themes may have. Fixes theme issues. +#: prefs.ui:983 +msgid "Remove excess panel styling" +msgstr "Уклони прекомерно стилизовање панела" + +#: prefs.ui:990 +msgid "(Fixes theme incompatibilities.)" +msgstr "(поправља несагласности у теми)" + +#. Hides the panel corners. +#: prefs.ui:1008 +msgid "Hide corners" +msgstr "Скривени ћошкови" + +#: prefs.ui:1027 +msgid "Background" +msgstr "Позадина" + +#: prefs.ui:1037 +msgid "About" +msgstr "О проширењу" + +#~ msgid "App Tweaks" +#~ msgstr "Штеловање апликација" + +#~ msgid "Add a Custom WM_CLASS" +#~ msgstr "Додај прилагођену WM_CLASS" + +#~ msgid "Advanced..." +#~ msgstr "Напредно..." + +#~ msgid "" +#~ "A WM_CLASS is a code that identifies a window. Use this setting if the " +#~ "window you want custom settings for is not part of an application. You " +#~ "can find a window's WM_CLASS with the command 'xprop WM_CLASS'. Enter the " +#~ "second result from 'xprop WM_CLASS' below without quotation marks." +#~ msgstr "" +#~ "WM_CLASS је код који идентификује прозор. Користите ово подешавање " +#~ "уколико прозор чију поставку желите није део апликације. WM_CLASS прозора " +#~ "можете добити командом „xprop WM_CLASS“. Унесите други резултат добијен " +#~ "том наредбом, без наводника." + +#~ msgid "WM_CLASS cannot be blank." +#~ msgstr "WM_CLASS не може бити празна." + +#~ msgid "Enable background app tweaks" +#~ msgstr "Укључи штеловање позадинске апликације" + +#~ msgid "Theme Source" +#~ msgstr "Основа теме" + +#~ msgid "Always trigger the panel." +#~ msgstr "Увек окини панел." diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/sr@latin/LC_MESSAGES/dynamic-panel-transparency.mo b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/sr@latin/LC_MESSAGES/dynamic-panel-transparency.mo new file mode 100644 index 0000000000000000000000000000000000000000..7b08022711a2b6a531d7ab88225ee68550ba2053 GIT binary patch literal 2669 zcmZvd$!}Xl9LEP}Sq!C6mKMq~fi8*Qxpt`3;>KOlri&oN%0f7H;y1C!?|H_Zndc;? zhe}BF)B{p`YE>zS3(J8@MSxH_C6y2t_a7*t0s$u^E*!Y<{XN^+V5H~Ido#b^Z+^?n z&v&cko2}}V(c++2lynoyUPRMEBHPGJ_^R*7H}SX5d07%yO+SN;HMzj`4-#` z-Ugol?}B^5KS7FXJDeta`@yHcqu@*6B*;MrlD*5|L*OS}egWP_PHuuE_dXU+f!9Ft z>lg50@Gr0*{0F4?cVTh|co-yqr$MssK=SVc@DcDbxDC7xlK(eAlK%l50DlF^@2wD~ z{dR)bf(?MX!8}NIr$NeV1?&ec_&9hK>Z-)1P&{(T_T@lBBY z90Mu8XF;SPtrh9hAbq%(j=nBQhX(mX<5@hU-y6^41tm#i>djFdXipm0COuy`CM$#d zquM`>hhpG(=y;O9loJ{hD?&+YO?juBzKn-zk&gb8YF;|-tFkV+(q&U`2(M;Tth|yg zJHU={SCw(ht#n>k&y~aLxkOnRa&LH3$vN%}l+7$(Fp1^ms?Zgv@)L$8_?*z5Bzp$5 znLEOc&6vcqVNs4M)+9P)$7A8#0{2#E*AQ0f@&cbVwqvp(w2ZTCM2neN@^a$5sdLj1 zW#tz(%%-BeY+_?Ktav%^I}1uzdTXj|Gy)W%nOifPimuL{G*))qJgq`FV62wbvD3z? zmeF3sd~9~sNzX>bysE1fQZv@|ZmcP-g_7fYD?SQj8bTnnsw2%tyS-UfqUiYX)a8=J zf?eo-jf+qvj*ZK@X-Y2V%hEaCiO~6lm^_}4h#*{5nMh;l>QAJV8}C+byj!*`o1`qG zz=(aKA!WFBVy-GRM>3ER91D|_t8_RzyRMT{zOw??ES-Wii;hj{`hQX}`e- zODke*Wtq0?86}aXatd{4?@H@cc{SP7at1+27Rp(X#GaL`smZbr&V&I(9il7n%(!eA z>w{5OQDHEgR9rA=FkKo8#w8gxIyfP`Ebzfx{y>n=2l<0|E#&gA!O`Viefi_=(~nmidCT$Q7u&q^DN;FgCtI|Y7lM)}Np!6}L3+zGE_ zWT-qJ%;vKDGo?7OA`Wo;>#o3cBmH#6HxBX6Yq4MN&gY87Jm0g2(`&A;n9uVgJjWr9 z@59Bx-qKKUKfR_JxuJZHpFiJGflO`yI(g^}4jjs`{)TB-(Tr!exsb^(TXaW-9Oojb(Xp>0$ye?p?V4zYXf%g=<+h zygmlBD1=ObF=|mY^r)#QHgKq6WrRNiq=BPkac3(k6ZJTXF+#In)lTg?sUvc9V^v`z zEvu(MdUcg-821XhW2acOD5XRm3DlTE#eYU8f{{joM B?Pvf1 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/sr@latin/LC_MESSAGES/dynamic-panel-transparency.po b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/sr@latin/LC_MESSAGES/dynamic-panel-transparency.po new file mode 100644 index 0000000..80897c0 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/sr@latin/LC_MESSAGES/dynamic-panel-transparency.po @@ -0,0 +1,196 @@ +# Dynamic Panel Transparency Translation File +# Copyright (c) 2017 +# This file is distributed under the same license as the dynamic-panel-transparency package. +# +# Evan Welsh , 2017. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-03 11:20-0700\n" +"PO-Revision-Date: 2017-11-18 20:01+0100\n" +"Last-Translator: Слободан Терзић \n" +"Language-Team: Evan Welsh \n" +"Language: sr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.0.4\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#: prefs.js:89 +msgid "default" +msgstr "porazumevana" + +#: prefs.ui:7 +msgid "Classy transparency for your panel." +msgstr "Elegantna transparentnost vašeg panela." + +#: prefs.ui:9 +msgid "Website" +msgstr "Domaća stranica" + +#: prefs.ui:72 +msgid "Shell Restart Required." +msgstr "Neophodno je ponovno pokretanje školjke" + +#. Meant to ask the user if they would like to restart right now or later in a yes/no format. +#: prefs.ui:73 +msgid "" +"A shell restart is required to view some of your changes. Do you want to " +"restart now?" +msgstr "Neophodno je ponovo pokrenuti školjku. Želite li da to učinim odmah?" + +#. How long the transition will take. +#: prefs.ui:134 +msgid "Transition Speed" +msgstr "Brzina prelaza" + +#: prefs.ui:160 prefs.ui:183 +msgid "Overrides 'gtk-enable-animations'." +msgstr "Premošćuje „gtk-enable-animations“." + +#. Whether the panel transitions should happen with the overview transitions or after. +#: prefs.ui:171 +msgid "Transition with the overview" +msgstr "Prelaz pri pregledu" + +#. Whether the panel should transition when a window touches it or only when a window is actually maximized. +#: prefs.ui:194 +msgid "Transition when windows touch the panel" +msgstr "Prelaz kada prozor dotakne panel" + +#: prefs.ui:210 +msgid "Transitions" +msgstr "Prelazi" + +#. Allows the user to set their own text color. +#: prefs.ui:257 +msgid "Enable custom text coloring" +msgstr "Prilagođeno bojenje teksta" + +#. The main color to be used. +#: prefs.ui:281 prefs.ui:294 +msgid "Primary Color" +msgstr "Primarna boja" + +#. Another color for special functions. +#: prefs.ui:307 prefs.ui:319 +msgid "Secondary Color" +msgstr "Sekundarna boja" + +#: prefs.ui:334 +msgid "Use when a window is maximized" +msgstr "Kada je prozor maksimizovan" + +#: prefs.ui:352 +msgid "Use when the overview is visible" +msgstr "Kada je vidljiv pregled" + +#: prefs.ui:409 +msgid "Enable text shadowing" +msgstr "Senčenje teksta" + +#: prefs.ui:442 prefs.ui:496 prefs.ui:609 prefs.ui:663 +msgid "Shadow Color" +msgstr "Boja senke" + +#: prefs.ui:508 prefs.ui:675 +msgid "Radius" +msgstr "Radijus" + +#: prefs.ui:520 prefs.ui:687 +msgid "Horizontal Offset" +msgstr "Horizontalno odstupanje" + +#: prefs.ui:532 prefs.ui:699 +msgid "Vertical Offset" +msgstr "Vertikalno odstupanje" + +#: prefs.ui:576 +msgid "Enable icon shadowing" +msgstr "Senčenje ikone" + +#: prefs.ui:734 +msgid "Foreground" +msgstr "Prvi plan" + +#. Opacity of the panel when no window is maximized. +#: prefs.ui:785 +msgid "Unmaximized Opacity" +msgstr "Neprozirnost nemaksimizovanog" + +#. Opacity of the panel when a window is maximized. +#: prefs.ui:829 +msgid "Maximized Opacity" +msgstr "Neprozirnost maksimizovanog" + +#. Allows the user to set their own opacity. +#: prefs.ui:850 +msgid "Enable custom opacity" +msgstr "Prilagođena neprozirnost" + +#. The panel's background color. +#: prefs.ui:902 prefs.ui:915 +msgid "Panel Color" +msgstr "Boja panela" + +#. Allows the user to set their own background color for the panel. +#: prefs.ui:935 +msgid "Enable custom panel color" +msgstr "Prilagođena boja panela" + +#. Removes any extra styling certain themes may have. Fixes theme issues. +#: prefs.ui:983 +msgid "Remove excess panel styling" +msgstr "Ukloni prekomerno stilizovanje panela" + +#: prefs.ui:990 +msgid "(Fixes theme incompatibilities.)" +msgstr "(popravlja nesaglasnosti u temi)" + +#. Hides the panel corners. +#: prefs.ui:1008 +msgid "Hide corners" +msgstr "Skriveni ćoškovi" + +#: prefs.ui:1027 +msgid "Background" +msgstr "Pozadina" + +#: prefs.ui:1037 +msgid "About" +msgstr "O proširenju" + +#~ msgid "App Tweaks" +#~ msgstr "Štelovanje aplikacija" + +#~ msgid "Add a Custom WM_CLASS" +#~ msgstr "Dodaj prilagođenu WM_CLASS" + +#~ msgid "Advanced..." +#~ msgstr "Napredno..." + +#~ msgid "" +#~ "A WM_CLASS is a code that identifies a window. Use this setting if the " +#~ "window you want custom settings for is not part of an application. You " +#~ "can find a window's WM_CLASS with the command 'xprop WM_CLASS'. Enter the " +#~ "second result from 'xprop WM_CLASS' below without quotation marks." +#~ msgstr "" +#~ "WM_CLASS je kod koji identifikuje prozor. Koristite ovo podešavanje " +#~ "ukoliko prozor čiju postavku želite nije deo aplikacije. WM_CLASS prozora " +#~ "možete dobiti komandom „xprop WM_CLASS“. Unesite drugi rezultat dobijen " +#~ "tom naredbom, bez navodnika." + +#~ msgid "WM_CLASS cannot be blank." +#~ msgstr "WM_CLASS ne može biti prazna." + +#~ msgid "Enable background app tweaks" +#~ msgstr "Uključi štelovanje pozadinske aplikacije" + +#~ msgid "Theme Source" +#~ msgstr "Osnova teme" + +#~ msgid "Always trigger the panel." +#~ msgstr "Uvek okini panel." diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/zh_CN/LC_MESSAGES/dynamic-panel-transparency.mo b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/zh_CN/LC_MESSAGES/dynamic-panel-transparency.mo new file mode 100644 index 0000000000000000000000000000000000000000..053d1276b1daf5c760970b6e28949a5c60d233ef GIT binary patch literal 2318 zcmZ{kTWl0n7{`x_q6=O?6t8&H7+Q6fT@)w_D5YEzOPgNA7h|Tor`v(qIm?{cw%Z4@ zluIen77fsr7O17R6=EsUXs`4^UwqWW=!?e0o!Q+7W1@+#`aiQnOKUjE>2J^ZF6X;V ze_grc9Km`4`#J1Y_YhJDF25Ilutq@&-T_&)eFD8OgI zHt+s(kF%h(`w?6Y{syi9|M2)P_!7=5mJqTGd=q>a+zvhf?gypcYVc9;Fev>T0at_V z;A7w*DDA%n9|yky*ML8P6#NI2_N$T1QgE%ubzmGd*aS+wVLVy|eh13;*FahF74TK? zbx_80z@zZk0j|XTHIKJIY4<%SAcRuUMKLur6!=Uv4EhzPV0j1sFp!823%5y1*w8y7m*s?!z$$G-AyW|_PAFHrYYIorY@(i}Di<~#vmnX61!dSTrWnZvlFXg<**_3la z3U+IaOi-(i#TeC$FptGmORLdz&C;0g7m{Ks>X@!mlL<>TEvgCpJe<%>7NHiW^%`rS zg2R~C(j-rqG+d_|QJB)5Twc%y)v%=IZ3hN#c$*a0@PtKnsNq9VlP8P_DbZCSlGHL) zL&Q~+8Q~z9+;f`BZb^dlYH<=gaUM+$ExM?s`kiFd0nz&)9IxVZM70e=~YNHm@noycD zFS@dNW|}Y(bZyi+q%b!HMK!b-%FGS1)=$c1cC^F|x!i=nOnRpZHKHX1sbDc)&nRmQ zGa+a`Lt!O#8F3{;29bG_N;enJzS8Ynh1aXm z{2&Wk%D#vKRe``l)K|gc+_aQZ5!E8fjzm-_RUFslWl9B;j=7=iR4o>w>jS}!N-&@V zf;6}xwBe1{0!4vyF%hCh+&zkI!7ViZb6a4Gj})Y5N9=*N z^!QY+zbkX$SbDt8K0a-aOgO2b-&4(nBt1Tvo;;BqYR>e2l%DBxRZ{869;au_IXi)) zbMB;bsW;b~$_^gMwY1r1uIK+zz}h)_Gt)k5_YT{mH}WF;Oy{4|mx+D+W_q%nWRIS9 zy04SWRCnHU?qa`lan?T7mmWWrp6<%^9Ebm0s>A7OcaB|#Ljv!av%}eztMjncZ{C>1)Y#joC9dJatdYyJ~HdvF0wE$#!y$AvQ4C%W(U*~_dJgo;+U5x1SPZyh7> zoa-LQ&bA;!Sq!&A=vnTw(cG1Gr*E3rt=FY~K5{Rm1>JBzez$lhTb#E3THI1^GmG*QXRtXtbTT`1LKbcA*1pu7=^68?k(v28bFG`?rn<5t G1LQw2_aA@& literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/zh_CN/LC_MESSAGES/dynamic-panel-transparency.po b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/zh_CN/LC_MESSAGES/dynamic-panel-transparency.po new file mode 100644 index 0000000..26fc0fb --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/locale/zh_CN/LC_MESSAGES/dynamic-panel-transparency.po @@ -0,0 +1,232 @@ +# Dynamic Panel Transparency Translation File +# Copyright (c) 2017 +# This file is distributed under the same license as the dynamic-panel-transparency package. +# +# Evan Welsh , 2017. +# Dingzhong Chen , 2016. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-03 11:20-0700\n" +"PO-Revision-Date: 2016-10-01 15:53+0800\n" +"Last-Translator: Dingzhong Chen \n" +"Language-Team: Chinese (China) \n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Gtranslator 2.91.7\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: prefs.js:89 +msgid "default" +msgstr "默认" + +#: prefs.ui:7 +msgid "Classy transparency for your panel." +msgstr "给你的面板加上优雅的透明感。" + +#: prefs.ui:9 +msgid "Website" +msgstr "网站" + +#: prefs.ui:72 +msgid "Shell Restart Required." +msgstr "需要重启 Shell。" + +#. Meant to ask the user if they would like to restart right now or later in a yes/no format. +#: prefs.ui:73 +msgid "" +"A shell restart is required to view some of your changes. Do you want to " +"restart now?" +msgstr "为了观看你的一些更改的效果需要重启 shell。想现在就重启吗?" + +#. How long the transition will take. +#: prefs.ui:134 +msgid "Transition Speed" +msgstr "切换速度" + +#: prefs.ui:160 prefs.ui:183 +msgid "Overrides 'gtk-enable-animations'." +msgstr "改写 'gtk-enable-animations'。" + +#. Whether the panel transitions should happen with the overview transitions or after. +#: prefs.ui:171 +msgid "Transition with the overview" +msgstr "" + +#. Whether the panel should transition when a window touches it or only when a window is actually maximized. +#: prefs.ui:194 +msgid "Transition when windows touch the panel" +msgstr "" + +#: prefs.ui:210 +msgid "Transitions" +msgstr "切换" + +#. Allows the user to set their own text color. +#: prefs.ui:257 +msgid "Enable custom text coloring" +msgstr "启用自定义文字颜色" + +#. The main color to be used. +#: prefs.ui:281 prefs.ui:294 +msgid "Primary Color" +msgstr "主色" + +#. Another color for special functions. +#: prefs.ui:307 prefs.ui:319 +msgid "Secondary Color" +msgstr "副色" + +#: prefs.ui:334 +msgid "Use when a window is maximized" +msgstr "窗口最大化时使用" + +#: prefs.ui:352 +msgid "Use when the overview is visible" +msgstr "概览视图时使用" + +#: prefs.ui:409 +msgid "Enable text shadowing" +msgstr "启用文字阴影" + +#: prefs.ui:442 prefs.ui:496 prefs.ui:609 prefs.ui:663 +msgid "Shadow Color" +msgstr "阴影颜色" + +#: prefs.ui:508 prefs.ui:675 +msgid "Radius" +msgstr "半径" + +#: prefs.ui:520 prefs.ui:687 +msgid "Horizontal Offset" +msgstr "水平偏移" + +#: prefs.ui:532 prefs.ui:699 +msgid "Vertical Offset" +msgstr "垂直偏移" + +#: prefs.ui:576 +msgid "Enable icon shadowing" +msgstr "启用图标阴影" + +#: prefs.ui:734 +msgid "Foreground" +msgstr "前景" + +#. Opacity of the panel when no window is maximized. +#: prefs.ui:785 +msgid "Unmaximized Opacity" +msgstr "非最大化的不透明度" + +#. Opacity of the panel when a window is maximized. +#: prefs.ui:829 +msgid "Maximized Opacity" +msgstr "最大化的不透明度" + +#. Allows the user to set their own opacity. +#: prefs.ui:850 +msgid "Enable custom opacity" +msgstr "启用自定义不透明度" + +#. The panel's background color. +#: prefs.ui:902 prefs.ui:915 +msgid "Panel Color" +msgstr "面板颜色" + +#. Allows the user to set their own background color for the panel. +#: prefs.ui:935 +msgid "Enable custom panel color" +msgstr "启用自定义面板颜色" + +#. Removes any extra styling certain themes may have. Fixes theme issues. +#: prefs.ui:983 +msgid "Remove excess panel styling" +msgstr "移除过度的面板修饰风格" + +#: prefs.ui:990 +msgid "(Fixes theme incompatibilities.)" +msgstr "(修复主题的不兼容性。)" + +#. Hides the panel corners. +#: prefs.ui:1008 +msgid "Hide corners" +msgstr "隐藏圆角" + +#: prefs.ui:1027 +msgid "Background" +msgstr "背景" + +#: prefs.ui:1037 +msgid "About" +msgstr "关于" + +#~ msgid "App Tweaks" +#~ msgstr "应用优化" + +#~ msgid "Add a Custom WM_CLASS" +#~ msgstr "添加自定义 WM_CLASS" + +#~ msgid "Advanced..." +#~ msgstr "高级…" + +#~ msgid "" +#~ "A WM_CLASS is a code that identifies a window. Use this setting if the " +#~ "window you want custom settings for is not part of an application. You " +#~ "can find a window's WM_CLASS with the command 'xprop WM_CLASS'. Enter the " +#~ "second result from 'xprop WM_CLASS' below without quotation marks." +#~ msgstr "" +#~ "WM_CLASS 是用于标识窗口的代码。使用此项设置,如果您想自定义设置的窗口不是" +#~ "应用程序的一部分。您可以用命令 'xprop WM_CLASS' 来查找窗口的 WM_CLASS。输" +#~ "入 'xprop WM_CLASS' 下面的第二个结果,不带引号。" + +#~ msgid "WM_CLASS cannot be blank." +#~ msgstr "WM_CLASS 无法为空。" + +#~ msgid "Enable background app tweaks" +#~ msgstr "启用后台应用优化" + +#~ msgid "Theme Source" +#~ msgstr "主题来源" + +#~ msgid "Always trigger the panel." +#~ msgstr "总是触发面板。" + +#~ msgid "Force animation" +#~ msgstr "强制动画" + +#~ msgid "Transition Style" +#~ msgstr "切换风格" + +#~ msgid "Linear (default)" +#~ msgstr "线性(默认)" + +#~ msgid "Sine" +#~ msgstr "正弦" + +#~ msgid "Quadratic" +#~ msgstr "平方" + +#~ msgid "Cubic" +#~ msgstr "立方" + +#~ msgid "Quartic" +#~ msgstr "四阶" + +#~ msgid "Quintic" +#~ msgstr "五阶" + +#~ msgid "Exponential" +#~ msgstr "指数" + +#~ msgid "Circle" +#~ msgstr "循环" + +#~ msgid "Elastic" +#~ msgstr "弹性" + +#~ msgid "Bounce" +#~ msgstr "跳动" diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/metadata.json b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/metadata.json new file mode 100644 index 0000000..22ebcf5 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/metadata.json @@ -0,0 +1,13 @@ +{ + "_generated": "Generated by SweetTooth, do not edit", + "description": "Miss dynamic panel transparency in 3.32 and up? Try the original dynamic panel with much more customization! This extension will fade your top panel to nothingness when there are no maximized windows present! Never again will the panel be abruptly darkened.\n\nMay be incompatible with some extensions that make extensive changes to the panel.\n\nIf your theme isn't working correctly with this extension enable 'Remove Excessive Panel Styling' in the Background section of preferences. This particularly impacts the default *Ubuntu* theme!", + "gettext-domain": "dynamic-panel-transparency", + "name": "Dynamic Panel Transparency", + "settings-schema": "org.gnome.shell.extensions.dynamic-panel-transparency", + "shell-version": [ + "40" + ], + "url": "https://github.com/ewlsh/dynamic-panel-transparency/", + "uuid": "dynamic-panel-transparency@rockon999.github.io", + "version": 35 +} \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/prefs.js b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/prefs.js new file mode 100644 index 0000000..c65e454 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/prefs.js @@ -0,0 +1,370 @@ +/* exported init, buildPrefsWidget */ + +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gdk = imports.gi.Gdk; +const Gio = imports.gi.Gio; +const Gtk = imports.gi.Gtk; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); + +const Convenience = Me.imports.convenience; +const Util = Me.imports.util; + +const Gettext = imports.gettext.domain('dynamic-panel-transparency'); +const _ = Gettext.gettext; + +const gtk30_ = imports.gettext.domain('gtk30').gettext; + +/* Settings Keys */ +const SETTINGS_ENABLE_BACKGROUND_COLOR = 'enable-background-color'; +const SETTINGS_ENABLE_MAXIMIZED_TEXT_COLOR = 'enable-maximized-text-color'; +const SETTINGS_ENABLE_OPACITY = 'enable-opacity'; +const SETTINGS_ENABLE_OVERVIEW_TEXT_COLOR = 'enable-overview-text-color'; +const SETTINGS_ENABLE_TEXT_COLOR = 'enable-text-color'; +const SETTINGS_HIDE_CORNERS = 'hide-corners'; +const SETTINGS_ICON_SHADOW = 'icon-shadow'; +const SETTINGS_ICON_SHADOW_COLOR = 'icon-shadow-color'; +const SETTINGS_ICON_SHADOW_POSITION = 'icon-shadow-position'; +const SETTINGS_MAXIMIZED_OPACITY = 'maximized-opacity'; +const SETTINGS_MAXIMIZED_TEXT_COLOR = 'maximized-text-color'; +const SETTINGS_PANEL_COLOR = 'panel-color'; +const SETTINGS_REMOVE_PANEL_STYLING = 'remove-panel-styling'; +const SETTINGS_TEXT_COLOR = 'text-color'; +const SETTINGS_TEXT_SHADOW = 'text-shadow'; +const SETTINGS_TEXT_SHADOW_COLOR = 'text-shadow-color'; +const SETTINGS_TEXT_SHADOW_POSITION = 'text-shadow-position'; +const SETTINGS_TRANSITION_SPEED = 'transition-speed'; +const SETTINGS_TRANSITION_WITH_OVERVIEW = 'transition-with-overview'; +const SETTINGS_TRANSITION_WINDOWS_TOUCH = 'transition-windows-touch'; +const SETTINGS_UNMAXIMIZED_OPACITY = 'unmaximized-opacity'; + +const Page = { TRANSITIONS: 0, FOREGROUND: 1, BACKGROUND: 2, ABOUT: 3 }; +Object.freeze(Page); + +/* Color Array Indices */ +const RED = 0; +const GREEN = 1; +const BLUE = 2; +const ALPHA = 3; + +/* Shadow Positioning Indices */ +const HORIZONTAL_OFFSET = 0; +const VERTICAL_OFFSET = 1; +const BLUR_RADIUS = 2; + +/* UI spacing & similar values. */ +const WEBSITE_LABEL_BOTTOM_MARGIN = 50; +const WEBSITE_LABEL_TOP_MARGIN = 20; + +/* Color Scaling Factor (Byte to Decimal) */ +const SCALE_FACTOR = 255.9999999; + +function init() { + Convenience.initTranslations(); +} + +/* UI Setup */ +function buildPrefsWidget() { + /* Stores settings until the user applies them. */ + + /* Get Settings */ + let settings = Convenience.getSettings(); + /* Create a UI Builder */ + let builder = new Gtk.Builder(); + /* Setup Translation */ + builder.set_translation_domain(Me.metadata['gettext-domain']); + /* Get UI File */ + builder.add_from_file(Me.path + '/prefs.ui'); + + /* Main Widget (Grid) */ + let main_widget = builder.get_object('main_box'); + + { + /* Transition speed control */ + let speed_scale = builder.get_object('speed_scale'); + /* Init value. */ + speed_scale.adjustment.set_value(settings.get_int(SETTINGS_TRANSITION_SPEED)); + /* Add default marking. */ + speed_scale.add_mark(settings.get_default_value(SETTINGS_TRANSITION_SPEED).unpack(), Gtk.PositionType.BOTTOM, _("default")); + /* Add formatting */ + speed_scale.set_format_value_func((scale, value) => { + return value + 'ms'; + }); + speed_scale.connect('value-changed', (function(widget) { + settings.set_value(SETTINGS_TRANSITION_SPEED, new GLib.Variant('i', widget.adjustment.get_value())); + }).bind(this)); + + let transition_windows_touch = builder.get_object('transition_windows_touch_check'); + transition_windows_touch.set_active(settings.get_boolean(SETTINGS_TRANSITION_WINDOWS_TOUCH)); + + transition_windows_touch.connect('toggled', (function(widget) { + settings.set_value(SETTINGS_TRANSITION_WINDOWS_TOUCH, new GLib.Variant('b', widget.get_active())); + + }).bind(this)); + + let transition_with_overview = builder.get_object('transition_with_overview_check'); + transition_with_overview.set_active(settings.get_boolean(SETTINGS_TRANSITION_WITH_OVERVIEW)); + + transition_with_overview.connect('toggled', (function(widget) { + settings.set_value(SETTINGS_TRANSITION_WITH_OVERVIEW, new GLib.Variant('b', widget.get_active())); + + }).bind(this)); + } + + /* Setup foreground tab */ + { + let text_color_switch = builder.get_object('text_color_switch'); + let text_color_revealer = builder.get_object('text_color_revealer'); + + text_color_switch.set_active(settings.get_boolean(SETTINGS_ENABLE_TEXT_COLOR)); + text_color_switch.connect('state-set', (function(widget, state) { + settings.set_value(SETTINGS_ENABLE_TEXT_COLOR, new GLib.Variant('b', state)); + text_color_revealer.set_reveal_child(state); + + }).bind(this)); + + let maximized_text_color_switch = builder.get_object('maximized_text_color_check'); + maximized_text_color_switch.set_active(settings.get_boolean(SETTINGS_ENABLE_MAXIMIZED_TEXT_COLOR)); + + maximized_text_color_switch.connect('toggled', (function(widget) { + settings.set_value(SETTINGS_ENABLE_MAXIMIZED_TEXT_COLOR, new GLib.Variant('b', widget.get_active())); + }).bind(this)); + + let overview_text_color_switch = builder.get_object('overview_text_color_check'); + overview_text_color_switch.set_active(settings.get_boolean(SETTINGS_ENABLE_OVERVIEW_TEXT_COLOR)); + + overview_text_color_switch.connect('toggled', (function(widget) { + settings.set_value(SETTINGS_ENABLE_OVERVIEW_TEXT_COLOR, new GLib.Variant('b', widget.get_active())); + }).bind(this)); + + let remove_panel_styling_check = builder.get_object('remove_panel_styling_check'); + remove_panel_styling_check.set_active(settings.get_boolean(SETTINGS_REMOVE_PANEL_STYLING)); + + remove_panel_styling_check.connect('toggled', (function(widget) { + settings.set_value(SETTINGS_REMOVE_PANEL_STYLING, new GLib.Variant('b', widget.get_active())); + }).bind(this)); + + let maximized_text_color_btn = builder.get_object('maximized_text_color_btn'); + let maximized_text_color = settings.get_value(SETTINGS_MAXIMIZED_TEXT_COLOR).deep_unpack(); + + let css_color = 'rgba(' + maximized_text_color[RED] + ',' + maximized_text_color[GREEN] + ',' + maximized_text_color[BLUE] + ', 1.0)'; + let scaled_color = new Gdk.RGBA(); + + if (scaled_color.parse(css_color)) { + maximized_text_color_btn.set_rgba(scaled_color); + } + + maximized_text_color_btn.connect('color-set', (function(color_btn) { + let color = Util.gdk_to_css_color(color_btn.get_rgba()); + let rgb = [color.red, color.green, color.blue]; + + settings.set_value(SETTINGS_MAXIMIZED_TEXT_COLOR, new GLib.Variant('(iii)', rgb)); + }).bind(this)); + + let text_color_btn = builder.get_object('text_color_btn'); + let text_color = settings.get_value(SETTINGS_TEXT_COLOR).deep_unpack(); + + css_color = 'rgba(' + text_color[RED] + ',' + text_color[GREEN] + ',' + text_color[BLUE] + ', 1.0)'; + scaled_color = new Gdk.RGBA(); + + if (scaled_color.parse(css_color)) { + text_color_btn.set_rgba(scaled_color); + } + + text_color_btn.connect('color-set', (function(color_btn) { + let color = Util.gdk_to_css_color(color_btn.get_rgba()); + let rgb = [color.red, color.green, color.blue]; + + settings.set_value(SETTINGS_TEXT_COLOR, new GLib.Variant('(iii)', rgb)); + + }).bind(this)); + + let text_shadow_switch = builder.get_object('text_shadow_switch'); + let text_shadow_revealer = builder.get_object('text_shadow_revealer'); + + text_shadow_switch.set_active(settings.get_boolean(SETTINGS_TEXT_SHADOW)); + + text_shadow_switch.connect('state-set', (function(widget, state) { + settings.set_value(SETTINGS_TEXT_SHADOW, new GLib.Variant('b', state)); + text_shadow_revealer.set_reveal_child(state); + }).bind(this)); + + let text_shadow_vertical_offset = builder.get_object('text_shadow_vertical_offset'); + settings.set_value(SETTINGS_TEXT_SHADOW_POSITION, settings.get_value(SETTINGS_TEXT_SHADOW_POSITION)); + text_shadow_vertical_offset.set_value(settings.get_value(SETTINGS_TEXT_SHADOW_POSITION).deep_unpack()[VERTICAL_OFFSET]); + text_shadow_vertical_offset.connect('value-changed', (function(widget) { + let position = settings.get_value(SETTINGS_TEXT_SHADOW_POSITION).deep_unpack(); + position[VERTICAL_OFFSET] = widget.get_value_as_int(); + settings.set_value(SETTINGS_TEXT_SHADOW_POSITION, new GLib.Variant('(iii)', position)); + }).bind(this)); + + let text_shadow_horizontal_offset = builder.get_object('text_shadow_horizontal_offset'); + text_shadow_horizontal_offset.set_value(settings.get_value(SETTINGS_TEXT_SHADOW_POSITION).deep_unpack()[HORIZONTAL_OFFSET]); + text_shadow_horizontal_offset.connect('value-changed', (function(widget) { + let position = settings.get_value(SETTINGS_TEXT_SHADOW_POSITION).deep_unpack(); + position[HORIZONTAL_OFFSET] = widget.get_value_as_int(); + settings.set_value(SETTINGS_TEXT_SHADOW_POSITION, new GLib.Variant('(iii)', position)); + }).bind(this)); + + let text_shadow_radius = builder.get_object('text_shadow_radius'); + text_shadow_radius.set_value(settings.get_value(SETTINGS_TEXT_SHADOW_POSITION).deep_unpack()[BLUR_RADIUS]); + text_shadow_radius.connect('value-changed', (function(widget) { + let position = settings.get_value(SETTINGS_TEXT_SHADOW_POSITION).deep_unpack(); + position[BLUR_RADIUS] = widget.get_value_as_int(); + settings.set_value(SETTINGS_TEXT_SHADOW_POSITION, new GLib.Variant('(iii)', position)); + }).bind(this)); + + let text_shadow_color_btn = builder.get_object('text_shadow_color'); + text_shadow_color_btn.show_editor = true; + + let text_shadow_color = settings.get_value(SETTINGS_TEXT_SHADOW_COLOR).deep_unpack(); + + css_color = 'rgba(' + text_shadow_color[RED] + ',' + text_shadow_color[GREEN] + ',' + text_shadow_color[BLUE] + ',' + text_shadow_color[ALPHA].toFixed(2) + ')'; + scaled_color = new Gdk.RGBA(); + if (scaled_color.parse(css_color)) + text_shadow_color_btn.set_rgba(scaled_color); + + text_shadow_color_btn.connect('color-set', (function(color_btn) { + let color = Util.gdk_to_css_color(color_btn.get_rgba()); + let alpha = +(color_btn.get_rgba().alpha.toFixed(2)); + + let rgba = [color.red, color.green, color.blue, alpha]; + settings.set_value(SETTINGS_TEXT_SHADOW_COLOR, new GLib.Variant('(iiid)', rgba)); + }).bind(this)); + + let icon_shadow = builder.get_object('icon_shadow_switch'); + let icon_shadow_revealer = builder.get_object('icon_shadow_revealer'); + + icon_shadow.set_active(settings.get_boolean(SETTINGS_ICON_SHADOW)); + + icon_shadow.connect('state-set', (function(widget, state) { + settings.set_value(SETTINGS_ICON_SHADOW, new GLib.Variant('b', state)); + icon_shadow_revealer.set_reveal_child(state); + }).bind(this)); + + let icon_shadow_vertical_offset = builder.get_object('icon_shadow_vertical_offset'); + + settings.set_value(SETTINGS_ICON_SHADOW_POSITION, settings.get_value(SETTINGS_ICON_SHADOW_POSITION)); + icon_shadow_vertical_offset.set_value(settings.get_value(SETTINGS_ICON_SHADOW_POSITION).deep_unpack()[VERTICAL_OFFSET]); + icon_shadow_vertical_offset.connect('value-changed', (function(widget) { + let position = settings.get_value(SETTINGS_ICON_SHADOW_POSITION).deep_unpack(); + position[VERTICAL_OFFSET] = widget.get_value_as_int(); + settings.set_value(SETTINGS_ICON_SHADOW_POSITION, new GLib.Variant('(iii)', position)); + }).bind(this)); + let icon_shadow_horizontal_offset = builder.get_object('icon_shadow_horizontal_offset'); + icon_shadow_horizontal_offset.set_value(settings.get_value(SETTINGS_ICON_SHADOW_POSITION).deep_unpack()[HORIZONTAL_OFFSET]); + icon_shadow_horizontal_offset.connect('value-changed', (function(widget) { + let position = settings.get_value(SETTINGS_ICON_SHADOW_POSITION).deep_unpack(); + position[HORIZONTAL_OFFSET] = widget.get_value_as_int(); + settings.set_value(SETTINGS_ICON_SHADOW_POSITION, new GLib.Variant('(iii)', position)); + }).bind(this)); + let icon_shadow_radius = builder.get_object('icon_shadow_radius'); + icon_shadow_radius.set_value(settings.get_value(SETTINGS_ICON_SHADOW_POSITION).deep_unpack()[BLUR_RADIUS]); + icon_shadow_radius.connect('value-changed', (function(widget) { + let position = settings.get_value(SETTINGS_ICON_SHADOW_POSITION).deep_unpack(); + position[BLUR_RADIUS] = widget.get_value_as_int(); + settings.set_value(SETTINGS_ICON_SHADOW_POSITION, new GLib.Variant('(iii)', position)); + }).bind(this)); + + let icon_shadow_color_btn = builder.get_object('icon_shadow_color'); + icon_shadow_color_btn.show_editor = true; + + let icon_shadow_color = settings.get_value(SETTINGS_ICON_SHADOW_COLOR).deep_unpack(); + + css_color = 'rgba(' + icon_shadow_color[RED] + ',' + icon_shadow_color[GREEN] + ',' + icon_shadow_color[BLUE] + ',' + icon_shadow_color[ALPHA].toFixed(2) + ')'; + scaled_color = new Gdk.RGBA(); + if (scaled_color.parse(css_color)) { + icon_shadow_color_btn.set_rgba(scaled_color); + } + + icon_shadow_color_btn.connect('color-set', (function(color_btn) { + let color = Util.gdk_to_css_color(color_btn.get_rgba()); + let alpha = +(color_btn.get_rgba().alpha.toFixed(2)); + + let rgba = [color.red, color.green, color.blue, alpha]; + + settings.set_value(SETTINGS_ICON_SHADOW_COLOR, new GLib.Variant('(iiid)', rgba)); + }).bind(this)); + } + + /* Setup Background Tab */ + { + let background_color_switch = builder.get_object('background_color_switch'); + let opacity_switch = builder.get_object('opacity_switch'); + let background_color_revealer = builder.get_object('background_color_revealer'); + let opacity_revealer = builder.get_object('opacity_revealer'); + + background_color_switch.set_active(settings.get_boolean(SETTINGS_ENABLE_BACKGROUND_COLOR)); + background_color_switch.connect('state-set', (function(widget, state) { + settings.set_value(SETTINGS_ENABLE_BACKGROUND_COLOR, new GLib.Variant('b', state)); + background_color_revealer.set_reveal_child(state); + }).bind(this)); + + opacity_switch.set_active(settings.get_boolean(SETTINGS_ENABLE_OPACITY)); + opacity_switch.connect('state-set', (function(widget, state) { + settings.set_value(SETTINGS_ENABLE_OPACITY, new GLib.Variant('b', state)); + opacity_revealer.set_reveal_child(state); + + }).bind(this)); + + /* Maximum opacity control */ + let maximum_scale = builder.get_object('maximum_scale'); + /* Init value. */ + maximum_scale.adjustment.set_value(settings.get_int(SETTINGS_MAXIMIZED_OPACITY)); + /* Add formatting */ + maximum_scale.set_format_value_func((scale, value) => { + return (((value / SCALE_FACTOR) * 100).toFixed(0) + '%'); // eslint-disable-line no-magic-numbers + }); + maximum_scale.connect('value-changed', (function(widget) { + settings.set_value(SETTINGS_MAXIMIZED_OPACITY, new GLib.Variant('i', widget.adjustment.get_value())); + }).bind(this)); + + /* Minimum opacity control */ + let minimum_scale = builder.get_object('minimum_scale'); + /* Init value. */ + minimum_scale.adjustment.set_value(settings.get_int(SETTINGS_UNMAXIMIZED_OPACITY)); + /* Add formatting */ + minimum_scale.set_format_value_func((scale, value) => { + return ((value / SCALE_FACTOR) * 100).toFixed(0) + '%'; // eslint-disable-line no-magic-numbers + }); + minimum_scale.connect('value-changed', (function(widget) { + settings.set_value(SETTINGS_UNMAXIMIZED_OPACITY, new GLib.Variant('i', widget.adjustment.get_value())); + }).bind(this)); + + /* Convert & scale color. */ + let panel_color = settings.get_value(SETTINGS_PANEL_COLOR).deep_unpack(); + + let color_btn = builder.get_object('color_btn'); + let css_color = 'rgba(' + panel_color[RED] + ',' + panel_color[GREEN] + ',' + panel_color[BLUE] + ', 1.0)'; + + let scaled_color = new Gdk.RGBA(); + if (scaled_color.parse(css_color)) { + color_btn.set_rgba(scaled_color); + } + color_btn.connect('color-set', (function(color_btn) { + let color = Util.gdk_to_css_color(color_btn.get_rgba()); + let rgb = [color.red, color.green, color.blue]; + + settings.set_value(SETTINGS_PANEL_COLOR, new GLib.Variant('ai', rgb)); + }).bind(this)); + + let hide_corners = builder.get_object('hide_corners_check'); + hide_corners.set_active(settings.get_boolean(SETTINGS_HIDE_CORNERS)); + + hide_corners.connect('toggled', (function(widget) { + settings.set_value(SETTINGS_HIDE_CORNERS, new GLib.Variant('b', widget.get_active())); + }).bind(this)); + } + + let about_button = builder.get_object('about_button'); + let about_dialog = builder.get_object('about_dialog'); + about_dialog.set_version('v' + Me.metadata['version']); + about_button.connect('clicked', () => { + about_dialog.set_transient_for(main_widget.get_root()); + about_dialog.set_modal(true); + about_dialog.present(); + }); + + return main_widget; +} \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/prefs.ui b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/prefs.ui new file mode 100644 index 0000000..b380149 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/prefs.ui @@ -0,0 +1,1053 @@ + + + + + Dynamic Panel Transparency + Version Unknown + Classy transparency for your panel. + https://github.com/ewlsh/dynamic-panel-transparency + Website + Evan Welsh (ewlsh) + Alexey Varfolomeev (varlesh) +Jonatan Hatakeyama Zeidler (jonnius) +Fábio Nogueira (frnogueira) +Mosaab Alzoubi (moceap) +Alonso Lara (AlonsoLP) +Dingzhong Chen (wsxy162) +narzb + + computer + gpl-2-0-only + + + 10 + 1 + 10 + + + 10 + 1 + 10 + + + 10 + 1 + 10 + + + 10 + 1 + 10 + + + 100 + 1 + 10 + + + 10 + 1 + 10 + + + 10 + 1 + 10 + + + + 255 + 1 + 10 + + + 255 + 1 + 10 + + + 0 + question + yes-no + Shell Restart Required. + A shell restart is required to view some of your changes. Do you want to restart now? + 1 + + + 0 + 0 + vertical + 2 + + + + + 5000 + 1 + 10 + + + 10 + 1 + 10 + + + 10 + 1 + 10 + + + 10 + 1 + 10 + + + 0 + vertical + + + 5 + 5 + 5 + 5 + + + + + 0 + + 5 + 5 + 5 + 5 + 5 + 5 + 1 + + + 0 + start + 5 + 5 + 5 + 5 + Transition Speed + 1 + + 0 + 0 + + + + + + 1 + 5 + 5 + 5 + 5 + 1 + speed_adjustment + 0 + + 1 + 0 + + + + + + Overrides 'gtk-enable-animations'. + start + + 5 + 5 + 5 + 5 + + + 0 + 3 + Transition with the overview + + + + 0 + 2 + 2 + + + + + + Overrides 'gtk-enable-animations'. + start + + 5 + 5 + 5 + 5 + + + 0 + 3 + Transition when windows touch the panel + + + + 0 + 1 + 2 + + + + + + + + 0 + 1 + Transitions + + + + + + + 1 + + + 0 + 20 + 20 + 20 + 20 + 1 + 5 + 10 + + + 0 + 1 + 0.029999999329447746 + + + 20 + 20 + 20 + 20 + 0 + 1 + 10 + 1 + + + end + + 1 + 0 + + + + + + 0 + start + + Enable custom text coloring + 1 + + 0 + 0 + + + + + + 0 + 1 + + + 0 + 5 + 5 + 5 + + + 0 + start + + 1 + Primary Color + 1 + + 0 + 0 + + + + + + 1 + end + 1 + Primary Color + + 1 + 0 + + + + + + 0 + start + + 1 + Secondary Color + + 0 + 1 + + + + + + 1 + end + 1 + Secondary Color + + 1 + 1 + + + + + + start + + + + 0 + 3 + Use when a window is maximized + + + + 0 + 2 + 2 + + + + + + start + + + + 0 + 3 + Use when the overview is visible + + + + 0 + 3 + 2 + + + + + + + 0 + 1 + 2 + + + + + + + + + + 0 + 0 + + + + + + 0 + 10 + 1 + 10 + + + 0 + + 1 + 0.05999999865889549 + + + 20 + 20 + 20 + 20 + 0 + 1 + 10 + 1 + + + 0 + start + + Enable text shadowing + + 0 + 0 + + + + + + end + + 1 + 0 + + + + + + 0 + 1 + + + 0 + 10 + 5 + 10 + 10 + 1 + 1 + + + 1 + end + Shadow Color + + 1 + 3 + + + + + + end + + 0 + text_radius_adjustment + 1 + 1 + + 1 + 2 + + + + + + end + + 0 + text_shadow_hz_adjustment + 1 + 1 + + 1 + 1 + + + + + + end + + 0 + text_shadow_vrt_adjustment + 1 + 1 + + 1 + 0 + + + + + + 0 + start + + Shadow Color + + 0 + 3 + + + + + + 0 + start + + Radius + + 0 + 2 + + + + + + 0 + start + + Horizontal Offset + + 0 + 1 + + + + + + 0 + start + + Vertical Offset + + 0 + 0 + + + + + + + 0 + 1 + 2 + + + + + + + + + + + + + 0 + + 1 + 0.05999999865889549 + + + 20 + 20 + 20 + 20 + 0 + 1 + 10 + 1 + + + 0 + start + + Enable icon shadowing + + 0 + 0 + + + + + + end + + 1 + 0 + + + + + + 0 + 1 + + + 0 + 10 + 5 + 10 + 10 + 1 + 1 + + + 1 + end + Shadow Color + + 1 + 3 + + + + + + end + + 0 + adjustment1 + 1 + 1 + + 1 + 2 + + + + + + end + + 0 + adjustment6 + 1 + 1 + + 1 + 1 + + + + + + end + + 0 + adjustment7 + 1 + 1 + + 1 + 0 + + + + + + 0 + start + + Shadow Color + + 0 + 3 + + + + + + 0 + start + + Radius + + 0 + 2 + + + + + + 0 + start + + Horizontal Offset + + 0 + 1 + + + + + + 0 + start + + Vertical Offset + + 0 + 0 + + + + + + + 0 + 1 + 2 + + + + + + + + + + + + 0 + 1 + + + + + + + + 0 + 1 + Foreground + + + + + + + 2 + + + 0 + 20 + 20 + 20 + 20 + 20 + 20 + vertical + + + 0 + 1 + 5 + 10 + + + 0 + 5 + 1 + + + 20 + 20 + 20 + 20 + 0 + + + 0 + 1 + 1 + + + 0 + 10 + + + 0 + start + 5 + 5 + Unmaximized Opacity + 1 + + 0 + 1 + + + + + + 1 + 5 + 5 + 12 + 1 + maximum_opacity_adjustment + 1 + + 1 + 0 + + + + + + 1 + 5 + 5 + 12 + 1 + minimum_opacity_adjustment + 1 + + 1 + 1 + + + + + + 0 + start + 5 + 5 + Maximized Opacity + 1 + + 0 + 0 + + + + + + + 0 + 1 + 2 + + + + + + 0 + start + Enable custom opacity + + 0 + 0 + + + + + + end + + 1 + 0 + + + + + + + + + + 0 + 0 + + + + + + 0 + 5 + 1 + + + 20 + 20 + 20 + 20 + 0 + + + 0 + 1 + 1 + + + 0 + + + 0 + start + 1 + Panel Color + 1 + + 0 + 0 + + + + + + 1 + end + 5 + Panel Color + + 1 + 0 + + + + + + + 0 + 1 + 2 + + + + + + 0 + start + Enable custom panel color + + 0 + 0 + + + + + + end + + 1 + 0 + + + + + + + + + + 0 + 1 + + + + + + + + 0 + start + 10 + 5 + 1 + 5 + 1 + 1 + + + + + 0 + 5 + + + 0 + Remove excess panel styling + 1 + + + + + 0 + (Fixes theme incompatibilities.) + + + + + + 0 + 0 + + + + + + start + + + 0 + 3 + Hide corners + 1 + + + + 0 + 1 + + + + + + + + + + xs + 0 + 1 + Background + + + + + + + + + end + About + 5 + 5 + 5 + 5 + + + + + + + + 100 + 1 + 10 + + diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/schemas/gschemas.compiled b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/schemas/gschemas.compiled new file mode 100644 index 0000000000000000000000000000000000000000..71ad29de86145c6cc04474ea5caeefb491a3832d GIT binary patch literal 1756 zcmZ`(J7^S96n#-wqcI;%G*KHXnMG$8kyuC|snmotLJ$=NC$lfvhr4fvnb{<(M*JXX z5uzxVCRo@gSlB39h$$=tQKJ}0VPX_S!6GUAVxi~E&Tcju2hN_ud2jE#@6NrmXTRks zO9vu)n!pDN+E@~K72wYHw%cT`HH*FC61Z!r5V3Va{Khk}9ERu=li)XiL3Fi~TM*2V5Nb5Tj4M2L1)` zEuiAum2vviRq*eF9|9l0&mN*r&HN|9Q$WWy-9ew4apu5ZfhRBby`WFM8h#}TSq*$X zbZ>}0HRBlI7U1309Y5()^W44QgTVF6*O)@6nSUCd3+(*9`x$-ediWQ>mw;BQW|%(p zCir*2cY*u)v0wD5o8XUwCxCC$u{!$HtKiRozW}e=TOQG;u7O{PKC1@m8%Ag8Q}e!? z!3MDO>=BkA)Vx<4ye{B~9B!sh&3hdMOJJ(4dnGdhuZ-)P~os8pg zx6vI>xK@y+79xp(N`?cK3;ai%zPtAR>@bH=&Cb;F0@IFFn`&Kc*j$MI}wT3ThTVhP~lUgZTU z-4{#$JaesuTBYJ~Wji`5YGE7VcruIPMIRDB$Y+%9EycikRN#N=npP=k+K#8C=STZ1 zm{-;sP+4_crv7>r*z>y##rIeEfGu(X!&33GvIlurvbL;uPR}3WxR$Mg=&Kj4M=8&5 zT9`b8pnm*j7G7z;(r7c^44j_MgS_F81BR;}_h&lp6ofyvxeJpr^UaZ!>$Q ySeLTbOY^UMgLJ9mu^Q%E9(<8eA@ literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/schemas/org.gnome.shell.extensions.dynamic-panel-transparency.gschema.xml b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/schemas/org.gnome.shell.extensions.dynamic-panel-transparency.gschema.xml new file mode 100644 index 0000000..6a532e4 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/schemas/org.gnome.shell.extensions.dynamic-panel-transparency.gschema.xml @@ -0,0 +1,119 @@ + + + + 1000 + Transition Speed + How fast the panel fades in and out. + + + 255 + Maximized Opacity + The opacity of the panel when there are maximized windows [0-255]. + + + 0 + Unmaximized Opacity + The opacity of the panel when there are no maximized windows [0-255]. + + + true + Hide Corners + Hides the rounded corners of the panel. (improves fade aesthetic) + + + false + Force Animation + Overrides 'gtk-enable-animations' on installations where animations are disabled. + + + [0,0,0] + Panel Color + The background color of the panel. + + + false + Enable custom opacities + Whether to enable custom opacities or use the theme values. + + + false + Enable custom background color + Whether to enable custom coloring or use the theme value. + + + false + Add shadowing to text + Adds a shadowing effect to make the panel's text readable on light backgrounds. + + + false + Add shadowing to icons + Adds a shadowing effect to make the panel's icons visible on light backgrounds. + + + (0,3,5) + Text shadow position + The position of the text shadow. (h-offset, v-offset, radius) + + + (0,2,5) + Icon shadow position + The position of the icon shadow. (h-offset, v-offset, radius) + + + (0,0,0,0.5) + Color of shadowing for icons. + The color of the icon shadow. (red, green, blue, alpha) + + + (0,0,0,1.0) + Color of shadowing for text. + The color of the text shadow. (red, green, blue, alpha) + + + (255,255,255) + What color the panel text should be. + What color the panel text should be. + + + (255,255,255) + What color the maximized panel text should be. + What color the panel text should be when windows are maximized + + + false + Enable custom text color + Whether to enable custom primary text coloring or use the user's theme values. + + + false + Enable custom maximized text color + Whether to enable custom secondary text coloring when a window is maximized or not. + + + false + Enable custom overview text color + Whether to enable secondary custom text coloring when the overview is visible. + + + false + Remove incompatible theming + Whether certain usually incompatible theme styles should be removed. + + + 1 + Transition type + The transition function used on the panel + + + true + Transition with overview + Transition the panel at the same time as the overview + + + true + Transition when windows touch the panel + Transition when windows touch the panel + + + \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/settings.js b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/settings.js new file mode 100644 index 0000000..6360dc8 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/settings.js @@ -0,0 +1,269 @@ +/* exported init, cleanup, add, bind, unbind */ + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Params = imports.misc.params; + +const Convenience = Me.imports.convenience; +const Intellifade = Me.imports.intellifade; + +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; + +/* This might impair visibility of the code, but it makes my life a thousand times simpler */ +/* settings.js takes a key and watches for it to change in Gio.Settings & creates a getter for it. */ +/* Also can parse, handle, etc. a setting. */ + +const OVERRIDES_SCHEMA_ID = 'org.gnome.shell.extensions.dynamic-panel-transparency.appOverrides'; + +const GNOME_BACKGROUND_SCHEMA = 'org.gnome.desktop.wm.keybindings'; +const SETTINGS_SHOW_DESKTOP = 'show-desktop'; + +const GNOME_INTERFACE_SCHEMA = 'org.gnome.desktop.interface'; +const SETTINGS_ENABLE_ANIMATIONS = 'enable-animations'; + +function init() { + this._settings = Convenience.getSettings(); + this._background_settings = null; + this._interface_settings = null; + + /* Setup background settings. */ + + try { + let schemaObj = Convenience.getSchemaObj(GNOME_BACKGROUND_SCHEMA, true); + + if (schemaObj) { + this._background_settings = new Gio.Settings({ + settings_schema: schemaObj + }); + } + } catch (error) { } // eslint-disable-line + + try { + let schemaObj = Convenience.getSchemaObj(GNOME_INTERFACE_SCHEMA, true); + + if (schemaObj) { + this._interface_settings = new Gio.Settings({ + settings_schema: schemaObj + }); + } + } catch (error) { } // eslint-disable-line + + this._keys = []; + this._app_keys = {}; + this._overriden_keys = []; + + this.settingsBoundIds = []; + + this._show_desktop = null; + + if (this._background_settings) { + this._show_desktop = this._background_settings.get_strv(SETTINGS_SHOW_DESKTOP).length > 0; + + this.settingsBoundIds.push(this._background_settings.connect('changed::' + SETTINGS_SHOW_DESKTOP, (function() { + this._show_desktop = this._background_settings.get_strv(SETTINGS_SHOW_DESKTOP).length > 0; + }).bind(this))); + } + + if (this._interface_settings) { + this._enable_animations = this._interface_settings.get_boolean(SETTINGS_ENABLE_ANIMATIONS); + + this.settingsBoundIds.push(this._interface_settings.connect('changed::' + SETTINGS_ENABLE_ANIMATIONS, (function() { + this._enable_animations = this._interface_settings.get_boolean(SETTINGS_ENABLE_ANIMATIONS); + }).bind(this))); + } + + this.gs_show_desktop = function() { + return this._show_desktop; + }; + + this.gs_enable_animations = function() { + return this._enable_animations; + }; +} + +function cleanup() { + for (let i = 0; i < this._keys.length; ++i) { + let setting = this._keys[i]; + if (!setting.getter) { + this['get_' + setting.name] = null; + } else { + this[setting.getter] = null; + } + } + + this._keys = null; + this.settingsBoundIds = null; + this._settings = null; +} + +/* Settings Management */ +function add(params) { + let key = { + key: params.key, + name: params.name, + type: params.type, + parser: null, + getter: null, + handler: null + }; + + if (typeof (params.getter) !== 'undefined') + key.getter = params.getter; + if (typeof (params.handler) !== 'undefined') + key.handler = params.handler; + if (typeof (params.parser) !== 'undefined') + key.parser = params.parser; + + this._keys.push(key); +} + +function bind() { + this.settings_manager = new SettingsManager(this._settings, this._keys); + + for (let i = 0; i < this._keys.length; ++i) { + let setting = this._keys[i]; + + /* Watch for changes */ + this.settingsBoundIds.push(this._settings.connect('changed::' + setting.key, (function() { + this.settings_manager.update(setting); + }).bind(this))); + + if (setting.handler) { + this.settingsBoundIds.push(this._settings.connect('changed::' + setting.key, function() { + // TODO: Find a better way to handle settings being changed right as the extension starts up. + try { + setting.handler.call(this); + } catch (error) { + log('[Dynamic Panel Transparency] Error handling setting (' + setting.key + ') change.'); + log(error); + } + })); + } + + let parser = (setting.parser !== null ? setting.parser : function(input) { + return input; + }); + + let getter = function() { + return parser(this.settings_manager[setting.name]); + }; + + if (!setting.getter) { + this['get_' + setting.name] = getter; + } else { + this[setting.getter] = getter; + } + } +} + +function unbind() { + for (let i = 0; i < this.settingsBoundIds.length; ++i) { + this._settings.disconnect(this.settingsBoundIds[i]); + } +} + +/* Basic class to hold settings values */ +class SettingsManager { + constructor(settings, params) { + + this.values = []; + this.settings = settings; + + for (let i = 0; i < params.length; ++i) { + let setting = params[i]; + this.values.push(setting); + if (this.settings.list_keys().indexOf(setting.key) === -1 || !setting) + continue; + let variant = GLib.VariantType.new(setting.type); + if (variant.is_array() || variant.is_tuple()) { + this[setting.name] = this.settings.get_value(setting.key).deep_unpack(); + } else { + this[setting.name] = this.settings.get_value(setting.key).unpack(); + } + } + } + + update(setting) { + if (this.settings.list_keys().indexOf(setting.key) === -1) + return; + + let variant = GLib.VariantType.new(setting.type); + + if (variant.is_array() || variant.is_tuple()) { + this[setting.name] = this.settings.get_value(setting.key).deep_unpack(); + } else { + this[setting.name] = this.settings.get_value(setting.key).unpack(); + } + } +} + +class AppSettingsManager { + constructor(params, apps, path) { + this.values = []; + this.settings = {}; + this.settingsBoundIds = {}; + + for (let setting_key of Object.keys(params)) { + let setting = params[setting_key]; + this[setting.name] = {}; + + this.values.push(setting); + } + + for (let a = 0; a < apps.length; ++a) { + let app_id = apps[a]; + let app_path = path + app_id + '/'; + let sett = null; + + let obj = Convenience.getSchemaObj(OVERRIDES_SCHEMA_ID); + sett = new Gio.Settings({ path: app_path, settings_schema: obj }); + + this.settings[app_id] = sett; + + for (let setting_key of Object.keys(params)) { + let setting = params[setting_key]; + + if (!setting || this.settings[app_id].list_keys().indexOf(setting.key) === -1) { + continue; + } + + if (!this.settingsBoundIds[app_id]) { + this.settingsBoundIds[app_id] = []; + } + + this.settingsBoundIds[app_id].push(this.settings[app_id].connect('changed::' + setting.key, (function() { + this.update(setting, app_id); + }).bind(this))); + + let variant = GLib.VariantType.new(setting.type); + + if (variant.is_array() || variant.is_tuple()) { + this[setting.name][app_id] = this.settings[app_id].get_value(setting.key).deep_unpack(); + } else { + this[setting.name][app_id] = this.settings[app_id].get_value(setting.key).unpack(); + } + } + } + } + + update(setting, app_id) { + if (!setting || this.settings[app_id].list_keys().indexOf(setting.key) === -1) + return; + let variant = GLib.VariantType.new(setting.type); + if (!this[setting.name]) + this[setting.name] = {}; + if (variant.is_array() || variant.is_tuple()) { + this[setting.name][app_id] = this.settings[app_id].get_value(setting.key).deep_unpack(); + } else { + this[setting.name][app_id] = this.settings[app_id].get_value(setting.key).unpack(); + } + } + + unbind() { + for (let app_id of Object.keys(this.settings)) { + for (let id of this.settingsBoundIds[app_id]) { + this.settings[app_id].disconnect(id); + } + } + } +} \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/background/panel-custom.dpt.css b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/background/panel-custom.dpt.css new file mode 100644 index 0000000..78c4946 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/background/panel-custom.dpt.css @@ -0,0 +1,2 @@ +.dpt-panel-custom-unmaximized { background-color: rgba(36, 31, 49, 0.12); } +.dpt-panel-custom-maximized { background-color: rgba(36, 31, 49, 0.35); } \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/background/panel.dpt.css b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/background/panel.dpt.css new file mode 100644 index 0000000..8ddcde9 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/background/panel.dpt.css @@ -0,0 +1,2 @@ +.dpt-panel-unmaximized { background-color: rgba(0, 0, 0, 0.12); } +.dpt-panel-maximized { background-color: rgba(0, 0, 0, 0.35); } \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/foreground/panel-icon-shadow.dpt.css b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/foreground/panel-icon-shadow.dpt.css new file mode 100644 index 0000000..385c12f --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/foreground/panel-icon-shadow.dpt.css @@ -0,0 +1,2 @@ +.dpt-panel-icon-shadow .system-status-icon { icon-shadow: 0px 2px 5px rgba(0, 0, 0, 0.50); } +.dpt-panel-arrow-shadow .popup-menu-arrow { icon-shadow: 0px 2px 5px rgba(0, 0, 0, 0.50); } \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/foreground/panel-maximized-text-color.dpt.css b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/foreground/panel-maximized-text-color.dpt.css new file mode 100644 index 0000000..53aa225 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/foreground/panel-maximized-text-color.dpt.css @@ -0,0 +1,3 @@ +.dpt-panel-maximized-text-color .panel-button { color: rgb(255, 255, 255); } +.dpt-panel-maximized-icon-color .system-status-icon { color: rgb(255, 255, 255); } +.dpt-panel-maximized-arrow-color .popup-menu-arrow { color: rgb(255, 255, 255); } \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/foreground/panel-text-color.dpt.css b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/foreground/panel-text-color.dpt.css new file mode 100644 index 0000000..a968f40 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/foreground/panel-text-color.dpt.css @@ -0,0 +1,3 @@ +.dpt-panel-text-color .panel-button { color: rgb(255, 255, 255); } +.dpt-panel-icon-color .system-status-icon { color: rgb(255, 255, 255); } +.dpt-panel-arrow-color .popup-menu-arrow { color: rgb(255, 255, 255); } \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/foreground/panel-text-shadow.dpt.css b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/foreground/panel-text-shadow.dpt.css new file mode 100644 index 0000000..74b2b10 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/foreground/panel-text-shadow.dpt.css @@ -0,0 +1 @@ +.dpt-panel-text-shadow .panel-button { text-shadow: 0px 3px 5px rgba(0, 0, 0, 1.00); } \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/transitions/panel-transition-duration.dpt.css b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/transitions/panel-transition-duration.dpt.css new file mode 100644 index 0000000..e2f6b67 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/styles/transitions/panel-transition-duration.dpt.css @@ -0,0 +1 @@ +.dpt-panel-transition-duration { transition-duration: 1000ms; } \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/stylesheet.css b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/stylesheet.css new file mode 100644 index 0000000..702169d --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/stylesheet.css @@ -0,0 +1,22 @@ +/* Used to remove odd effects when full transparency is needed. */ + +.panel-transparency { + background-color: rgba(0, 0, 0, 0); +} + +.panel-effect-transparency { + box-shadow: none; + border: none; + /* Gnome Shell specific CSS */ + background-gradient-direction: none; + background-gradient-start: none; + background-gradient-end: none; +} + +/* Used to remove backgrounds when full transparency is needed. */ + +.panel-background-image-transparency { + border-image: none; + background-image: none; +} + diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/theming.js b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/theming.js new file mode 100644 index 0000000..ef88d6e --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/theming.js @@ -0,0 +1,505 @@ +/* exported init, cleanup, remove_maximized_background_color, remove_unmaximized_background_color, set_maximized_background_color, set_unmaximized_background_color, remove_background_color, register_text_shadow, add_text_shadow, register_icon_shadow, add_icon_shadow, has_text_shadow, has_icon_shadow, remove_text_shadow, remove_icon_shadow, register_text_color, set_text_color, remove_text_color, set_panel_color, set_corner_color, clear_corner_color, get_background_image_color, get_background_color, get_maximized_opacity, get_unmaximized_opacity, strip_panel_styling, reapply_panel_styling, strip_panel_background_image, reapply_panel_background_image, strip_panel_background, reapply_panel_background, set_background_alpha */ + +const St = imports.gi.St; + +const Main = imports.ui.main; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Params = imports.misc.params; + +const Settings = Me.imports.settings; +const Util = Me.imports.util; + +const GdkPixbuf = imports.gi.GdkPixbuf; +const GLib = imports.gi.GLib; + +/* Convenience constant for the shell panel. */ +const Panel = Main.panel; + +/* Constants for theme opacity detection. */ +const THEME_OPACITY_THRESHOLD = 50; + +/* Constants for color averaging. */ +const SATURATION_WEIGHT = 1.5; +const WEIGHT_THRESHOLD = 1.0; +const ALPHA_THRESHOLD = 24; + +/* Scale factor for color conversion. */ +const SCALE_FACTOR = 255.9999999; + +/** + * @typedef {Object} Color - Represents a standard color object + * @property {number} red - Red value ranging from 0-255. + * @property {number} green - Green value ranging from 0-255. + * @property {number} blue - Blue value ranging from 0-255. + * @property {number} [alpha=1.0] - Alpha value ranging from 0-1.0 with support for two decimal places. + */ + +/** + * Intialize. + * + */ +function init() { + this.stylesheets = []; + this.styles = []; + + this.background_styles = []; + + update_transition_css(); +} + +/** + * Used to release any held assets of theming. + * + */ +function cleanup() { + let theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); + + for (let style of this.styles) { + Panel.remove_style_class_name(style); + } + + for (let style of this.background_styles) { + Panel.remove_style_class_name(style); + } + + for (let sheet of this.stylesheets) { + theme.unload_stylesheet(Util.get_file(sheet)); + Util.remove_file(sheet); + } + + this.background_styles = null; + this.stylesheets = null; + this.styles = null; +} + +/** + * Registers a shadow stylesheet for text in the panel. + * + * @param {Color} text_color - Object representing an RGBA color. + * @param {Number[]} text_position - Integer array containing horizontal offset, vertical offset, radius. (in that order) + */ +function register_text_shadow(text_color, text_position) { + let text_color_css = 'rgba(' + text_color.red + ', ' + text_color.green + ', ' + text_color.blue + ', ' + text_color.alpha.toFixed(2) + ')'; + let text_position_css = '' + text_position[0] + 'px ' + text_position[1] + 'px ' + text_position[2] + 'px'; + + register_style('dpt-panel-text-shadow'); + + return apply_stylesheet_css('.dpt-panel-text-shadow .panel-button { text-shadow: ' + text_position_css + ' ' + text_color_css + '; }', 'foreground/panel-text-shadow'); +} + +/** + * Adds the currently registered shadow stylesheet to the text in the panel. + * + * @param {Color} text_color - Object representing an RGBA color. + * @param {Number[]} text_position - Integer array containing horizontal offset, vertical offset, radius. (in that order) + */ +function add_text_shadow() { + Panel.add_style_class_name('dpt-panel-text-shadow'); +} + +/** + * Register a shadow stylesheet for icons in the panel. + * + * @param {Color} icon_color - Object representing an RGBA color. + * @param {Number[]} icon_position - Integer array containing horizontal offset, vertical offset, radius. (in that order) + */ +function register_icon_shadow(icon_color, icon_position) { + let icon_color_css = 'rgba(' + icon_color.red + ', ' + icon_color.green + ', ' + icon_color.blue + ', ' + icon_color.alpha.toFixed(2) + ')'; + let icon_position_css = '' + icon_position[0] + 'px ' + icon_position[1] + 'px ' + icon_position[2] + 'px'; + + let stylesheet = apply_stylesheet_css('.dpt-panel-icon-shadow .system-status-icon { icon-shadow: ' + icon_position_css + ' ' + icon_color_css + '; }\n.dpt-panel-arrow-shadow .popup-menu-arrow { icon-shadow: ' + icon_position_css + ' ' + icon_color_css + '; }', 'foreground/panel-icon-shadow'); + + register_style('dpt-panel-icon-shadow'); + register_style('dpt-panel-arrow-shadow'); + + return stylesheet; +} + +/** + * Adds the currently register shadow stylesheet to icons in the panel. + * + */ +function add_icon_shadow() { + Panel.add_style_class_name('dpt-panel-icon-shadow'); + Panel.add_style_class_name('dpt-panel-arrow-shadow'); +} + +/** + * Determines if the panel currently has text shadowing applied. + * + * @returns {Boolean} If the panel has text shadowing. + */ +function has_text_shadow() { + return Panel.has_style_class_name('dpt-panel-text-shadow'); +} + +/** + * Determines if the panel currently has icon shadowing applied. + * + * @returns {Boolean} If the panel has icon shadowing. + */ +function has_icon_shadow() { + return (Panel.has_style_class_name('dpt-panel-icon-shadow') || Panel.has_style_class_name('dpt-panel-arrow-shadow')); +} + +/** + * Removes any text shadowing; deregistering the stylesheet and removing the css. + * + */ +function remove_text_shadow() { + Panel.remove_style_class_name('dpt-panel-text-shadow'); +} + +/** + * Removes any icon shadowing; deregistering the stylesheet and removing the css. + * + */ +function remove_icon_shadow() { + Panel.remove_style_class_name('dpt-panel-icon-shadow'); + Panel.remove_style_class_name('dpt-panel-arrow-shadow'); +} + +/** + * Registers text & icon coloring. + * + * @param {Color} color - Object containing an RGB color value. + * @param {string} prefix - What prefix to apply to the stylesheet. '-' is the default. + */ +function register_text_color(color, prefix) { + let color_css = 'color: rgb(' + color.red + ', ' + color.green + ', ' + color.blue + ');'; + + if (prefix) { + prefix = '-' + prefix + '-'; + } else { + prefix = '-'; + } + + let stylesheet = apply_stylesheet_css('.dpt-panel' + prefix + 'text-color .panel-button { ' + color_css + ' }\n.dpt-panel' + prefix + 'icon-color .system-status-icon { ' + color_css + ' }\n.dpt-panel' + prefix + 'arrow-color .popup-menu-arrow { ' + color_css + ' }', 'foreground/panel' + prefix + 'text-color'); + + register_style('dpt-panel' + prefix + 'text-color'); + register_style('dpt-panel' + prefix + 'icon-color'); + register_style('dpt-panel' + prefix + 'arrow-color'); + + return stylesheet; +} + +/** + * Sets which registered text color stylesheet to use for the text coloring. @see register_text_color + * + * @param {string} prefix - What stylesheet prefix to retrieve. '-' is the default. + */ +function set_text_color(prefix) { + if (prefix) { + prefix = '-' + prefix + '-'; + } else { + prefix = '-'; + } + + Panel.add_style_class_name('dpt-panel' + prefix + 'text-color'); + Panel.add_style_class_name('dpt-panel' + prefix + 'icon-color'); + Panel.add_style_class_name('dpt-panel' + prefix + 'arrow-color'); +} + +/** + * Remove a registered text color stylesheet from the panel. @see set_text_color + * + * @param {string} prefix - What stylesheet prefix to retrieve. '-' is the default. + */ +function remove_text_color(prefix) { + if (prefix) { + prefix = '-' + prefix + '-'; + } else { + prefix = '-'; + } + + Panel.remove_style_class_name('dpt-panel' + prefix + 'text-color'); + Panel.remove_style_class_name('dpt-panel' + prefix + 'icon-color'); + Panel.remove_style_class_name('dpt-panel' + prefix + 'arrow-color'); +} + +/** + * Registers any custom style so that it can be removed when the extension is disabled. + * + * @param {string} style - The name of a CSS styling. + */ +function register_style(style) { + if (this.styles.indexOf(style) === -1) { + this.styles.push(style); + } +} + +/** + * Set's the panel corners' actors to a specific background color. + * + * @param {Color} color [color={}] - Object containing an RGBA color value. + */ +// TODO: Gnome needs CSS styling for the corners. +function set_corner_color(color) { + let panel_color = get_background_color(); + + color = Params.parse(color, { + red: panel_color.red, + green: panel_color.green, + blue: panel_color.blue, + alpha: 0 + }); + + let opacity = Util.clamp(color.alpha / SCALE_FACTOR, 0, 1).toFixed(2); + + /* I strongly dislike using a deprecated method (set_style) + * but this is a hold over from the older extension code and + * the only way to keep per-app coloring working with corners. */ + let coloring = '-panel-corner-background-color: rgba(' + color.red + ', ' + color.green + ', ' + color.blue + ', ' + opacity + ');' + + '' + '-panel-corner-border-color: transparent;'; + + // TODO: Update this code. We're using @deprecated code. + Panel._leftCorner.set_style(coloring); + Panel._rightCorner.set_style(coloring); +} + +/** + * Removes any corner styling this extension has applied. + * + */ +function clear_corner_color() { + Panel._leftCorner.set_style(null); + Panel._rightCorner.set_style(null); +} + +/** + * Returns the user's desired panel color from Settings. Handles theme detection again. + * DEPENDENCY: Settings + * + * @returns {Object} Object containing an RGBA color value. + */ +function get_background_color() { + if (!Settings.enable_custom_background_color()) { + return { + red: 0, + blue: 0, + green: 0 + }; + } + + return Settings.get_panel_color(); +} + +/** + * Returns the user's desired maximized panel opacity from Settings or their theme. + * DEPENDENCY: Settings + * TODO: Needs better system to determine when default theme opacities are too low. + * + * @returns {Number} Alpha value from 0-255. + */ +function get_maximized_opacity() { + let maximized_opacity = Settings.get_maximized_opacity(); + + if (!Settings.enable_custom_opacity()) { + return 255; + } else { + return maximized_opacity; + } +} + +/** + * Returns the user's desired unmaximized panel opacity from Settings or their theme. + * DEPENDENCY: Settings + * + * @returns {Number} Alpha value from 0-255. + */ +function get_unmaximized_opacity() { + if (Settings.enable_custom_opacity()) { + return Settings.get_unmaximized_opacity(); + } else { + return 50; + } +} + +/** + * Applies the style class 'panel-transparency' which makes the panel fully transparent. + * + */ +function apply_panel_transparency() { + Panel.add_style_class_name('panel-transparency'); +} + +/** + * Applies the style class 'panel-transparency' which makes the panel fully transparent. + * + */ +function remove_panel_transparency() { + Panel.remove_style_class_name('panel-transparency'); +} + +/** + * Applies the style class 'panel-effect-transparency' and removes the basic CSS preventing this extension's transitions. + * + */ +function strip_panel_styling() { + Panel.add_style_class_name('panel-effect-transparency'); +} + +/** + * Removes the style class 'panel-effect-transparency' and enables the stock CSS preventing this extension's transitions. + * + */ +function reapply_panel_styling() { + Panel.remove_style_class_name('panel-effect-transparency'); +} + +/** + * Applies the style class 'panel-background-image-transparency' and removes the basic CSS preventing this extension's transitions. + * + */ +function strip_panel_background_image() { + Panel.add_style_class_name('panel-background-image-transparency'); +} + +/** + * Removes the style class 'panel-background-image-transparency' and enables the stock CSS preventing this extension's transitions. + * + */ +function reapply_panel_background_image() { + Panel.remove_style_class_name('panel-background-image-transparency'); +} + +/** + * Writes CSS data to a file and loads the stylesheet into the Shell. + * + * @param {string} css - CSS data. + * @param {string} name - Name of the intended CSS stylesheet. + * + * @returns {string} Filename of the stylesheet. + */ +function apply_stylesheet_css(css, name) { + let file_name = GLib.build_filenamev([GLib.get_user_data_dir(), 'gnome-shell', 'extensions', Me.uuid, 'styles', name + '.dpt.css']); + + /* Write to the file. */ + if (!Util.write_to_file(file_name, css)) { + log('[Dynamic Panel Transparency] Could not access: ' + file_name + ''); + log('[Dynamic Panel Transparency] The extension will not function until access is granted.'); + return null; + } + + let theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); + + if (theme.load_stylesheet(Util.get_file(file_name))) { + this.stylesheets.push(file_name); + } else { + log('[Dynamic Panel Transparency] Error Loading Temporary Stylesheet: ' + name); + return null; + } + + return file_name; +} + +/* Backend24 (3.24+) Specific Functions (Not backwards compatible) */ + +function initialize_background_styles() { + register_background_color(Settings.get_panel_color(), 'custom'); + register_background_color({ + red: 0, + green: 0, + blue: 0 + }); +} + +function cleanup_background_styles() { + remove_background_color(); +} + +function register_background_style(style) { + if (this.background_styles.indexOf(style) === -1) { + this.background_styles.push(style); + } +} + +function register_background_color(bg_color, prefix) { + let suffix = (prefix ? '-' + prefix : ''); + + if (prefix) { + prefix = '-' + prefix + '-'; + } else { + prefix = '-'; + } + + let maximized_opacity = Util.clamp(get_maximized_opacity() / SCALE_FACTOR, 0, 1).toFixed(2); + let unmaximized_opacity = Util.clamp(get_unmaximized_opacity() / SCALE_FACTOR, 0, 1).toFixed(2); + + let maximized_bg_color_css = 'rgba(' + bg_color.red + ', ' + bg_color.green + ', ' + bg_color.blue + ', ' + maximized_opacity + ')'; + let unmaximized_bg_color_css = 'rgba(' + bg_color.red + ', ' + bg_color.green + ', ' + bg_color.blue + ', ' + unmaximized_opacity + ')'; + + register_background_style('dpt-panel' + prefix + 'maximized'); + register_background_style('dpt-panel' + prefix + 'unmaximized'); + + let file_prefix = 'background/panel'; + + let panel = apply_stylesheet_css('.dpt-panel' + prefix + 'unmaximized { background-color: ' + unmaximized_bg_color_css + '; }\n.dpt-panel' + prefix + 'maximized { background-color: ' + maximized_bg_color_css + '; }', file_prefix + suffix); + + return panel; +} + +function set_unmaximized_background_color(prefix) { + if (prefix) { + prefix = '-' + prefix + '-'; + } else { + prefix = '-'; + } + + let style = 'dpt-panel' + prefix + 'unmaximized'; + + Panel.add_style_class_name(style); +} + +function set_maximized_background_color(prefix) { + if (prefix) { + prefix = '-' + prefix + '-'; + } else { + prefix = '-'; + } + + let style = 'dpt-panel' + prefix + 'maximized'; + + Panel.add_style_class_name(style); +} + +function remove_unmaximized_background_color(prefix) { + if (prefix) { + prefix = '-' + prefix + '-'; + } else { + prefix = '-'; + } + + Panel.remove_style_class_name('dpt-panel' + prefix + 'unmaximized'); +} + +function remove_maximized_background_color(prefix) { + if (prefix) { + prefix = '-' + prefix + '-'; + } else { + prefix = '-'; + } + + Panel.remove_style_class_name('dpt-panel' + prefix + 'maximized'); +} + +function remove_background_color() { + remove_unmaximized_background_color('custom'); + remove_unmaximized_background_color(); + + remove_maximized_background_color('custom'); + remove_maximized_background_color(); +} + +function update_transition_css() { + let duration_css = Settings.get_transition_speed(); + + let stylesheet = apply_stylesheet_css('.dpt-panel-transition-duration { transition-duration: ' + duration_css + 'ms; }', 'transitions/panel-transition-duration'); + + Panel.add_style_class_name('dpt-panel-transition-duration'); + + register_style('dpt-panel-transition-duration'); + + return stylesheet; +} diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/transitions.js b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/transitions.js new file mode 100644 index 0000000..f395bec --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/transitions.js @@ -0,0 +1,274 @@ +/* exported init, cleanup, lock, unlock, get_animation_status, get_transparency_status, minimum_fade_in, update_transition_type */ +/* exported fade_in, fade_out, blank_fade_out */ + +const Mainloop = imports.mainloop; + +const St = imports.gi.St; + +const Me = imports.misc.extensionUtils.getCurrentExtension(); + +const Settings = Me.imports.settings; +const Theming = Me.imports.theming; + +const Equations = imports.tweener.equations; + +const CORNER_UPDATE_FREQUENCY = 30; + +class TransparencyStatus { + constructor() { + this.transparent = false; + this.blank = false; + } + + is_transparent() { + return this.transparent; + } + + is_blank() { + return this.blank; + } + + set_transparent(transparent) { + this.transparent = transparent; + } + + set_blank(blank) { + this.blank = blank; + } +}; + +/** + * Intialize. + * + */ +function init() { + /* Objects to track where the transparency is and where it's going. */ + this.status = new TransparencyStatus(); + + this.corner_timeout_id = 0; +} + +/** + * Freeup any held assets on disable. + * + */ +function cleanup() { + this.status = null; + + this.corner_timeout_id = null; +} + +/** + * Get the current status of the panel's transparency. + * + * @returns {Object} Current transparency. @see TransparencyStatus + */ +function get_transparency_status() { + return this.status; +} + +/** + * Get any animation that the panel is currently doing. + * DEPRECATED. + * + * @returns {Object} Current animation status. @see AnimationStatus + */ +function get_animation_status() { + return { destination: null, action: null }; +} + +/** + * Fades the panel into the unmaximized (minimum) alpha. Used for closing the overview. + * + */ +function minimum_fade_in() { + /* The CSS backend doesn't need different starting/ending values */ + fade_out(); +} + +/** + * Fades the panel into the nmaximized (maximum) alpha. + * + */ +function fade_in() { + if (!Settings.remove_panel_styling()) { + Theming.reapply_panel_styling(); + Theming.reapply_panel_background_image(); + } + + Theming.remove_panel_transparency(); + + if (Settings.enable_custom_background_color()) { + Theming.set_maximized_background_color('custom'); + + Theming.remove_unmaximized_background_color(); + Theming.remove_unmaximized_background_color('custom'); + } else { + Theming.set_maximized_background_color(); + + Theming.remove_unmaximized_background_color(); + Theming.remove_unmaximized_background_color('custom'); + } + + if (!Settings.get_hide_corners()) { + let speed = St.Settings.get().slow_down_factor * Settings.get_transition_speed(); + + let maximized = Settings.get_maximized_opacity(); + let unmaximized = !this.status.is_transparent() ? maximized : Settings.get_unmaximized_opacity(); + + this.status.set_transparent(false); + this.status.set_blank(false); + + let count = 0; + + const id = this.corner_timeout_id = Mainloop.timeout_add(Math.floor(speed / CORNER_UPDATE_FREQUENCY), (function() { + if (id === this.corner_timeout_id && !this.status.is_transparent()) { + count++; + + let alpha = Equations.linear(Math.floor(count * (speed / CORNER_UPDATE_FREQUENCY)), unmaximized, maximized - unmaximized, speed); + + update_corner_alpha(alpha); + + if (count > CORNER_UPDATE_FREQUENCY) { + update_corner_alpha(maximized); + return false; + } + } else { + return false; + } + + return true; + }).bind(this)); + } else { + update_corner_alpha(); + } + + this.status.set_transparent(false); + this.status.set_blank(false); +} + +/** + * Fades the panel into the unmaximized (minimum) alpha. + * + */ +function fade_out() { + Theming.strip_panel_background_image(); + Theming.strip_panel_styling(); + + if (Settings.enable_custom_background_color()) { + Theming.set_unmaximized_background_color('custom'); + + Theming.remove_maximized_background_color(); + Theming.remove_maximized_background_color('custom'); + } else { + Theming.set_unmaximized_background_color(); + + Theming.remove_maximized_background_color(); + Theming.remove_maximized_background_color('custom'); + } + + Theming.remove_panel_transparency(); + + // TODO: Figure out how to write the panel corners in pure CSS. + if (!Settings.get_hide_corners()) { + let speed = St.Settings.get().slow_down_factor * Settings.get_transition_speed(); + + let unmaximized = Settings.get_unmaximized_opacity(); + let maximized = this.status.is_transparent() ? unmaximized : Settings.get_maximized_opacity(); + + /* Keep the status up to date */ + this.status.set_transparent(true); + this.status.set_blank(false); + + let count = 0; + + const id = this.corner_timeout_id = Mainloop.timeout_add(Math.floor(speed / CORNER_UPDATE_FREQUENCY), (function() { + if (id === this.corner_timeout_id && this.status.is_transparent()) { + count++; + + let alpha = Equations.linear(Math.floor(count * (speed / CORNER_UPDATE_FREQUENCY)), maximized, unmaximized - maximized, speed); + + update_corner_alpha(alpha); + + if (count > CORNER_UPDATE_FREQUENCY) { + update_corner_alpha(unmaximized); + return false; + } + } else { + return false; + } + return true; + }).bind(this)); + } else { + update_corner_alpha(); + } + + /* Keep the status up to date */ + this.status.set_transparent(true); + this.status.set_blank(false); +} + +/** + * Fades the panel's alpha to 0. Used for opening the overview & displaying the screenShield. + * + */ +function blank_fade_out() { + this.status.set_transparent(true); + this.status.set_blank(true); + + /* Completely remove every possible background style... */ + Theming.remove_background_color(); + + Theming.strip_panel_background_image(); + Theming.strip_panel_styling(); + + Theming.apply_panel_transparency(); + + // TODO: These corners... + if (!Settings.get_hide_corners()) { + let speed = St.Settings.get().slow_down_factor * Settings.get_transition_speed(); + + let maximized = Settings.get_maximized_opacity(); + + let count = 0; + + const id = this.corner_timeout_id = Mainloop.timeout_add(Math.floor(speed / CORNER_UPDATE_FREQUENCY), (function() { + if (id === this.corner_timeout_id && this.status.is_transparent()) { + count++; + + let alpha = Equations.linear(Math.floor(count * (speed / CORNER_UPDATE_FREQUENCY)), maximized, -maximized, speed); + + update_corner_alpha(alpha); + + if (count > CORNER_UPDATE_FREQUENCY) { + update_corner_alpha(0); + return false; + } + } else { + return false; + } + return true; + }).bind(this)); + } else { + update_corner_alpha(); + } +} + +/** + * Updates the alpha value of the corners' coloring. Slightly awkward overlap is unavoidable. + * + * @param {Number} alpha - Alpha value ranging from 0-255. + */ +function update_corner_alpha(alpha = null) { + if (alpha === null) { + if (Settings.get_hide_corners()) { + alpha = 0; + } else { + alpha = this.status.is_transparent() ? Theming.get_unmaximized_opacity() : Theming.get_maximized_opacity(); + } + } + + Theming.set_corner_color({ + alpha: alpha + }); +} diff --git a/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/util.js b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/util.js new file mode 100644 index 0000000..3ab2d32 --- /dev/null +++ b/.local/share/gnome-shell/extensions/dynamic-panel-transparency@rockon999.github.io/util.js @@ -0,0 +1,240 @@ +/* exported get_shell_version, is_undef, clamp, is_valid, match_colors, remove_file, get_file, write_to_file, gdk_to_css_color, clutter_to_native_color, tuple_to_native_color, deep_freeze, strip_args */ + +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; + +/* This import can't be a constant as it requires lazy initialization. */ +let Meta = null; + +/* Gnome Versioning */ +const MAJOR_VERSION = parseInt(imports.misc.config.PACKAGE_VERSION.split('.')[0], 10); +const MINOR_VERSION = parseInt(imports.misc.config.PACKAGE_VERSION.split('.')[1], 10); + +/* Permission setting for created files. */ +const PERMISSIONS_MODE = parseInt('0744', 8); + +/* Utility Variable Access */ + +/** + * Returns the current shell version. + * + * @returns {Object} The current shell version. + * + */ +function get_shell_version() { + return { major: MAJOR_VERSION, minor: MINOR_VERSION }; +} + +/* Utility Functions */ + +/** + * Evaluates parameter 'value' and returns either 'value' or 'min'/'max' if 'value' is outside of the range. + * + * @param {Number} value - Test value. + * @param {Number} min - Minimum value. + * @param {Number} max - Maximum value. + * + * @returns {Number} 'value' or the minimum or maximum. + * + */ +function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); +} + +/** + * Determines if 'window' is a valid window to watch. + * Will not work outside the extension code. + * + * @param {Object} window - Window to check. + * + * @returns {Boolean} Whether 'window' is a valid window to watch. + * + */ +function is_valid(window) { + if (!Meta) { + Meta = imports.gi.Meta; + } + + let windowTypes = [ + Meta.WindowType.NORMAL, + Meta.WindowType.DIALOG, + Meta.WindowType.MODAL_DIALOG, + Meta.WindowType.TOOLBAR, + Meta.WindowType.MENU, + Meta.WindowType.UTILITY, + ]; + + let type = window.get_window_type(); + + return (windowTypes.indexOf(type) !== -1); +} + +/** + * Retrieves the GFile for a file path. + * + * @param {string} file_path - Path for a file. + * + * @returns {Object} GFile for the path or null if the path is not valid. + * + */ +function get_file(file_path) { + try { + return Gio.file_new_for_path(file_path); + } catch (error) { + log('[Dynamic Panel Transparency] Error getting file: ' + error); + } + + return null; +} + +/** + * Write the given string to a file path; creating the necessary files and directories. + * + * @param {string} file_path - Path for a file. + * @param {string} text - Text to write to the file. + * + * @returns {Boolean} Whether the file write was a success. + * + */ +function write_to_file(file_path, text) { + try { + let file = get_file(file_path); + let parent = file.get_parent(); + + if (GLib.mkdir_with_parents(parent.get_path(), PERMISSIONS_MODE) === 0) { + let success = file.replace_contents(text, null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null); + return success[0]; + } + } catch (error) { + log('[Dynamic Panel Transparency] Error writing to file: ' + file_path); + log(error); + } + + return false; +} + +/** + * Deletes the given file_path. + * + * @param {string} file_path - Path for a file. + * + * @returns {Boolean} Whether the file deletion was a success. + * + */ +function remove_file(file_path) { + try { + let file = get_file(file_path); + let result = file.delete(null); + return result; + } catch (error) { + log('[Dynamic Panel Transparency] Error removing file: ' + file_path); + log(error); + } + + return false; +} + +/** + * Converts a GdkColor into a JS/CSS color object. + * + * @param {Object} color - GdkColor to convert. + * + * @returns {Object} Converted RGB color. + * + */ +function gdk_to_css_color(color) { + let red = Math.round(clamp((color.red * 255), 0, 255)); + let green = Math.round(clamp((color.green * 255), 0, 255)); + let blue = Math.round(clamp((color.blue * 255), 0, 255)); + + return { 'red': red, 'green': green, 'blue': blue }; +} + +/** + * Converts a ClutterColor into a JS/CSS color object. + * + * @param {Object} color - ClutterColor to convert. + * @param {Boolean} [alpha = false] - Whether to transfer the alpha value. + * + * @returns {Object} Converted RGB(A) color. + * + */ +function clutter_to_native_color(color, alpha = false) { + let output = { red: color.red, green: color.green, blue: color.blue }; + if (alpha) { + output.alpha = color.alpha; + } + return output; +} + +/** + * Converts a tuple from a GVariant (typically) into a JS/CSS color object. + * + * @param {Object} color - Tuple to convert. + * @param {Boolean} [alpha = false] - Whether to transfer the alpha value. + * + * @returns {Object} Converted RGB(A) color. + * + */ +function tuple_to_native_color(tuple) { + let color = { red: tuple[0], green: tuple[1], blue: tuple[2] }; + if (tuple.length === 4) { + color.alpha = tuple[3]; + } + return color; +} + +/** + * Compares two colors for equivalency. + * + * @param {Object} a - First color. + * @param {Object} a - Second color. + * @param {Boolean} [alpha = false] - Whether to check the alpha value. + * + * @returns {Boolean} Whether the two colors are equal. + * + */ +function match_colors(a, b, alpha = false) { + let result = (a.red === b.red); + result = result && (a.green === b.green); + result = result && (a.blue === b.blue); + if (alpha) { + result = result && (a.alpha === b.alpha); + } + return result; +} + +/** + * Freezes an Object's and its children. + * + * @param {Object} type - Object to freeze. + * @param {Boolean} [recursive = false] - Whether to recursively traverse the object's children. + * + */ +function deep_freeze(type, recursive = false) { + const freeze_children = function(obj) { + Object.keys(obj).forEach(function(value, index, arr) { + if (typeof (value) === 'object' && !Object.isFrozen(value)) { + Object.freeze(value); + if (recursive) { + freeze_children(value); + } + } + }); + }; + + Object.freeze(type); + freeze_children(type); +} + +/** + * Prevents any arguments from passing on. + * + * @param {Object} method - Method to call. + * + */ +function strip_args(method) { + return function() { + method.call(this); + }; +} \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/gnome-shell-go-to-last-workspace@github.com/extension.js b/.local/share/gnome-shell/extensions/gnome-shell-go-to-last-workspace@github.com/extension.js new file mode 100644 index 0000000..d0c7407 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gnome-shell-go-to-last-workspace@github.com/extension.js @@ -0,0 +1,47 @@ +const Meta = imports.gi.Meta; +const Main = imports.ui.main; +const Shell = imports.gi.Shell; + +const SHORTCUT_KEY = 'shortcut-key'; + +let settings = imports.misc.extensionUtils.getSettings(); + +var currentWorkspace = -1; +var lastWorkspace = -1; + +function goToLastWorkspace() { + if (lastWorkspace < 0) { + return; + } + + // keep global.screen for backwards compatibility + let ws = (global.screen || global.workspace_manager).get_workspace_by_index(lastWorkspace); + ws.activate(global.get_current_time()); +} + + +function init() { +} + +let signals = []; + +function enable() { + var ModeType = Shell.hasOwnProperty('ActionMode') ? Shell.ActionMode : Shell.KeyBindingMode; + Main.wm.addKeybinding(SHORTCUT_KEY, settings, Meta.KeyBindingFlags.NONE, ModeType.NORMAL | ModeType.OVERVIEW, goToLastWorkspace); + + signals.push((global.screen || global.workspace_manager).connect('workspace-switched', function(display, prev, current, direction) { + lastWorkspace = currentWorkspace; + currentWorkspace = current; + })); +} + +function disable() { + // clean up + Main.wm.removeKeybinding(SHORTCUT_KEY); + + let i = signals.length; + while (i--) { + (global.screen || global.workspace_manager).disconnect(signals.pop()); + } + +} diff --git a/.local/share/gnome-shell/extensions/gnome-shell-go-to-last-workspace@github.com/metadata.json b/.local/share/gnome-shell/extensions/gnome-shell-go-to-last-workspace@github.com/metadata.json new file mode 100644 index 0000000..949a676 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gnome-shell-go-to-last-workspace@github.com/metadata.json @@ -0,0 +1,18 @@ +{ + "_generated": "Generated by SweetTooth, do not edit", + "description": "Quickly toggle between two workspaces with one key", + "name": "Go To Last Workspace", + "settings-schema": "org.gnome.shell.extensions.go-to-last-workspace", + "shell-version": [ + "3.28", + "3.30", + "3.34", + "3.32", + "3.36", + "3.38", + "40" + ], + "url": "https://github.com/arjan/gnome-shell-go-to-last-workspace", + "uuid": "gnome-shell-go-to-last-workspace@github.com", + "version": 7 +} \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/gnome-shell-go-to-last-workspace@github.com/prefs.js b/.local/share/gnome-shell/extensions/gnome-shell-go-to-last-workspace@github.com/prefs.js new file mode 100644 index 0000000..b13f191 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gnome-shell-go-to-last-workspace@github.com/prefs.js @@ -0,0 +1,82 @@ +// Library imports +const GObject = imports.gi.GObject; +const Gdk = imports.gi.Gdk; +const Gtk = imports.gi.Gtk; +const ExtensionUtils = imports.misc.extensionUtils; + +// Globals +const pretty_names = { + "shortcut-key": "Go to last workspace", +}; + +function init() {} + +function buildPrefsWidget() { + let model = new Gtk.ListStore(); + + model.set_column_types([ + GObject.TYPE_STRING, + GObject.TYPE_STRING, + GObject.TYPE_INT, + GObject.TYPE_INT, + ]); + + let settings = ExtensionUtils.getSettings(); + + for (key in pretty_names) { + append_hotkey(model, settings, key, pretty_names[key]); + } + + let treeview = new Gtk.TreeView({ + model: model, + }); + + let col; + let cellrend; + cellrend = new Gtk.CellRendererText(); + col = new Gtk.TreeViewColumn({ + title: "Keybinding", + }); + col.pack_start(cellrend, true); + col.add_attribute(cellrend, "text", 1); + treeview.append_column(col); + + cellrend = new Gtk.CellRendererAccel({ + editable: true, + "accel-mode": Gtk.CellRendererAccelMode.GTK, + }); + cellrend.connect("accel-edited", function (rend, iter, key, mods) { + let value = Gtk.accelerator_name(key, mods); + log("change"); + log(value); + log(key); + log(mods); + let [success, iter1] = model.get_iter_from_string(iter); + if (!success) { + throw new Error("Something be broken, yo."); + } + let name = model.get_value(iter1, 0); + model.set(iter1, [2, 3], [mods, key]); + settings.set_strv(name, [value]); + }); + + col = new Gtk.TreeViewColumn({ + title: "Accel", + }); + + col.pack_end(cellrend, false); + col.add_attribute(cellrend, "accel-mods", 2); + col.add_attribute(cellrend, "accel-key", 3); + treeview.append_column(col); + treeview.expand_all(); + + return treeview; +} + +function append_hotkey(model, settings, name, pretty_name) { + let [_something, key, mods] = Gtk.accelerator_parse( + settings.get_strv(name)[0] + ); + let row = model.insert(-1); + model.set(row, [0, 1, 2, 3], [name, pretty_name, mods, key]); +} diff --git a/.local/share/gnome-shell/extensions/gnome-shell-go-to-last-workspace@github.com/schemas/gschemas.compiled b/.local/share/gnome-shell/extensions/gnome-shell-go-to-last-workspace@github.com/schemas/gschemas.compiled new file mode 100644 index 0000000000000000000000000000000000000000..e2f9af069ca7a99c05deb18278bd546fd4bc6941 GIT binary patch literal 332 zcmZ<{ODxJv%qwAl0tp};0by$}LB$w>G@GAcNRGxPI`_0scoOY(Jd5{pZ8%kzt}iwhEy zQ$dAMU0LGmCr4^Z3yNV@VV{@2314D6fMt)IAa%qWfc50 + + + + escape']]]> + Keybinding switch to last workspace + + The keybinding used to switch to the last used workspace. + + + + diff --git a/.local/share/gnome-shell/extensions/gnome-shell-go-to-last-workspace@github.com/utils.js b/.local/share/gnome-shell/extensions/gnome-shell-go-to-last-workspace@github.com/utils.js new file mode 100644 index 0000000..bff9293 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gnome-shell-go-to-last-workspace@github.com/utils.js @@ -0,0 +1,46 @@ +const Gio = imports.gi.Gio; +const Config = imports.misc.config; +const ExtensionUtils = imports.misc.extensionUtils; + +/** + * getSettings: + * @schema: (optional): the GSettings schema id + * + * Builds and return a GSettings schema for @schema, using schema files + * in extensionsdir/schemas. If @schema is not provided, it is taken from + * metadata['settings-schema']. + */ +function getSettings(schema) { + let extension = ExtensionUtils.getCurrentExtension(); + + schema = schema || extension.metadata["settings-schema"]; + + const GioSSS = Gio.SettingsSchemaSource; + + // check if this extension was built with "make zip-file", and thus + // has the schema files in a subfolder + // otherwise assume that extension has been installed in the + // same prefix as gnome-shell (and therefore schemas are available + // in the standard folders) + let schemaDir = extension.dir.get_child("schemas"); + let schemaSource; + if (schemaDir.query_exists(null)) + schemaSource = GioSSS.new_from_directory( + schemaDir.get_path(), + GioSSS.get_default(), + false + ); + else schemaSource = GioSSS.get_default(); + + let schemaObj = schemaSource.lookup(schema, true); + if (!schemaObj) + throw new Error( + "Schema " + + schema + + " could not be found for extension " + + extension.metadata.uuid + + ". Please check your installation." + ); + + return new Gio.Settings({ settings_schema: schemaObj }); +} diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/config.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/config.js new file mode 100644 index 0000000..f9d5690 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/config.js @@ -0,0 +1,19 @@ +var PACKAGE_VERSION = 47; +var PACKAGE_URL = 'https://github.com/andyholmes/gnome-shell-extension-gsconnect'; +var PACKAGE_BUGREPORT = 'https://github.com/andyholmes/gnome-shell-extension-gsconnect/issues/new'; +var PACKAGE_DATADIR = '/usr/local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io'; +var PACKAGE_LOCALEDIR = '/usr/local/share/locale'; +var GSETTINGS_SCHEMA_DIR = '/usr/local/share/glib-2.0/schemas'; +var GNOME_SHELL_LIBDIR = '/usr/local/lib64'; + +var APP_ID = 'org.gnome.Shell.Extensions.GSConnect'; +var APP_PATH = '/org/gnome/Shell/Extensions/GSConnect'; + +var IS_USER = false; + +// External binary paths +var OPENSSL_PATH = 'openssl'; +var SSHADD_PATH = 'ssh-add'; +var SSHKEYGEN_PATH = 'ssh-keygen'; +var FFMPEG_PATH = 'ffmpeg'; + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/extension.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/extension.js new file mode 100644 index 0000000..abe14e0 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/extension.js @@ -0,0 +1,455 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + +const Main = imports.ui.main; +const PanelMenu = imports.ui.panelMenu; +const PopupMenu = imports.ui.popupMenu; +const AggregateMenu = Main.panel.statusArea.aggregateMenu; + +// Bootstrap +const Extension = imports.misc.extensionUtils.getCurrentExtension(); +const Utils = Extension.imports.shell.utils; + +// eslint-disable-next-line no-redeclare +const _ = Extension._; +const Clipboard = Extension.imports.shell.clipboard; +const Config = Extension.imports.config; +const Device = Extension.imports.shell.device; +const Keybindings = Extension.imports.shell.keybindings; +const Notification = Extension.imports.shell.notification; +const Remote = Extension.imports.utils.remote; + +Extension.getIcon = Utils.getIcon; + + +/** + * A System Indicator used as the hub for spawning device indicators and + * indicating that the extension is active when there are none. + */ +const ServiceIndicator = GObject.registerClass({ + GTypeName: 'GSConnectServiceIndicator', +}, class ServiceIndicator extends PanelMenu.SystemIndicator { + + _init() { + super._init(); + + this._menus = {}; + + this._keybindings = new Keybindings.Manager(); + + // GSettings + this.settings = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup( + 'org.gnome.Shell.Extensions.GSConnect', + null + ), + path: '/org/gnome/shell/extensions/gsconnect/', + }); + + this._enabledId = this.settings.connect( + 'changed::enabled', + this._onEnabledChanged.bind(this) + ); + + this._panelModeId = this.settings.connect( + 'changed::show-indicators', + this._sync.bind(this) + ); + + // Service Proxy + this.service = new Remote.Service(); + + this._deviceAddedId = this.service.connect( + 'device-added', + this._onDeviceAdded.bind(this) + ); + + this._deviceRemovedId = this.service.connect( + 'device-removed', + this._onDeviceRemoved.bind(this) + ); + + this._serviceChangedId = this.service.connect( + 'notify::active', + this._onServiceChanged.bind(this) + ); + + // Service Indicator + this._indicator = this._addIndicator(); + this._indicator.gicon = Extension.getIcon( + 'org.gnome.Shell.Extensions.GSConnect-symbolic' + ); + this._indicator.visible = false; + + AggregateMenu._indicators.insert_child_at_index(this, 0); + AggregateMenu._gsconnect = this; + + // Service Menu + this._item = new PopupMenu.PopupSubMenuMenuItem(_('Mobile Devices'), true); + this._item.icon.gicon = this._indicator.gicon; + this._item.label.clutter_text.x_expand = true; + this.menu.addMenuItem(this._item); + + // Find current index of network menu + const menuItems = AggregateMenu.menu._getMenuItems(); + const networkMenuIndex = menuItems.indexOf(AggregateMenu._network.menu); + const menuIndex = networkMenuIndex > -1 ? networkMenuIndex : 3; + // Place our menu below the network menu + AggregateMenu.menu.addMenuItem(this.menu, menuIndex + 1); + + // Service Menu -> Devices Section + this.deviceSection = new PopupMenu.PopupMenuSection(); + this.deviceSection.actor.add_style_class_name('gsconnect-device-section'); + this.settings.bind( + 'show-indicators', + this.deviceSection.actor, + 'visible', + Gio.SettingsBindFlags.INVERT_BOOLEAN + ); + this._item.menu.addMenuItem(this.deviceSection); + + // Service Menu -> Separator + this._item.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + + // Service Menu -> "Turn On/Off" + this._enableItem = this._item.menu.addAction( + _('Turn On'), + this._enable.bind(this) + ); + + // Service Menu -> "Mobile Settings" + this._item.menu.addAction(_('Mobile Settings'), this._preferences); + + // Prime the service + this._initService(); + } + + async _initService() { + try { + if (this.settings.get_boolean('enabled')) + await this.service.start(); + else + await this.service.reload(); + } catch (e) { + logError(e, 'GSConnect'); + } + } + + _enable() { + try { + const enabled = this.settings.get_boolean('enabled'); + + // If the service state matches the enabled setting, we should + // toggle the service by toggling the setting + if (this.service.active === enabled) + this.settings.set_boolean('enabled', !enabled); + + // Otherwise, we should change the service to match the setting + else if (this.service.active) + this.service.stop(); + else + this.service.start(); + } catch (e) { + logError(e, 'GSConnect'); + } + } + + _preferences() { + Gio.Subprocess.new([`${Extension.path}/gsconnect-preferences`], 0); + } + + _sync() { + const available = this.service.devices.filter(device => { + return (device.connected && device.paired); + }); + const panelMode = this.settings.get_boolean('show-indicators'); + + // Hide status indicator if in Panel mode or no devices are available + this._indicator.visible = (!panelMode && available.length); + + // Show device indicators in Panel mode if available + for (const device of this.service.devices) { + const isAvailable = available.includes(device); + const indicator = Main.panel.statusArea[device.g_object_path]; + + indicator.visible = panelMode && isAvailable; + + const menu = this._menus[device.g_object_path]; + menu.actor.visible = !panelMode && isAvailable; + menu._title.actor.visible = !panelMode && isAvailable; + } + + // One connected device in User Menu mode + if (!panelMode && available.length === 1) { + const device = available[0]; + + // Hide the menu title and move it to the submenu item + this._menus[device.g_object_path]._title.actor.visible = false; + this._item.label.text = device.name; + + // Destroy any other device's signalStrength + if (this._item._signalStrength && this._item._signalStrength.device !== device) { + this._item._signalStrength.destroy(); + this._item._signalStrength = null; + } + + // Add the signalStrength to the submenu item + if (!this._item._signalStrength) { + this._item._signalStrength = new Device.SignalStrength({ + device: device, + opacity: 128, + }); + this._item.actor.insert_child_below( + this._item._signalStrength, + this._item._triangleBin + ); + } + + // Destroy any other device's battery + if (this._item._battery && this._item._battery.device !== device) { + this._item._battery.destroy(); + this._item._battery = null; + } + + // Add the battery to the submenu item + if (!this._item._battery) { + this._item._battery = new Device.Battery({ + device: device, + opacity: 128, + }); + this._item.actor.insert_child_below( + this._item._battery, + this._item._triangleBin + ); + } + } else { + if (available.length > 1) { + // TRANSLATORS: %d is the number of devices connected + this._item.label.text = Extension.ngettext( + '%d Connected', + '%d Connected', + available.length + ).format(available.length); + } else { + this._item.label.text = _('Mobile Devices'); + } + + // Destroy any battery in the submenu item + if (this._item._battery) { + this._item._battery.destroy(); + this._item._battery = null; + } + + // Destroy any signalStrength in the submenu item + if (this._item._signalStrength) { + this._item._signalStrength.destroy(); + this._item._signalStrength = null; + } + } + } + + _onDeviceChanged(device, changed, invalidated) { + try { + const properties = changed.deepUnpack(); + + if (properties.hasOwnProperty('Connected') || + properties.hasOwnProperty('Paired')) + this._sync(); + } catch (e) { + logError(e, 'GSConnect'); + } + } + + _onDeviceAdded(service, device) { + try { + // Device Indicator + const indicator = new Device.Indicator({device: device}); + Main.panel.addToStatusArea(device.g_object_path, indicator); + + // Device Menu + const menu = new Device.Menu({ + device: device, + menu_type: 'list', + }); + this._menus[device.g_object_path] = menu; + this.deviceSection.addMenuItem(menu); + + // Device Settings + device.settings = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup( + 'org.gnome.Shell.Extensions.GSConnect.Device', + true + ), + path: `/org/gnome/shell/extensions/gsconnect/device/${device.id}/`, + }); + + // Keyboard Shortcuts + device.__keybindingsChangedId = device.settings.connect( + 'changed::keybindings', + this._onDeviceKeybindingsChanged.bind(this, device) + ); + this._onDeviceKeybindingsChanged(device); + + // Watch the for status changes + device.__deviceChangedId = device.connect( + 'g-properties-changed', + this._onDeviceChanged.bind(this) + ); + + this._sync(); + } catch (e) { + logError(e, 'GSConnect'); + } + } + + _onDeviceRemoved(service, device, sync = true) { + try { + // Stop watching for status changes + if (device.__deviceChangedId) + device.disconnect(device.__deviceChangedId); + + // Release keybindings + if (device.__keybindingsChangedId) { + device.settings.disconnect(device.__keybindingsChangedId); + device._keybindings.map(id => this._keybindings.remove(id)); + } + + // Destroy the indicator + Main.panel.statusArea[device.g_object_path].destroy(); + + // Destroy the menu + this._menus[device.g_object_path].destroy(); + delete this._menus[device.g_object_path]; + + if (sync) + this._sync(); + } catch (e) { + logError(e, 'GSConnect'); + } + } + + _onDeviceKeybindingsChanged(device) { + try { + // Reset any existing keybindings + if (device.hasOwnProperty('_keybindings')) + device._keybindings.map(id => this._keybindings.remove(id)); + + device._keybindings = []; + + // Get the keybindings + const keybindings = device.settings.get_value('keybindings').deepUnpack(); + + // Apply the keybindings + for (const [action, accelerator] of Object.entries(keybindings)) { + const [, name, parameter] = Gio.Action.parse_detailed_name(action); + + const actionId = this._keybindings.add( + accelerator, + () => device.action_group.activate_action(name, parameter) + ); + + if (actionId !== 0) + device._keybindings.push(actionId); + } + } catch (e) { + logError(e, 'GSConnect'); + } + } + + async _onEnabledChanged(settings, key) { + try { + if (this.settings.get_boolean('enabled')) + await this.service.start(); + else + await this.service.stop(); + } catch (e) { + logError(e, 'GSConnect'); + } + } + + async _onServiceChanged(service, pspec) { + try { + if (this.service.active) { + // TRANSLATORS: A menu option to deactivate the extension + this._enableItem.label.text = _('Turn Off'); + } else { + // TRANSLATORS: A menu option to activate the extension + this._enableItem.label.text = _('Turn On'); + + // If it's enabled, we should try to restart now + if (this.settings.get_boolean('enabled')) + await this.service.start(); + } + } catch (e) { + logError(e, 'GSConnect'); + } + } + + destroy() { + // Unhook from Remote.Service + if (this.service) { + this.service.disconnect(this._serviceChangedId); + this.service.disconnect(this._deviceAddedId); + this.service.disconnect(this._deviceRemovedId); + + for (const device of this.service.devices) + this._onDeviceRemoved(this.service, device, false); + + this.service.destroy(); + } + + // Disconnect any keybindings + this._keybindings.destroy(); + + // Disconnect from any GSettings changes + this.settings.disconnect(this._enabledId); + this.settings.disconnect(this._panelModeId); + this.settings.run_dispose(); + + // Destroy the PanelMenu.SystemIndicator actors + this._item.destroy(); + this.menu.destroy(); + + delete AggregateMenu._gsconnect; + super.destroy(); + } +}); + + +var serviceIndicator = null; + + +function init() { + // If installed as a user extension, this will install the Desktop entry, + // DBus and systemd service files necessary for DBus activation and + // GNotifications. Since there's no uninit()/uninstall() hook for extensions + // and they're only used *by* GSConnect, they should be okay to leave. + Utils.installService(); + + // These modify the notification source for GSConnect's GNotifications and + // need to be active even when the extension is disabled (eg. lock screen). + // Since they *only* affect notifications from GSConnect, it should be okay + // to leave them applied. + Notification.patchGSConnectNotificationSource(); + Notification.patchGtkNotificationDaemon(); + + // This watches for the service to start and exports a custom clipboard + // portal for use on Wayland + Clipboard.watchService(); +} + + +function enable() { + serviceIndicator = new ServiceIndicator(); + Notification.patchGtkNotificationSources(); +} + + +function disable() { + serviceIndicator.destroy(); + serviceIndicator = null; + Notification.unpatchGtkNotificationSources(); +} diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/gsconnect-preferences b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/gsconnect-preferences new file mode 100755 index 0000000..f66c742 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/gsconnect-preferences @@ -0,0 +1,114 @@ +#!/usr/bin/env gjs + +// -*- mode: js; -*- + +'use strict'; + +imports.gi.versions.Gdk = '3.0'; +imports.gi.versions.GdkPixbuf = '2.0'; +imports.gi.versions.Gio = '2.0'; +imports.gi.versions.GLib = '2.0'; +imports.gi.versions.GObject = '2.0'; +imports.gi.versions.Gtk = '3.0'; + +const Gdk = imports.gi.Gdk; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + + +// Bootstrap +function get_datadir() { + let m = /@(.+):\d+/.exec((new Error()).stack.split('\n')[1]); + return Gio.File.new_for_path(m[1]).get_parent().get_path(); +} + +imports.searchPath.unshift(get_datadir()); +imports.config.PACKAGE_DATADIR = imports.searchPath[0]; + + +// Local Imports +const Config = imports.config; +const Settings = imports.preferences.service; + + +/** + * Class representing the GSConnect service daemon. + */ +const Preferences = GObject.registerClass({ + GTypeName: 'GSConnectPreferences', +}, class Preferences extends Gtk.Application { + + _init() { + super._init({ + application_id: 'org.gnome.Shell.Extensions.GSConnect.Preferences', + resource_base_path: '/org/gnome/Shell/Extensions/GSConnect', + }); + + GLib.set_prgname('gsconnect-preferences'); + GLib.set_application_name(_('GSConnect Preferences')); + } + + vfunc_activate() { + if (this._window === undefined) { + this._window = new Settings.Window({ + application: this, + }); + } + + this._window.present(); + } + + vfunc_startup() { + super.vfunc_startup(); + + // Init some resources + let provider = new Gtk.CssProvider(); + provider.load_from_resource(`${Config.APP_PATH}/application.css`); + Gtk.StyleContext.add_provider_for_screen( + Gdk.Screen.get_default(), + provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ); + + let actions = [ + ['refresh', null], + ['connect', GLib.VariantType.new('s')], + ]; + + for (let [name, type] of actions) { + let action = new Gio.SimpleAction({ + name: name, + parameter_type: type, + }); + this.add_action(action); + } + } + + vfunc_activate_action(action_name, parameter) { + try { + let paramArray = []; + + if (parameter instanceof GLib.Variant) + paramArray[0] = parameter; + + this.get_dbus_connection().call( + 'org.gnome.Shell.Extensions.GSConnect', + '/org/gnome/Shell/Extensions/GSConnect', + 'org.freedesktop.Application', + 'ActivateAction', + GLib.Variant.new('(sava{sv})', [action_name, paramArray, {}]), + null, + Gio.DBusCallFlags.NONE, + -1, + null, + null + ); + } catch (e) { + logError(e); + } + } +}); + +(new Preferences()).run([imports.system.programInvocationName].concat(ARGV)); diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/ar/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/ar/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..0f4ae6321c87246b6aafe921f420211522a66261 GIT binary patch literal 4545 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/cs/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/cs/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..9682ec9e78493de7c031824e852923df9507d1c7 GIT binary patch literal 15539 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/da/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/da/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..3744b63fe60614b1885862c728f08074af9d9c00 GIT binary patch literal 12683 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 E3!HCUivR!s literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/es/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/es/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..d67cd51bd480840d1b736f3c93049042d3e93fa5 GIT binary patch literal 17948 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/fi/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/fi/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..a0b4e949913618b38cd1f9b5c9dedc7419f75697 GIT binary patch literal 14635 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/fr/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/fr/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..5176b23313e1d2b8bde367b1c9ca7869eb3fe588 GIT binary patch literal 15657 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/hu/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/hu/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..a89de62741b8ed99e88836a34c7f39f410d58306 GIT binary patch literal 12811 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 C7I*Lf literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/it/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/it/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..74b66b0feae2e145166732b482f4aa7c4599db2b GIT binary patch literal 15065 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 A$N&HU literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/lt/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/lt/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..34f6f8e469a306d308d6ba8b2c15c0008a3c7138 GIT binary patch literal 15723 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 JHJ+xy{{jyTYCiw~ literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/nl_BE/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/nl_BE/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..af70536b7d94744528b8abd5f3833e80c2c73d07 GIT binary patch literal 7966 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 X9r@yeNy8i_)l=$XE7Jb5OwjxPK_2Bfcm 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 z XBi2|Bcw-~K9c7+-4t{%GM0)=V^{`Ul literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/pl/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/pl/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..f6a677d3d6ffad1c38e0e456f182aa2c95759893 GIT binary patch literal 16200 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/pt_BR/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/pt_BR/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..c0cb2ed31c4c616fe616db8fa35e83a75de26b39 GIT binary patch literal 15340 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/sr/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/sr/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..5400f0a785d8b5d0946a79e4d1b17fa39863a570 GIT binary patch literal 12495 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 W&)GE`xNJj>|ICQ``P1QY@BaV*Ge(>M literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/tr/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/tr/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..a86379129bfdc4d9004e9cf432f01fe6c6f2e5fc GIT binary patch literal 14874 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 A3;+NC literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/uk/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/uk/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..9ad5b6f17b20dffe07c6d2d636f7828233ed35d6 GIT binary patch literal 16559 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 Q`O+Z&$6&s=bXh+7AM`U~8UO$Q literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/zh_CN/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/zh_CN/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..6aea16b529bcf8e9a2aeeef8acff853f1d03a8e4 GIT binary patch literal 14101 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 S=Ep}0Lpuv+j^?)xn*RmVoNSl? literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/zh_TW/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/locale/zh_TW/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..10f2db4b90375c63fe5c51d7176d72b20b84e8d5 GIT binary patch literal 11004 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/preferences/__init__.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/preferences/__init__.js new file mode 100644 index 0000000..355df3e --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/preferences/__init__.js @@ -0,0 +1,41 @@ +'use strict'; + +const Gettext = imports.gettext; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; + +const Config = imports.config; + + +// Ensure config.js is setup properly +const userDir = GLib.build_filenamev([GLib.get_user_data_dir(), 'gnome-shell']); + +if (Config.PACKAGE_DATADIR.startsWith(userDir)) { + Config.IS_USER = true; + + Config.PACKAGE_LOCALEDIR = `${Config.PACKAGE_DATADIR}/locale`; + Config.GSETTINGS_SCHEMA_DIR = `${Config.PACKAGE_DATADIR}/schemas`; +} + + +// Init Gettext +String.prototype.format = imports.format.format; +Gettext.bindtextdomain(Config.APP_ID, Config.PACKAGE_LOCALEDIR); +globalThis._ = GLib.dgettext.bind(null, Config.APP_ID); +globalThis.ngettext = GLib.dngettext.bind(null, Config.APP_ID); + + +// Init GResources +Gio.Resource.load( + GLib.build_filenamev([Config.PACKAGE_DATADIR, `${Config.APP_ID}.gresource`]) +)._register(); + + +// Init GSchema +Config.GSCHEMA = Gio.SettingsSchemaSource.new_from_directory( + Config.GSETTINGS_SCHEMA_DIR, + Gio.SettingsSchemaSource.get_default(), + false +); + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/preferences/device.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/preferences/device.js new file mode 100644 index 0000000..e647907 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/preferences/device.js @@ -0,0 +1,1108 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; +const Pango = imports.gi.Pango; + +const Config = imports.config; +const Keybindings = imports.preferences.keybindings; + + +// Build a list of plugins and shortcuts for devices +const DEVICE_PLUGINS = []; +const DEVICE_SHORTCUTS = {}; + +for (const name in imports.service.plugins) { + const module = imports.service.plugins[name]; + + // Plugins + DEVICE_PLUGINS.push(name); + + // Shortcuts (GActions without parameters) + for (const [name, action] of Object.entries(module.Metadata.actions)) { + if (action.parameter_type === null) + DEVICE_SHORTCUTS[name] = [action.icon_name, action.label]; + } +} + + +/** + * A Gtk.ListBoxHeaderFunc for sections that adds separators between each row. + * + * @param {Gtk.ListBoxRow} row - The current row + * @param {Gtk.ListBoxRow} before - The previous row + */ +function rowSeparators(row, before) { + const header = row.get_header(); + + if (before === null) { + if (header !== null) + header.destroy(); + + return; + } + + if (header === null) + row.set_header(new Gtk.Separator({visible: true})); +} + + +/** + * A Gtk.ListBoxSortFunc for SectionRow rows + * + * @param {Gtk.ListBoxRow} row1 - The first row + * @param {Gtk.ListBoxRow} row2 - The second row + * @return {number} -1, 0 or 1 + */ +function titleSortFunc(row1, row2) { + if (!row1.title || !row2.title) + return 0; + + return row1.title.localeCompare(row2.title); +} + + +/** + * A row for a section of settings + */ +const SectionRow = GObject.registerClass({ + GTypeName: 'GSConnectPreferencesSectionRow', + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/preferences-section-row.ui', + Children: ['icon-image', 'title-label', 'subtitle-label'], + Properties: { + 'gicon': GObject.ParamSpec.object( + 'gicon', + 'GIcon', + 'A GIcon for the row', + GObject.ParamFlags.READWRITE, + Gio.Icon.$gtype + ), + 'icon-name': GObject.ParamSpec.string( + 'icon-name', + 'Icon Name', + 'An icon name for the row', + GObject.ParamFlags.READWRITE, + null + ), + 'subtitle': GObject.ParamSpec.string( + 'subtitle', + 'Subtitle', + 'A subtitle for the row', + GObject.ParamFlags.READWRITE, + null + ), + 'title': GObject.ParamSpec.string( + 'title', + 'Title', + 'A title for the row', + GObject.ParamFlags.READWRITE, + null + ), + 'widget': GObject.ParamSpec.object( + 'widget', + 'Widget', + 'An action widget for the row', + GObject.ParamFlags.READWRITE, + Gtk.Widget.$gtype + ), + }, +}, class SectionRow extends Gtk.ListBoxRow { + + _init(params = {}) { + super._init(); + + // NOTE: we can't pass construct properties to _init() because the + // template children are not assigned until after it runs. + this.freeze_notify(); + Object.assign(this, params); + this.thaw_notify(); + } + + get icon_name() { + return this.icon_image.icon_name; + } + + set icon_name(icon_name) { + if (this.icon_name === icon_name) + return; + + this.icon_image.visible = !!icon_name; + this.icon_image.icon_name = icon_name; + this.notify('icon-name'); + } + + get gicon() { + return this.icon_image.gicon; + } + + set gicon(gicon) { + if (this.gicon === gicon) + return; + + this.icon_image.visible = !!gicon; + this.icon_image.gicon = gicon; + this.notify('gicon'); + } + + get title() { + return this.title_label.label; + } + + set title(text) { + if (this.title === text) + return; + + this.title_label.visible = !!text; + this.title_label.label = text; + this.notify('title'); + } + + get subtitle() { + return this.subtitle_label.label; + } + + set subtitle(text) { + if (this.subtitle === text) + return; + + this.subtitle_label.visible = !!text; + this.subtitle_label.label = text; + this.notify('subtitle'); + } + + get widget() { + if (this._widget === undefined) + this._widget = null; + + return this._widget; + } + + set widget(widget) { + if (this.widget === widget) + return; + + if (this.widget instanceof Gtk.Widget) + this.widget.destroy(); + + // Add the widget + this._widget = widget; + this.get_child().attach(widget, 2, 0, 1, 2); + this.notify('widget'); + } +}); + + +/** + * Command Editor Dialog + */ +const CommandEditor = GObject.registerClass({ + GTypeName: 'GSConnectPreferencesCommandEditor', + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/preferences-command-editor.ui', + Children: [ + 'cancel-button', 'save-button', + 'command-entry', 'name-entry', 'command-chooser', + ], +}, class CommandEditor extends Gtk.Dialog { + + _onBrowseCommand(entry, icon_pos, event) { + this.command_chooser.present(); + } + + _onCommandChosen(dialog, response_id) { + if (response_id === Gtk.ResponseType.OK) + this.command_entry.text = dialog.get_filename(); + + dialog.hide(); + } + + _onEntryChanged(entry, pspec) { + this.save_button.sensitive = (this.command_name && this.command_line); + } + + get command_line() { + return this.command_entry.text; + } + + set command_line(text) { + this.command_entry.text = text; + } + + get command_name() { + return this.name_entry.text; + } + + set command_name(text) { + this.name_entry.text = text; + } +}); + + +/** + * A widget for configuring a remote device. + */ +var Panel = GObject.registerClass({ + GTypeName: 'GSConnectPreferencesDevicePanel', + Properties: { + 'device': GObject.ParamSpec.object( + 'device', + 'Device', + 'The device being configured', + GObject.ParamFlags.READWRITE, + GObject.Object.$gtype + ), + }, + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/preferences-device-panel.ui', + Children: [ + 'sidebar', 'stack', 'infobar', + + // Sharing + 'sharing', 'sharing-page', + 'desktop-list', 'clipboard', 'clipboard-sync', 'mousepad', 'mpris', 'systemvolume', + 'share', 'share-list', 'receive-files', 'receive-directory', + + // Battery + 'battery', + 'battery-device-label', 'battery-device', 'battery-device-list', + 'battery-system-label', 'battery-system', 'battery-system-list', + 'battery-custom-notification-value', + + // RunCommand + 'runcommand', 'runcommand-page', + 'command-list', 'command-add', + + // Notifications + 'notification', 'notification-page', + 'notification-list', 'notification-apps', + + // Telephony + 'telephony', 'telephony-page', + 'ringing-list', 'ringing-volume', 'talking-list', 'talking-volume', + + // Shortcuts + 'shortcuts-page', + 'shortcuts-actions', 'shortcuts-actions-title', 'shortcuts-actions-list', + + // Advanced + 'advanced-page', + 'plugin-list', 'experimental-list', + + 'device-menu', + ], +}, class Panel extends Gtk.Grid { + + _init(device) { + super._init({ + device: device, + }); + + // GSettings + this.settings = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup( + 'org.gnome.Shell.Extensions.GSConnect.Device', + true + ), + path: `/org/gnome/shell/extensions/gsconnect/device/${device.id}/`, + }); + + // Infobar + this.device.bind_property( + 'paired', + this.infobar, + 'reveal-child', + (GObject.BindingFlags.SYNC_CREATE | + GObject.BindingFlags.INVERT_BOOLEAN) + ); + + this._setupActions(); + + // Settings Pages + this._sharingSettings(); + this._batterySettings(); + this._runcommandSettings(); + this._notificationSettings(); + this._telephonySettings(); + // -------------------------- + this._keybindingSettings(); + this._advancedSettings(); + + // Separate plugins and other settings + this.sidebar.set_header_func((row, before) => { + if (row.get_name() === 'shortcuts') + row.set_header(new Gtk.Separator({visible: true})); + }); + } + + get menu() { + if (this._menu === undefined) { + this._menu = this.device_menu; + this._menu.prepend_section(null, this.device.menu); + this.insert_action_group('device', this.device.action_group); + } + + return this._menu; + } + + get_incoming_supported(type) { + const incoming = this.settings.get_strv('incoming-capabilities'); + return incoming.includes(`kdeconnect.${type}`); + } + + get_outgoing_supported(type) { + const outgoing = this.settings.get_strv('outgoing-capabilities'); + return outgoing.includes(`kdeconnect.${type}`); + } + + _onKeynavFailed(widget, direction) { + if (direction === Gtk.DirectionType.UP && widget.prev) + widget.prev.child_focus(direction); + + else if (direction === Gtk.DirectionType.DOWN && widget.next) + widget.next.child_focus(direction); + + return true; + } + + _onSwitcherRowSelected(box, row) { + this.stack.set_visible_child_name(row.get_name()); + } + + _onSectionRowActivated(box, row) { + if (row.widget !== undefined) + row.widget.active = !row.widget.active; + } + + _onToggleRowActivated(box, row) { + const widget = row.get_child().get_child_at(1, 0); + widget.active = !widget.active; + } + + _onEncryptionInfo() { + const dialog = new Gtk.MessageDialog({ + buttons: Gtk.ButtonsType.OK, + text: _('Encryption Info'), + secondary_text: this.device.encryption_info, + modal: true, + transient_for: this.get_toplevel(), + }); + dialog.connect('response', (dialog) => dialog.destroy()); + dialog.present(); + } + + _deviceAction(action, parameter) { + this.action_group.activate_action(action.name, parameter); + } + + dispose() { + if (this._commandEditor !== undefined) + this._commandEditor.destroy(); + + // Device signals + this.device.action_group.disconnect(this._actionAddedId); + this.device.action_group.disconnect(this._actionRemovedId); + + // GSettings + for (const settings of Object.values(this._pluginSettings)) + settings.run_dispose(); + + this.settings.disconnect(this._keybindingsId); + this.settings.disconnect(this._disabledPluginsId); + this.settings.disconnect(this._supportedPluginsId); + this.settings.run_dispose(); + } + + pluginSettings(name) { + if (this._pluginSettings === undefined) + this._pluginSettings = {}; + + if (!this._pluginSettings.hasOwnProperty(name)) { + const meta = imports.service.plugins[name].Metadata; + + this._pluginSettings[name] = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup(meta.id, -1), + path: `${this.settings.path}plugin/${name}/`, + }); + } + + return this._pluginSettings[name]; + } + + _setupActions() { + this.actions = new Gio.SimpleActionGroup(); + this.insert_action_group('settings', this.actions); + + let settings = this.pluginSettings('battery'); + this.actions.add_action(settings.create_action('send-statistics')); + this.actions.add_action(settings.create_action('low-battery-notification')); + this.actions.add_action(settings.create_action('custom-battery-notification')); + this.actions.add_action(settings.create_action('custom-battery-notification-value')); + this.actions.add_action(settings.create_action('full-battery-notification')); + + settings = this.pluginSettings('clipboard'); + this.actions.add_action(settings.create_action('send-content')); + this.actions.add_action(settings.create_action('receive-content')); + + settings = this.pluginSettings('contacts'); + this.actions.add_action(settings.create_action('contacts-source')); + + settings = this.pluginSettings('mousepad'); + this.actions.add_action(settings.create_action('share-control')); + + settings = this.pluginSettings('mpris'); + this.actions.add_action(settings.create_action('share-players')); + + settings = this.pluginSettings('notification'); + this.actions.add_action(settings.create_action('send-notifications')); + this.actions.add_action(settings.create_action('send-active')); + + settings = this.pluginSettings('photo'); + this.actions.add_action(settings.create_action('share-camera')); + + settings = this.pluginSettings('sftp'); + this.actions.add_action(settings.create_action('automount')); + + settings = this.pluginSettings('share'); + this.actions.add_action(settings.create_action('receive-files')); + + settings = this.pluginSettings('sms'); + this.actions.add_action(settings.create_action('legacy-sms')); + + settings = this.pluginSettings('systemvolume'); + this.actions.add_action(settings.create_action('share-sinks')); + + settings = this.pluginSettings('telephony'); + this.actions.add_action(settings.create_action('ringing-volume')); + this.actions.add_action(settings.create_action('ringing-pause')); + this.actions.add_action(settings.create_action('talking-volume')); + this.actions.add_action(settings.create_action('talking-pause')); + this.actions.add_action(settings.create_action('talking-microphone')); + + // Pair Actions + const encryption_info = new Gio.SimpleAction({name: 'encryption-info'}); + encryption_info.connect('activate', this._onEncryptionInfo.bind(this)); + this.actions.add_action(encryption_info); + + const status_pair = new Gio.SimpleAction({name: 'pair'}); + status_pair.connect('activate', this._deviceAction.bind(this.device)); + this.settings.bind('paired', status_pair, 'enabled', 16); + this.actions.add_action(status_pair); + + const status_unpair = new Gio.SimpleAction({name: 'unpair'}); + status_unpair.connect('activate', this._deviceAction.bind(this.device)); + this.settings.bind('paired', status_unpair, 'enabled', 0); + this.actions.add_action(status_unpair); + } + + /** + * Sharing Settings + */ + _sharingSettings() { + // Share Plugin + const settings = this.pluginSettings('share'); + + settings.connect( + 'changed::receive-directory', + this._onReceiveDirectoryChanged.bind(this) + ); + this._onReceiveDirectoryChanged(settings, 'receive-directory'); + + // Visibility + this.desktop_list.foreach(row => { + const name = row.get_name(); + row.visible = this.get_outgoing_supported(`${name}.request`); + }); + + // Separators & Sorting + this.desktop_list.set_header_func(rowSeparators); + + this.desktop_list.set_sort_func((row1, row2) => { + row1 = row1.get_child().get_child_at(0, 0); + row2 = row2.get_child().get_child_at(0, 0); + return row1.label.localeCompare(row2.label); + }); + this.share_list.set_header_func(rowSeparators); + + // Scroll with keyboard focus + const sharing_box = this.sharing_page.get_child().get_child(); + sharing_box.set_focus_vadjustment(this.sharing_page.vadjustment); + + // Continue focus chain between lists + this.desktop_list.next = this.share_list; + this.share_list.prev = this.desktop_list; + } + + _onReceiveDirectoryChanged(settings, key) { + let receiveDir = settings.get_string(key); + + if (receiveDir.length === 0) { + receiveDir = GLib.get_user_special_dir( + GLib.UserDirectory.DIRECTORY_DOWNLOAD + ); + + // Account for some corner cases with a fallback + const homeDir = GLib.get_home_dir(); + + if (!receiveDir || receiveDir === homeDir) + receiveDir = GLib.build_filenamev([homeDir, 'Downloads']); + + settings.set_string(key, receiveDir); + } + + if (this.receive_directory.get_filename() !== receiveDir) + this.receive_directory.set_filename(receiveDir); + } + + _onReceiveDirectorySet(button) { + const settings = this.pluginSettings('share'); + const receiveDir = settings.get_string('receive-directory'); + const filename = button.get_filename(); + + if (filename !== receiveDir) + settings.set_string('receive-directory', filename); + } + + /** + * Battery Settings + */ + async _batterySettings() { + try { + this.battery_device_list.set_header_func(rowSeparators); + this.battery_system_list.set_header_func(rowSeparators); + const settings = this.pluginSettings('battery'); + const oldLevel = settings.get_uint('custom-battery-notification-value'); + this.battery_custom_notification_value.set_value(oldLevel); + + // If the device can't handle statistics we're done + if (!this.get_incoming_supported('battery')) { + this.battery_system_label.visible = false; + this.battery_system.visible = false; + return; + } + + // Check UPower for a battery + const hasBattery = await new Promise((resolve, reject) => { + Gio.DBus.system.call( + 'org.freedesktop.UPower', + '/org/freedesktop/UPower/devices/DisplayDevice', + 'org.freedesktop.DBus.Properties', + 'Get', + new GLib.Variant('(ss)', [ + 'org.freedesktop.UPower.Device', + 'IsPresent', + ]), + null, + Gio.DBusCallFlags.NONE, + -1, + null, + (connection, res) => { + try { + const variant = connection.call_finish(res); + const value = variant.deepUnpack()[0]; + const isPresent = value.get_boolean(); + + resolve(isPresent); + } catch (e) { + resolve(false); + } + } + ); + }); + + this.battery_system_label.visible = hasBattery; + this.battery_system.visible = hasBattery; + } catch (e) { + this.battery_system_label.visible = false; + this.battery_system.visible = false; + } + } + + _setCustomChargeLevel(spin) { + const settings = this.pluginSettings('battery'); + settings.set_uint('custom-battery-notification-value', spin.get_value_as_int()); + } + + /** + * RunCommand Page + */ + _runcommandSettings() { + // Scroll with keyboard focus + const runcommand_box = this.runcommand_page.get_child().get_child(); + runcommand_box.set_focus_vadjustment(this.runcommand_page.vadjustment); + + // Local Command List + const settings = this.pluginSettings('runcommand'); + this._commands = settings.get_value('command-list').recursiveUnpack(); + + this.command_list.set_sort_func(this._sortCommands); + this.command_list.set_header_func(rowSeparators); + + for (const uuid of Object.keys(this._commands)) + this._insertCommand(uuid); + } + + _sortCommands(row1, row2) { + if (!row1.title || !row2.title) + return 1; + + return row1.title.localeCompare(row2.title); + } + + _insertCommand(uuid) { + const row = new SectionRow({ + title: this._commands[uuid].name, + subtitle: this._commands[uuid].command, + activatable: false, + }); + row.set_name(uuid); + row.subtitle_label.ellipsize = Pango.EllipsizeMode.MIDDLE; + + const editButton = new Gtk.Button({ + image: new Gtk.Image({ + icon_name: 'document-edit-symbolic', + pixel_size: 16, + visible: true, + }), + tooltip_text: _('Edit'), + valign: Gtk.Align.CENTER, + vexpand: true, + visible: true, + }); + editButton.connect('clicked', this._onEditCommand.bind(this)); + editButton.get_accessible().set_name(_('Edit')); + row.get_child().attach(editButton, 2, 0, 1, 2); + + const deleteButton = new Gtk.Button({ + image: new Gtk.Image({ + icon_name: 'edit-delete-symbolic', + pixel_size: 16, + visible: true, + }), + tooltip_text: _('Remove'), + valign: Gtk.Align.CENTER, + vexpand: true, + visible: true, + }); + deleteButton.connect('clicked', this._onDeleteCommand.bind(this)); + deleteButton.get_accessible().set_name(_('Remove')); + row.get_child().attach(deleteButton, 3, 0, 1, 2); + + this.command_list.add(row); + } + + _onEditCommand(widget) { + if (this._commandEditor === undefined) { + this._commandEditor = new CommandEditor({ + modal: true, + transient_for: this.get_toplevel(), + use_header_bar: true, + }); + + this._commandEditor.connect( + 'response', + this._onSaveCommand.bind(this) + ); + + this._commandEditor.resize(1, 1); + } + + if (widget instanceof Gtk.Button) { + const row = widget.get_ancestor(Gtk.ListBoxRow.$gtype); + const uuid = row.get_name(); + + this._commandEditor.uuid = uuid; + this._commandEditor.command_name = this._commands[uuid].name; + this._commandEditor.command_line = this._commands[uuid].command; + } else { + this._commandEditor.uuid = GLib.uuid_string_random(); + this._commandEditor.command_name = ''; + this._commandEditor.command_line = ''; + } + + this._commandEditor.present(); + } + + _storeCommands() { + const variant = {}; + + for (const [uuid, command] of Object.entries(this._commands)) + variant[uuid] = new GLib.Variant('a{ss}', command); + + this.pluginSettings('runcommand').set_value( + 'command-list', + new GLib.Variant('a{sv}', variant) + ); + } + + _onDeleteCommand(button) { + const row = button.get_ancestor(Gtk.ListBoxRow.$gtype); + delete this._commands[row.get_name()]; + row.destroy(); + + this._storeCommands(); + } + + _onSaveCommand(dialog, response_id) { + if (response_id === Gtk.ResponseType.ACCEPT) { + this._commands[dialog.uuid] = { + name: dialog.command_name, + command: dialog.command_line, + }; + + this._storeCommands(); + + // + let row = null; + + for (const child of this.command_list.get_children()) { + if (child.get_name() === dialog.uuid) { + row = child; + break; + } + } + + if (row === null) { + this._insertCommand(dialog.uuid); + } else { + row.set_name(dialog.uuid); + row.title = dialog.command_name; + row.subtitle = dialog.command_line; + } + } + + dialog.hide(); + } + + /** + * Notification Settings + */ + _notificationSettings() { + const settings = this.pluginSettings('notification'); + + settings.bind( + 'send-notifications', + this.notification_apps, + 'sensitive', + Gio.SettingsBindFlags.DEFAULT + ); + + // Separators & Sorting + this.notification_list.set_header_func(rowSeparators); + + // Scroll with keyboard focus + const notification_box = this.notification_page.get_child().get_child(); + notification_box.set_focus_vadjustment(this.notification_page.vadjustment); + + // Continue focus chain between lists + this.notification_list.next = this.notification_apps; + this.notification_apps.prev = this.notification_list; + + this.notification_apps.set_sort_func(titleSortFunc); + this.notification_apps.set_header_func(rowSeparators); + + this._populateApplications(settings); + } + + _toggleNotification(widget) { + try { + const row = widget.get_ancestor(Gtk.ListBoxRow.$gtype); + const settings = this.pluginSettings('notification'); + let applications = {}; + + try { + applications = JSON.parse(settings.get_string('applications')); + } catch (e) { + applications = {}; + } + + applications[row.title].enabled = !applications[row.title].enabled; + row.widget.state = applications[row.title].enabled; + settings.set_string('applications', JSON.stringify(applications)); + + } catch (e) { + logError(e); + } + } + + + + _populateApplications(settings) { + const applications = this._queryApplications(settings); + + for (const name in applications) { + const row = new SectionRow({ + gicon: Gio.Icon.new_for_string(applications[name].iconName), + title: name, + height_request: 48, + widget: new Gtk.Switch({ + state: applications[name].enabled, + margin_start: 12, + margin_end: 12, + halign: Gtk.Align.END, + valign: Gtk.Align.CENTER, + vexpand: true, + visible: true, + }), + }); + + row.widget.connect('notify::active', this._toggleNotification.bind(this)); + this.notification_apps.add(row); + } + } + + _queryApplications(settings) { + let applications = {}; + + try { + applications = JSON.parse(settings.get_string('applications')); + } catch (e) { + applications = {}; + } + + // Scan applications that statically declare to show notifications + const ignoreId = 'org.gnome.Shell.Extensions.GSConnect.desktop'; + + for (const appInfo of Gio.AppInfo.get_all()) { + if (appInfo.get_id() === ignoreId) + continue; + + if (!appInfo.get_boolean('X-GNOME-UsesNotifications')) + continue; + + const appName = appInfo.get_name(); + + if (appName === null || applications.hasOwnProperty(appName)) + continue; + + let icon = appInfo.get_icon(); + icon = (icon) ? icon.to_string() : 'application-x-executable'; + + applications[appName] = { + iconName: icon, + enabled: true, + }; + } + + settings.set_string('applications', JSON.stringify(applications)); + + return applications; + } + + /** + * Telephony Settings + */ + _telephonySettings() { + // Continue focus chain between lists + this.ringing_list.next = this.talking_list; + this.talking_list.prev = this.ringing_list; + + this.ringing_list.set_header_func(rowSeparators); + this.talking_list.set_header_func(rowSeparators); + } + + /** + * Keyboard Shortcuts + */ + _keybindingSettings() { + // Scroll with keyboard focus + const shortcuts_box = this.shortcuts_page.get_child().get_child(); + shortcuts_box.set_focus_vadjustment(this.shortcuts_page.vadjustment); + + // Filter & Sort + this.shortcuts_actions_list.set_filter_func(this._filterPluginKeybindings.bind(this)); + this.shortcuts_actions_list.set_header_func(rowSeparators); + this.shortcuts_actions_list.set_sort_func(titleSortFunc); + + // Init + for (const name in DEVICE_SHORTCUTS) + this._addPluginKeybinding(name); + + this._setPluginKeybindings(); + + // Watch for GAction and Keybinding changes + this._actionAddedId = this.device.action_group.connect( + 'action-added', + () => this.shortcuts_actions_list.invalidate_filter() + ); + this._actionRemovedId = this.device.action_group.connect( + 'action-removed', + () => this.shortcuts_actions_list.invalidate_filter() + ); + this._keybindingsId = this.settings.connect( + 'changed::keybindings', + this._setPluginKeybindings.bind(this) + ); + } + + _addPluginKeybinding(name) { + const [icon_name, label] = DEVICE_SHORTCUTS[name]; + + const widget = new Gtk.Label({ + label: _('Disabled'), + visible: true, + }); + widget.get_style_context().add_class('dim-label'); + + const row = new SectionRow({ + height_request: 48, + icon_name: icon_name, + title: label, + widget: widget, + }); + row.icon_image.pixel_size = 16; + row.action = name; + this.shortcuts_actions_list.add(row); + } + + _filterPluginKeybindings(row) { + return this.device.action_group.has_action(row.action); + } + + _setPluginKeybindings() { + const keybindings = this.settings.get_value('keybindings').deepUnpack(); + + this.shortcuts_actions_list.foreach(row => { + if (keybindings[row.action]) { + const accel = Gtk.accelerator_parse(keybindings[row.action]); + row.widget.label = Gtk.accelerator_get_label(...accel); + } else { + row.widget.label = _('Disabled'); + } + }); + } + + _onResetActionShortcuts(button) { + const keybindings = this.settings.get_value('keybindings').deepUnpack(); + + for (const action in keybindings) { + // Don't reset remote command shortcuts + if (!action.includes('::')) + delete keybindings[action]; + } + + this.settings.set_value( + 'keybindings', + new GLib.Variant('a{ss}', keybindings) + ); + } + + async _onShortcutRowActivated(box, row) { + try { + const keybindings = this.settings.get_value('keybindings').deepUnpack(); + let accel = keybindings[row.action] || null; + + accel = await Keybindings.getAccelerator(row.title, accel); + + if (accel) + keybindings[row.action] = accel; + else + delete keybindings[row.action]; + + this.settings.set_value( + 'keybindings', + new GLib.Variant('a{ss}', keybindings) + ); + } catch (e) { + logError(e); + } + } + + /** + * Advanced Page + */ + _advancedSettings() { + // Scroll with keyboard focus + const advanced_box = this.advanced_page.get_child().get_child(); + advanced_box.set_focus_vadjustment(this.advanced_page.vadjustment); + + // Sort & Separate + this.plugin_list.set_header_func(rowSeparators); + this.plugin_list.set_sort_func(titleSortFunc); + this.experimental_list.set_header_func(rowSeparators); + + // Continue focus chain between lists + this.plugin_list.next = this.experimental_list; + this.experimental_list.prev = this.plugin_list; + + this._disabledPluginsId = this.settings.connect( + 'changed::disabled-plugins', + this._onPluginsChanged.bind(this) + ); + this._supportedPluginsId = this.settings.connect( + 'changed::supported-plugins', + this._onPluginsChanged.bind(this) + ); + this._onPluginsChanged(this.settings, null); + + for (const name of DEVICE_PLUGINS) + this._addPlugin(name); + } + + _onPluginsChanged(settings, key) { + if (key === 'disabled-plugins' || this._disabledPlugins === undefined) + this._disabledPlugins = settings.get_strv('disabled-plugins'); + + if (key === 'supported-plugins' || this._supportedPlugins === undefined) + this._supportedPlugins = settings.get_strv('supported-plugins'); + + this._enabledPlugins = this._supportedPlugins.filter(name => { + return !this._disabledPlugins.includes(name); + }); + + if (key !== null) + this._updatePlugins(); + } + + _addPlugin(name) { + const plugin = imports.service.plugins[name]; + + const row = new SectionRow({ + height_request: 48, + title: plugin.Metadata.label, + subtitle: plugin.Metadata.description || '', + visible: this._supportedPlugins.includes(name), + widget: new Gtk.Switch({ + active: this._enabledPlugins.includes(name), + valign: Gtk.Align.CENTER, + vexpand: true, + visible: true, + }), + }); + row.widget.connect('notify::active', this._togglePlugin.bind(this)); + row.set_name(name); + + if (this.hasOwnProperty(name)) + this[name].visible = row.widget.active; + + this.plugin_list.add(row); + } + + _updatePlugins(settings, key) { + for (const row of this.plugin_list.get_children()) { + const name = row.get_name(); + + row.visible = this._supportedPlugins.includes(name); + row.widget.active = this._enabledPlugins.includes(name); + + if (this.hasOwnProperty(name)) + this[name].visible = row.widget.active; + } + } + + _togglePlugin(widget) { + try { + const name = widget.get_ancestor(Gtk.ListBoxRow.$gtype).get_name(); + const index = this._disabledPlugins.indexOf(name); + + // Either add or remove the plugin from the disabled list + if (index > -1) + this._disabledPlugins.splice(index, 1); + else + this._disabledPlugins.push(name); + + this.settings.set_strv('disabled-plugins', this._disabledPlugins); + } catch (e) { + logError(e); + } + } +}); diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/preferences/keybindings.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/preferences/keybindings.js new file mode 100644 index 0000000..3fd8322 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/preferences/keybindings.js @@ -0,0 +1,312 @@ +'use strict'; + +const Gdk = imports.gi.Gdk; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + + +/* + * A list of modifier keysyms we ignore + */ +const _MODIFIERS = [ + Gdk.KEY_Alt_L, + Gdk.KEY_Alt_R, + Gdk.KEY_Caps_Lock, + Gdk.KEY_Control_L, + Gdk.KEY_Control_R, + Gdk.KEY_Meta_L, + Gdk.KEY_Meta_R, + Gdk.KEY_Num_Lock, + Gdk.KEY_Shift_L, + Gdk.KEY_Shift_R, + Gdk.KEY_Super_L, + Gdk.KEY_Super_R, +]; + +/** + * Response enum for ShortcutChooserDialog + */ +var ResponseType = { + CANCEL: Gtk.ResponseType.CANCEL, + SET: Gtk.ResponseType.APPLY, + UNSET: 2, +}; + + +/** + * A simplified version of the shortcut editor from GNOME Control Center + */ +var ShortcutChooserDialog = GObject.registerClass({ + GTypeName: 'GSConnectPreferencesShortcutEditor', + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/preferences-shortcut-editor.ui', + Children: [ + 'cancel-button', 'set-button', + 'stack', 'summary-label', + 'shortcut-label', 'conflict-label', + ], +}, class ShortcutChooserDialog extends Gtk.Dialog { + + _init(params) { + super._init({ + transient_for: Gio.Application.get_default().get_active_window(), + use_header_bar: true, + }); + + this._seat = Gdk.Display.get_default().get_default_seat(); + + // Current accelerator or %null + this.accelerator = params.accelerator; + + // TRANSLATORS: Summary of a keyboard shortcut function + // Example: Enter a new shortcut to change Messaging + this.summary = _('Enter a new shortcut to change %s').format( + params.summary + ); + } + + get accelerator() { + return this.shortcut_label.accelerator; + } + + set accelerator(value) { + this.shortcut_label.accelerator = value; + } + + get summary() { + return this.summary_label.label; + } + + set summary(value) { + this.summary_label.label = value; + } + + vfunc_key_press_event(event) { + let keyvalLower = Gdk.keyval_to_lower(event.keyval); + let realMask = event.state & Gtk.accelerator_get_default_mod_mask(); + + // TODO: Critical: 'WIDGET_REALIZED_FOR_EVENT (widget, event)' failed + if (_MODIFIERS.includes(keyvalLower)) + return true; + + // Normalize Tab + if (keyvalLower === Gdk.KEY_ISO_Left_Tab) + keyvalLower = Gdk.KEY_Tab; + + // Put shift back if it changed the case of the key, not otherwise. + if (keyvalLower !== event.keyval) + realMask |= Gdk.ModifierType.SHIFT_MASK; + + // HACK: we don't want to use SysRq as a keybinding (but we do want + // Alt+Print), so we avoid translation from Alt+Print to SysRq + if (keyvalLower === Gdk.KEY_Sys_Req && (realMask & Gdk.ModifierType.MOD1_MASK) !== 0) + keyvalLower = Gdk.KEY_Print; + + // A single Escape press cancels the editing + if (realMask === 0 && keyvalLower === Gdk.KEY_Escape) { + this.response(ResponseType.CANCEL); + return false; + } + + // Backspace disables the current shortcut + if (realMask === 0 && keyvalLower === Gdk.KEY_BackSpace) { + this.response(ResponseType.UNSET); + return false; + } + + // CapsLock isn't supported as a keybinding modifier, so keep it from + // confusing us + realMask &= ~Gdk.ModifierType.LOCK_MASK; + + if (keyvalLower !== 0 && realMask !== 0) { + this._ungrab(); + + // Set the accelerator property/label + this.accelerator = Gtk.accelerator_name(keyvalLower, realMask); + + // TRANSLATORS: When a keyboard shortcut is unavailable + // Example: [Ctrl]+[S] is already being used + this.conflict_label.label = _('%s is already being used').format( + Gtk.accelerator_get_label(keyvalLower, realMask) + ); + + // Show Cancel button and switch to confirm/conflict page + this.cancel_button.visible = true; + this.stack.visible_child_name = 'confirm'; + + this._check(); + } + + return true; + } + + async _check() { + try { + const available = await checkAccelerator(this.accelerator); + this.set_button.visible = available; + this.conflict_label.visible = !available; + } catch (e) { + logError(e); + this.response(ResponseType.CANCEL); + } + } + + _grab() { + const success = this._seat.grab( + this.get_window(), + Gdk.SeatCapabilities.KEYBOARD, + true, // owner_events + null, // cursor + null, // event + null + ); + + if (success !== Gdk.GrabStatus.SUCCESS) + return this.response(ResponseType.CANCEL); + + if (!this._seat.get_keyboard() && !this._seat.get_pointer()) + return this.response(ResponseType.CANCEL); + + this.grab_add(); + } + + _ungrab() { + this._seat.ungrab(); + this.grab_remove(); + } + + // Override to use our own ungrab process + response(response_id) { + this.hide(); + this._ungrab(); + + return super.response(response_id); + } + + // Override with a non-blocking version of Gtk.Dialog.run() + run() { + this.show(); + + // Wait a bit before attempting grab + GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => { + this._grab(); + return GLib.SOURCE_REMOVE; + }); + } +}); + + +/** + * Check the availability of an accelerator using GNOME Shell's DBus interface. + * + * @param {string} accelerator - An accelerator + * @param {number} [modeFlags] - Mode Flags + * @param {number} [grabFlags] - Grab Flags + * @param {boolean} %true if available, %false on error or unavailable + */ +async function checkAccelerator(accelerator, modeFlags = 0, grabFlags = 0) { + try { + let result = false; + + // Try to grab the accelerator + const action = await new Promise((resolve, reject) => { + Gio.DBus.session.call( + 'org.gnome.Shell', + '/org/gnome/Shell', + 'org.gnome.Shell', + 'GrabAccelerator', + new GLib.Variant('(suu)', [accelerator, modeFlags, grabFlags]), + null, + Gio.DBusCallFlags.NONE, + -1, + null, + (connection, res) => { + try { + res = connection.call_finish(res); + resolve(res.deepUnpack()[0]); + } catch (e) { + reject(e); + } + } + ); + }); + + // If successful, use the result of ungrabbing as our return + if (action !== 0) { + result = await new Promise((resolve, reject) => { + Gio.DBus.session.call( + 'org.gnome.Shell', + '/org/gnome/Shell', + 'org.gnome.Shell', + 'UngrabAccelerator', + new GLib.Variant('(u)', [action]), + null, + Gio.DBusCallFlags.NONE, + -1, + null, + (connection, res) => { + try { + res = connection.call_finish(res); + resolve(res.deepUnpack()[0]); + } catch (e) { + reject(e); + } + } + ); + }); + } + + return result; + } catch (e) { + logError(e); + return false; + } +} + + +/** + * Show a dialog to get a keyboard shortcut from a user. + * + * @param {string} summary - A description of the keybinding's function + * @param {string} accelerator - An accelerator as taken by Gtk.ShortcutLabel + * @return {string} An accelerator or %null if it should be unset. + */ +async function getAccelerator(summary, accelerator = null) { + try { + const dialog = new ShortcutChooserDialog({ + summary: summary, + accelerator: accelerator, + }); + + accelerator = await new Promise((resolve, reject) => { + dialog.connect('response', (dialog, response) => { + switch (response) { + case ResponseType.SET: + accelerator = dialog.accelerator; + break; + + case ResponseType.UNSET: + accelerator = null; + break; + + case ResponseType.CANCEL: + // leave the accelerator as passed in + break; + } + + dialog.destroy(); + + resolve(accelerator); + }); + + dialog.run(); + }); + + return accelerator; + } catch (e) { + logError(e); + return accelerator; + } +} + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/preferences/service.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/preferences/service.js new file mode 100644 index 0000000..d75f4f9 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/preferences/service.js @@ -0,0 +1,657 @@ +'use strict'; + +const Gdk = imports.gi.Gdk; +const GdkPixbuf = imports.gi.GdkPixbuf; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + +const Config = imports.config; +const Device = imports.preferences.device; +const Remote = imports.utils.remote; + + +/* + * Header for support logs + */ +const LOG_HEADER = new GLib.Bytes(` +GSConnect: ${Config.PACKAGE_VERSION} (${Config.IS_USER ? 'user' : 'system'}) +GJS: ${imports.system.version} +Session: ${GLib.getenv('XDG_SESSION_TYPE')} +OS: ${GLib.get_os_info('PRETTY_NAME')} +-------------------------------------------------------------------------------- +`); + + +/** + * Generate a support log. + * + * @param {string} time - Start time as a string (24-hour notation) + */ +async function generateSupportLog(time) { + try { + const [file, stream] = Gio.File.new_tmp('gsconnect.XXXXXX'); + const logFile = stream.get_output_stream(); + + await new Promise((resolve, reject) => { + logFile.write_bytes_async(LOG_HEADER, 0, null, (file, res) => { + try { + resolve(file.write_bytes_finish(res)); + } catch (e) { + reject(e); + } + }); + }); + + // FIXME: BSD??? + const proc = new Gio.Subprocess({ + flags: (Gio.SubprocessFlags.STDOUT_PIPE | + Gio.SubprocessFlags.STDERR_MERGE), + argv: ['journalctl', '--no-host', '--since', time], + }); + proc.init(null); + + logFile.splice_async( + proc.get_stdout_pipe(), + Gio.OutputStreamSpliceFlags.CLOSE_TARGET, + GLib.PRIORITY_DEFAULT, + null, + (source, res) => { + try { + source.splice_finish(res); + } catch (e) { + logError(e); + } + } + ); + + await new Promise((resolve, reject) => { + proc.wait_check_async(null, (proc, res) => { + try { + resolve(proc.wait_finish(res)); + } catch (e) { + reject(e); + } + }); + }); + + const uri = file.get_uri(); + Gio.AppInfo.launch_default_for_uri_async(uri, null, null, null); + } catch (e) { + logError(e); + } +} + + +/** + * "Connect to..." Dialog + */ +var ConnectDialog = GObject.registerClass({ + GTypeName: 'GSConnectConnectDialog', + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/connect-dialog.ui', + Children: [ + 'cancel-button', 'connect-button', + 'lan-grid', 'lan-ip', 'lan-port', + ], +}, class ConnectDialog extends Gtk.Dialog { + + _init(params = {}) { + super._init(Object.assign({ + use_header_bar: true, + }, params)); + } + + vfunc_response(response_id) { + if (response_id === Gtk.ResponseType.OK) { + try { + let address; + + // Lan host/port entered + if (this.lan_ip.text) { + const host = this.lan_ip.text; + const port = this.lan_port.value; + address = GLib.Variant.new_string(`lan://${host}:${port}`); + } else { + return false; + } + + this.application.activate_action('connect', address); + } catch (e) { + logError(e); + } + } + + this.destroy(); + return false; + } +}); + + +function rowSeparators(row, before) { + const header = row.get_header(); + + if (before === null) { + if (header !== null) + header.destroy(); + + return; + } + + if (header === null) + row.set_header(new Gtk.Separator({visible: true})); +} + + +var Window = GObject.registerClass({ + GTypeName: 'GSConnectPreferencesWindow', + Properties: { + 'display-mode': GObject.ParamSpec.string( + 'display-mode', + 'Display Mode', + 'Display devices in either the Panel or User Menu', + GObject.ParamFlags.READWRITE, + null + ), + }, + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/preferences-window.ui', + Children: [ + // HeaderBar + 'headerbar', 'infobar', 'stack', + 'service-menu', 'service-edit', 'refresh-button', + 'device-menu', 'prev-button', + + // Popover + 'rename-popover', 'rename', 'rename-label', 'rename-entry', 'rename-submit', + + // Focus Box + 'service-window', 'service-box', + + // Device List + 'device-list', 'device-list-spinner', 'device-list-placeholder', + ], +}, class PreferencesWindow extends Gtk.ApplicationWindow { + + _init(params = {}) { + super._init(params); + + // Service Settings + this.settings = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup( + 'org.gnome.Shell.Extensions.GSConnect', + true + ), + }); + + // Service Proxy + this.service = new Remote.Service(); + + this._deviceAddedId = this.service.connect( + 'device-added', + this._onDeviceAdded.bind(this) + ); + + this._deviceRemovedId = this.service.connect( + 'device-removed', + this._onDeviceRemoved.bind(this) + ); + + this._serviceChangedId = this.service.connect( + 'notify::active', + this._onServiceChanged.bind(this) + ); + + // HeaderBar (Service Name) + this.headerbar.title = this.settings.get_string('name'); + this.rename_entry.text = this.headerbar.title; + + // Scroll with keyboard focus + this.service_box.set_focus_vadjustment(this.service_window.vadjustment); + + // Device List + this.device_list.set_header_func(rowSeparators); + + // Discoverable InfoBar + this.settings.bind( + 'discoverable', + this.infobar, + 'reveal-child', + Gio.SettingsBindFlags.INVERT_BOOLEAN + ); + this.add_action(this.settings.create_action('discoverable')); + + // Application Menu + this._initMenu(); + + // Broadcast automatically every 5 seconds if there are no devices yet + this._refreshSource = GLib.timeout_add_seconds( + GLib.PRIORITY_DEFAULT, + 5, + this._refresh.bind(this) + ); + + // Restore window size/maximized/position + this._restoreGeometry(); + + // Prime the service + this._initService(); + } + + get display_mode() { + if (this.settings.get_boolean('show-indicators')) + return 'panel'; + + return 'user-menu'; + } + + set display_mode(mode) { + this.settings.set_boolean('show-indicators', (mode === 'panel')); + } + + vfunc_delete_event(event) { + if (this.service) { + this.service.disconnect(this._deviceAddedId); + this.service.disconnect(this._deviceRemovedId); + this.service.disconnect(this._serviceChangedId); + this.service.destroy(); + this.service = null; + } + + this._saveGeometry(); + GLib.source_remove(this._refreshSource); + + return false; + } + + async _initService() { + try { + this.refresh_button.grab_focus(); + + this._onServiceChanged(this.service, null); + await this.service.reload(); + } catch (e) { + logError(e, 'GSConnect'); + } + } + + _initMenu() { + // Panel/User Menu mode + const displayMode = new Gio.PropertyAction({ + name: 'display-mode', + property_name: 'display-mode', + object: this, + }); + this.add_action(displayMode); + + // About Dialog + const aboutDialog = new Gio.SimpleAction({name: 'about'}); + aboutDialog.connect('activate', this._aboutDialog.bind(this)); + this.add_action(aboutDialog); + + // "Connect to..." Dialog + const connectDialog = new Gio.SimpleAction({name: 'connect'}); + connectDialog.connect('activate', this._connectDialog.bind(this)); + this.add_action(connectDialog); + + // "Generate Support Log" GAction + const generateSupportLog = new Gio.SimpleAction({name: 'support-log'}); + generateSupportLog.connect('activate', this._generateSupportLog.bind(this)); + this.add_action(generateSupportLog); + + // "Help" GAction + const help = new Gio.SimpleAction({name: 'help'}); + help.connect('activate', this._help); + this.add_action(help); + } + + _refresh() { + if (this.service.active && this.device_list.get_children().length < 1) { + this.device_list_spinner.active = true; + this.service.activate_action('refresh', null); + } else { + this.device_list_spinner.active = false; + } + + return GLib.SOURCE_CONTINUE; + } + + /* + * Window State + */ + _restoreGeometry() { + this._windowState = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup( + 'org.gnome.Shell.Extensions.GSConnect.WindowState', + true + ), + path: '/org/gnome/shell/extensions/gsconnect/preferences/', + }); + + // Size + const [width, height] = this._windowState.get_value('window-size').deepUnpack(); + + if (width && height) + this.set_default_size(width, height); + + // Maximized State + if (this._windowState.get_boolean('window-maximized')) + this.maximize(); + } + + _saveGeometry() { + const state = this.get_window().get_state(); + + // Maximized State + const maximized = (state & Gdk.WindowState.MAXIMIZED); + this._windowState.set_boolean('window-maximized', maximized); + + // Leave the size at the value before maximizing + if (maximized || (state & Gdk.WindowState.FULLSCREEN)) + return; + + // Size + const size = this.get_size(); + this._windowState.set_value('window-size', new GLib.Variant('(ii)', size)); + } + + /** + * About Dialog + */ + _aboutDialog() { + if (this._about === undefined) { + this._about = new Gtk.AboutDialog({ + application: Gio.Application.get_default(), + authors: [ + 'Andy Holmes ', + 'Bertrand Lacoste ', + 'Frank Dana ', + ], + comments: _('A complete KDE Connect implementation for GNOME'), + logo: GdkPixbuf.Pixbuf.new_from_resource_at_scale( + '/org/gnome/Shell/Extensions/GSConnect/icons/org.gnome.Shell.Extensions.GSConnect.svg', + 128, + 128, + true + ), + program_name: 'GSConnect', + // TRANSLATORS: eg. 'Translator Name ' + translator_credits: _('translator-credits'), + version: Config.PACKAGE_VERSION.toString(), + website: Config.PACKAGE_URL, + license_type: Gtk.License.GPL_2_0, + modal: true, + transient_for: this, + }); + + // Persist + this._about.connect('response', (dialog) => dialog.hide_on_delete()); + this._about.connect('delete-event', (dialog) => dialog.hide_on_delete()); + } + + this._about.present(); + } + + /** + * Connect to..." Dialog + */ + _connectDialog() { + new ConnectDialog({ + application: Gio.Application.get_default(), + modal: true, + transient_for: this, + }); + } + + /* + * "Generate Support Log" GAction + */ + _generateSupportLog() { + const dialog = new Gtk.MessageDialog({ + text: _('Generate Support Log'), + secondary_text: _('Debug messages are being logged. Take any steps necessary to reproduce a problem then review the log.'), + }); + dialog.add_button(_('Cancel'), Gtk.ResponseType.CANCEL); + dialog.add_button(_('Review Log'), Gtk.ResponseType.OK); + + // Enable debug logging and mark the current time + this.settings.set_boolean('debug', true); + const now = GLib.DateTime.new_now_local().format('%R'); + + dialog.connect('response', (dialog, response_id) => { + // Disable debug logging and destroy the dialog + this.settings.set_boolean('debug', false); + dialog.destroy(); + + // Only generate a log if instructed + if (response_id === Gtk.ResponseType.OK) + generateSupportLog(now); + }); + + dialog.show_all(); + } + + /* + * "Help" GAction + */ + _help(action, parameter) { + const uri = `${Config.PACKAGE_URL}/wiki/Help`; + Gio.AppInfo.launch_default_for_uri_async(uri, null, null, null); + } + + /* + * HeaderBar Callbacks + */ + _onPrevious(button, event) { + // HeaderBar (Service) + this.prev_button.visible = false; + this.device_menu.visible = false; + + this.refresh_button.visible = true; + this.service_edit.visible = true; + this.service_menu.visible = true; + + this.headerbar.title = this.settings.get_string('name'); + this.headerbar.subtitle = null; + + // Panel + this.stack.visible_child_name = 'service'; + this._setDeviceMenu(); + } + + _onEditServiceName(button, event) { + this.rename_entry.text = this.headerbar.title; + this.rename_entry.has_focus = true; + } + + _onSetServiceName(widget) { + if (this.rename_entry.text.length) { + this.headerbar.title = this.rename_entry.text; + this.settings.set_string('name', this.rename_entry.text); + } + + this.service_edit.active = false; + this.service_edit.grab_focus(); + } + + /* + * Context Switcher + */ + _getTypeLabel(device) { + switch (device.type) { + case 'laptop': + return _('Laptop'); + case 'phone': + return _('Smartphone'); + case 'tablet': + return _('Tablet'); + case 'tv': + return _('Television'); + default: + return _('Desktop'); + } + } + + _setDeviceMenu(panel = null) { + this.device_menu.insert_action_group('device', null); + this.device_menu.insert_action_group('settings', null); + this.device_menu.set_menu_model(null); + + if (panel === null) + return; + + this.device_menu.insert_action_group('device', panel.device.action_group); + this.device_menu.insert_action_group('settings', panel.actions); + this.device_menu.set_menu_model(panel.menu); + } + + _onDeviceChanged(statusLabel, device, pspec) { + switch (false) { + case device.paired: + statusLabel.label = _('Unpaired'); + break; + + case device.connected: + statusLabel.label = _('Disconnected'); + break; + + default: + statusLabel.label = _('Connected'); + } + } + + _createDeviceRow(device) { + const row = new Gtk.ListBoxRow({ + height_request: 52, + selectable: false, + visible: true, + }); + row.set_name(device.id); + + const grid = new Gtk.Grid({ + column_spacing: 12, + margin_left: 20, + margin_right: 20, + margin_bottom: 8, + margin_top: 8, + visible: true, + }); + row.add(grid); + + const icon = new Gtk.Image({ + gicon: new Gio.ThemedIcon({name: device.icon_name}), + icon_size: Gtk.IconSize.BUTTON, + visible: true, + }); + grid.attach(icon, 0, 0, 1, 1); + + const title = new Gtk.Label({ + halign: Gtk.Align.START, + hexpand: true, + valign: Gtk.Align.CENTER, + vexpand: true, + visible: true, + }); + grid.attach(title, 1, 0, 1, 1); + + const status = new Gtk.Label({ + halign: Gtk.Align.END, + hexpand: true, + valign: Gtk.Align.CENTER, + vexpand: true, + visible: true, + }); + grid.attach(status, 2, 0, 1, 1); + + // Keep name up to date + device.bind_property( + 'name', + title, + 'label', + GObject.BindingFlags.SYNC_CREATE + ); + + // Keep status up to date + device.connect( + 'notify::connected', + this._onDeviceChanged.bind(null, status) + ); + device.connect( + 'notify::paired', + this._onDeviceChanged.bind(null, status) + ); + this._onDeviceChanged(status, device, null); + + return row; + } + + _onDeviceAdded(service, device) { + try { + if (!this.stack.get_child_by_name(device.id)) { + // Add the device preferences + const prefs = new Device.Panel(device); + this.stack.add_titled(prefs, device.id, device.name); + + // Add a row to the device list + prefs.row = this._createDeviceRow(device); + this.device_list.add(prefs.row); + } + } catch (e) { + logError(e); + } + } + + _onDeviceRemoved(service, device) { + try { + const prefs = this.stack.get_child_by_name(device.id); + + if (prefs === null) + return; + + if (prefs === this.stack.get_visible_child()) + this._onPrevious(); + + prefs.row.destroy(); + prefs.row = null; + + prefs.dispose(); + prefs.destroy(); + } catch (e) { + logError(e); + } + } + + _onDeviceSelected(box, row) { + try { + if (row === null) + return this._onPrevious(); + + // Transition the panel + const name = row.get_name(); + const prefs = this.stack.get_child_by_name(name); + + this.stack.visible_child = prefs; + this._setDeviceMenu(prefs); + + // HeaderBar (Device) + this.refresh_button.visible = false; + this.service_edit.visible = false; + this.service_menu.visible = false; + + this.prev_button.visible = true; + this.device_menu.visible = true; + + this.headerbar.title = prefs.device.name; + this.headerbar.subtitle = this._getTypeLabel(prefs.device); + } catch (e) { + logError(e); + } + } + + _onServiceChanged(service, pspec) { + if (this.service.active) + this.device_list_placeholder.label = _('Searching for devices…'); + else + this.device_list_placeholder.label = _('Waiting for service…'); + } +}); + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/prefs.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/prefs.js new file mode 100644 index 0000000..89760dd --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/prefs.js @@ -0,0 +1,28 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Gtk = imports.gi.Gtk; + +// Bootstrap +const Extension = imports.misc.extensionUtils.getCurrentExtension(); +const Utils = Extension.imports.shell.utils; + +function init() { + Utils.installService(); +} + +function buildPrefsWidget() { + // Destroy the window once the mainloop starts + const widget = new Gtk.Box(); + + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + widget.get_root().destroy(); + return false; + }); + + Gio.Subprocess.new([`${Extension.path}/gsconnect-preferences`], 0); + + return widget; +} + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/schemas/gschemas.compiled b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/schemas/gschemas.compiled new file mode 100644 index 0000000000000000000000000000000000000000..182908df4e242e180f0d8ee271d9181c817b0002 GIT binary patch literal 5077 zcmbW5acou99mg-Mg(5|YYqvV1fylz^=Idj#)oSL}hy|RKwG?ouOSpaS(q6gm-n)12 zdu=D^KP;L-V`oNe7#hrCLKckOG8|cCF$+2}XJ$5ZBtU{o$XGU#W^uu}E$;g{_ubk` zSa>_hx8L9U-E+_Ro!|ML-|yV6+iI6w+mEekYZdp+H$O7XsZu;o_>@pDED#n7*HzId zURp(y7~D|Br^SG=Kz)m4eWaGmvBAuBQ1i{7es`T7TgaW(dh3*UwI=-DJ=W9chb5yw zBt9YJ?)dG`(Ic;x{(W&ron?LFPt!j_zd>@B^n1i7guX3L96^6ha)tToLlCFPYT6{&mTgAyjXUg7IU7lJsP%4PBwD= zoe^E6lU=nK)Ekr|C$tOZ?T_tY?6r-rPr~GjC7O3wToh6}k+CxrlSdL;2$E9Z@ks6e z^^?Lpw(~KIn(+~~{Hp(JTVZk)ar=SzKSF8~GB%N{$zfQ-IN2yDm-0^S_B`nNgx6hx*!a%EXX%fOt%t?b`m|IOTuE_IO2UtK<#I?3UsiL7U35w|x|;J@p8|mwH86J2 zQd_xO%x<;p+r4(I!ep$TIWvq8use;3=G!{IKx}Y#xl!wHP{Cg!96YjeFM8ygq|b=6 z!oaTX9D3wd>DP;Ug$3DvzKtGvh4i6#uaK?<8Ncrn9}-p#?E8@Z$mGQ-F?XgNWzWkF zCdOy|Aa-}Sc{|o!g4a#c`N@|fRko{i({mk;t2|8h=YPDPt=L{!Cy3%pUB_Q}=FOBd zW0`WfO}PzS8h2^#G53u~SyyrG=1m!>v99%Px_^L)KCU19cyapsW6Z}LlHAoLzDG#? zii}^o#oR5Ecnm#z%qd~T>^x9Q{0va18Cp}PYfWS4@RMg>W!>CgvFW1tlEB$RkIdQA zL#rNCza@~dwL#2%H$7uJT+hK)nz1*1Fz4_k=MEcawX9{WYE|!;vPO~`uQ+Giv1hkh z#vbT3n>LddvG4A$FK>XkuV06M#FvEB56JjoTFgD(bB65vST-seCn$b-taeRy+~-tW z-4oOZ5!2WM!1)680(Dd6&D_~ZE9u=neCF~WdCoUPi)@_`?-yEs{SntEBb&S^-Fw2_ z2kvY}kGz1ji7yGS6!!lfJu>H+kIQ<&YxwHp=#jZUw2E)$Sv4k*@x^lS2I0d8p4~!! zWMXK$*bx>Uf8Nw~a>CCd8A%@a<>bK#$D2ro=~uwT(YC;~+E6 zYvR|1&qgiV=#Py3r^S3rxxOkEwK{Ky_Mq#zv75wd#*W&Gj$h8|w#KK9-legLlxi!v zcvs6^l$=XTA+Iq{%ti@QNtx8kS2;?t2iup+4!)YeHsU%$VuHI7WGF%*nx-sj2@ZuF)H3IO#J26N6{lQ&Qs!X;kFm=Uy2?XdnU!t3Hi&bR-i{Vbx?d% zNY4i{=i``|Z}|4mj)$em$!9t9(VdI<=*~GaAKkg3s2V#t?W0Rf(TE?DE0P@~=03SU zim!Z7(M?7{mb{eYW5+jPhsxyVf-_jIURq4NM#Cz&gojFXV9E~jnLNgxq`8RmMNG0^ z + + + + + true + + + false + + + + + "" + + + "" + + + [] + + + false + + + true + + + + + + + (0, 0) + + + false + + + + + + + "" + + + {} + + + ["sms", "ring", "mount", "commands", "share", "photo", "keyboard"] + + + "" + + + false + + + "smartphone" + + + [] + + + [] + + + [] + + + [] + + + "" + + + + + + false + + + true + + + false + Enables custom battery notification + + + + 80 + + + false + + + + + false + + + false + + + + + true + + + + + + true + + + + + true + + + + + true + + + true + + + '{}' + + + + + true + + + "" + + + + + + + {} + + + + + true + + + + + true + + + "" + + + + + false + + + + + true + + + + + "lower" + + + false + + + "mute" + + + true + + + true + + + + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/__init__.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/__init__.js new file mode 100644 index 0000000..3320f49 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/__init__.js @@ -0,0 +1,401 @@ +'use strict'; + +const ByteArray = imports.byteArray; +const Gettext = imports.gettext; + +const Gio = imports.gi.Gio; +const GIRepository = imports.gi.GIRepository; +const GLib = imports.gi.GLib; + +const Config = imports.config; + + +// User Directories +Config.CACHEDIR = GLib.build_filenamev([GLib.get_user_cache_dir(), 'gsconnect']); +Config.CONFIGDIR = GLib.build_filenamev([GLib.get_user_config_dir(), 'gsconnect']); +Config.RUNTIMEDIR = GLib.build_filenamev([GLib.get_user_runtime_dir(), 'gsconnect']); + + +// Ensure config.js is setup properly +const userDir = GLib.build_filenamev([GLib.get_user_data_dir(), 'gnome-shell']); + +if (Config.PACKAGE_DATADIR.startsWith(userDir)) { + Config.IS_USER = true; + + Config.GSETTINGS_SCHEMA_DIR = `${Config.PACKAGE_DATADIR}/schemas`; + Config.PACKAGE_LOCALEDIR = `${Config.PACKAGE_DATADIR}/locale`; + + // Infer libdir by assuming gnome-shell shares a common prefix with gjs; + // assume the parent directory if it's not there + let libdir = GIRepository.Repository.get_search_path().find(path => { + return path.endsWith('/gjs/girepository-1.0'); + }).replace('/gjs/girepository-1.0', ''); + + const gsdir = GLib.build_filenamev([libdir, 'gnome-shell']); + + if (!GLib.file_test(gsdir, GLib.FileTest.IS_DIR)) { + const currentDir = `/${GLib.path_get_basename(libdir)}`; + libdir = libdir.replace(currentDir, ''); + } + + Config.GNOME_SHELL_LIBDIR = libdir; +} + + +// Init Gettext +String.prototype.format = imports.format.format; +Gettext.bindtextdomain(Config.APP_ID, Config.PACKAGE_LOCALEDIR); +globalThis._ = GLib.dgettext.bind(null, Config.APP_ID); +globalThis.ngettext = GLib.dngettext.bind(null, Config.APP_ID); + + +// Init GResources +Gio.Resource.load( + GLib.build_filenamev([Config.PACKAGE_DATADIR, `${Config.APP_ID}.gresource`]) +)._register(); + + +// Init GSchema +Config.GSCHEMA = Gio.SettingsSchemaSource.new_from_directory( + Config.GSETTINGS_SCHEMA_DIR, + Gio.SettingsSchemaSource.get_default(), + false +); + + +// Load DBus interfaces +Config.DBUS = (() => { + const bytes = Gio.resources_lookup_data( + GLib.build_filenamev([Config.APP_PATH, `${Config.APP_ID}.xml`]), + Gio.ResourceLookupFlags.NONE + ); + + const xml = ByteArray.toString(bytes.toArray()); + const dbus = Gio.DBusNodeInfo.new_for_xml(xml); + dbus.nodes.forEach(info => info.cache_build()); + + return dbus; +})(); + + +// Init User Directories +for (const path of [Config.CACHEDIR, Config.CONFIGDIR, Config.RUNTIMEDIR]) + GLib.mkdir_with_parents(path, 0o755); + + +/** + * Check if we're in a Wayland session (mostly for input synthesis) + * https://wiki.gnome.org/Accessibility/Wayland#Bugs.2FIssues_We_Must_Address + */ +globalThis.HAVE_REMOTEINPUT = GLib.getenv('GDMSESSION') !== 'ubuntu-wayland'; +globalThis.HAVE_WAYLAND = GLib.getenv('XDG_SESSION_TYPE') === 'wayland'; + + +/** + * A custom debug function that logs at LEVEL_MESSAGE to avoid the need for env + * variables to be set. + * + * @param {Error|string} message - A string or Error to log + * @param {string} [prefix] - An optional prefix for the warning + */ +const _debugCallerMatch = new RegExp(/([^@]*)@([^:]*):([^:]*)/); +// eslint-disable-next-line func-style +const _debugFunc = function (error, prefix = null) { + let caller, message; + + if (error.stack) { + caller = error.stack.split('\n')[0]; + message = `${error.message}\n${error.stack}`; + } else { + caller = (new Error()).stack.split('\n')[1]; + message = JSON.stringify(error, null, 2); + } + + if (prefix) + message = `${prefix}: ${message}`; + + const [, func, file, line] = _debugCallerMatch.exec(caller); + const script = file.replace(Config.PACKAGE_DATADIR, ''); + + GLib.log_structured('GSConnect', GLib.LogLevelFlags.LEVEL_MESSAGE, { + 'MESSAGE': `[${script}:${func}:${line}]: ${message}`, + 'SYSLOG_IDENTIFIER': 'org.gnome.Shell.Extensions.GSConnect', + 'CODE_FILE': file, + 'CODE_FUNC': func, + 'CODE_LINE': line, + }); +}; + +// Swap the function out for a no-op anonymous function for speed +const settings = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup(Config.APP_ID, true), +}); + +settings.connect('changed::debug', (settings, key) => { + globalThis.debug = settings.get_boolean(key) ? _debugFunc : () => {}; +}); + +if (settings.get_boolean('debug')) + globalThis.debug = _debugFunc; +else + globalThis.debug = () => {}; + + +/** + * A simple (for now) pre-comparison sanitizer for phone numbers + * See: https://github.com/KDE/kdeconnect-kde/blob/master/smsapp/conversationlistmodel.cpp#L200-L210 + * + * @return {string} Return the string stripped of leading 0, and ' ()-+' + */ +String.prototype.toPhoneNumber = function () { + const strippedNumber = this.replace(/^0*|[ ()+-]/g, ''); + + if (strippedNumber.length) + return strippedNumber; + + return this; +}; + + +/** + * A simple equality check for phone numbers based on `toPhoneNumber()` + * + * @param {string} number - A phone number string to compare + * @return {boolean} If `this` and @number are equivalent phone numbers + */ +String.prototype.equalsPhoneNumber = function (number) { + const a = this.toPhoneNumber(); + const b = number.toPhoneNumber(); + + return (a.endsWith(b) || b.endsWith(a)); +}; + + +/** + * An implementation of `rm -rf` in Gio + * + * @param {Gio.File|string} file - a GFile or filepath + */ +Gio.File.rm_rf = function (file) { + try { + if (typeof file === 'string') + file = Gio.File.new_for_path(file); + + try { + const iter = file.enumerate_children( + 'standard::name', + Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, + null + ); + + let info; + + while ((info = iter.next_file(null))) + Gio.File.rm_rf(iter.get_child(info)); + + iter.close(null); + } catch (e) { + // Silence errors + } + + file.delete(null); + } catch (e) { + // Silence errors + } +}; + + +/** + * Extend GLib.Variant with a static method to recursively pack a variant + * + * @param {*} [obj] - May be a GLib.Variant, Array, standard Object or literal. + * @return {GLib.Variant} The resulting GVariant + */ +function _full_pack(obj) { + let packed; + const type = typeof obj; + + switch (true) { + case (obj instanceof GLib.Variant): + return obj; + + case (type === 'string'): + return GLib.Variant.new('s', obj); + + case (type === 'number'): + return GLib.Variant.new('d', obj); + + case (type === 'boolean'): + return GLib.Variant.new('b', obj); + + case (obj instanceof Uint8Array): + return GLib.Variant.new('ay', obj); + + case (obj === null): + return GLib.Variant.new('mv', null); + + case (typeof obj.map === 'function'): + return GLib.Variant.new( + 'av', + obj.filter(e => e !== undefined).map(e => _full_pack(e)) + ); + + case (obj instanceof Gio.Icon): + return obj.serialize(); + + case (type === 'object'): + packed = {}; + + for (const [key, val] of Object.entries(obj)) { + if (val !== undefined) + packed[key] = _full_pack(val); + } + + return GLib.Variant.new('a{sv}', packed); + + default: + throw Error(`Unsupported type '${type}': ${obj}`); + } +} + +GLib.Variant.full_pack = _full_pack; + + +/** + * Extend GLib.Variant with a method to recursively deepUnpack() a variant + * + * @param {*} [obj] - May be a GLib.Variant, Array, standard Object or literal. + * @return {*} The resulting object + */ +function _full_unpack(obj) { + obj = (obj === undefined) ? this : obj; + const unpacked = {}; + + switch (true) { + case (obj === null): + return obj; + + case (obj instanceof GLib.Variant): + return _full_unpack(obj.deepUnpack()); + + case (obj instanceof Uint8Array): + return obj; + + case (typeof obj.map === 'function'): + return obj.map(e => _full_unpack(e)); + + case (typeof obj === 'object'): + for (const [key, value] of Object.entries(obj)) { + // Try to detect and deserialize GIcons + try { + if (key === 'icon' && value.get_type_string() === '(sv)') + unpacked[key] = Gio.Icon.deserialize(value); + else + unpacked[key] = _full_unpack(value); + } catch (e) { + unpacked[key] = _full_unpack(value); + } + } + + return unpacked; + + default: + return obj; + } +} + +GLib.Variant.prototype.full_unpack = _full_unpack; + + +/** + * Creates a GTlsCertificate from the PEM-encoded data in @cert_path and + * @key_path. If either are missing a new pair will be generated. + * + * Additionally, the private key will be added using ssh-add to allow sftp + * connections using Gio. + * + * See: https://github.com/KDE/kdeconnect-kde/blob/master/core/kdeconnectconfig.cpp#L119 + * + * @param {string} certPath - Absolute path to a x509 certificate in PEM format + * @param {string} keyPath - Absolute path to a private key in PEM format + * @param {string} commonName - A unique common name for the certificate + * @return {Gio.TlsCertificate} A TLS certificate + */ +Gio.TlsCertificate.new_for_paths = function (certPath, keyPath, commonName = null) { + // Check if the certificate/key pair already exists + const certExists = GLib.file_test(certPath, GLib.FileTest.EXISTS); + const keyExists = GLib.file_test(keyPath, GLib.FileTest.EXISTS); + + // Create a new certificate and private key if necessary + if (!certExists || !keyExists) { + // If we weren't passed a common name, generate a random one + if (!commonName) + commonName = GLib.uuid_string_random(); + + const proc = new Gio.Subprocess({ + argv: [ + Config.OPENSSL_PATH, 'req', + '-new', '-x509', '-sha256', + '-out', certPath, + '-newkey', 'rsa:4096', '-nodes', + '-keyout', keyPath, + '-days', '3650', + '-subj', `/O=andyholmes.github.io/OU=GSConnect/CN=${commonName}`, + ], + flags: (Gio.SubprocessFlags.STDOUT_SILENCE | + Gio.SubprocessFlags.STDERR_SILENCE), + }); + proc.init(null); + proc.wait_check(null); + } + + return Gio.TlsCertificate.new_from_files(certPath, keyPath); +}; + +Object.defineProperties(Gio.TlsCertificate.prototype, { + /** + * Compute a SHA256 fingerprint of the certificate. + * See: https://gitlab.gnome.org/GNOME/glib/issues/1290 + * + * @return {string} A SHA256 fingerprint of the certificate. + */ + 'sha256': { + value: function () { + if (!this.__fingerprint) { + const proc = new Gio.Subprocess({ + argv: [Config.OPENSSL_PATH, 'x509', '-noout', '-fingerprint', '-sha256', '-inform', 'pem'], + flags: Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE, + }); + proc.init(null); + + const stdout = proc.communicate_utf8(this.certificate_pem, null)[1]; + this.__fingerprint = /[a-zA-Z0-9:]{95}/.exec(stdout)[0]; + } + + return this.__fingerprint; + }, + enumerable: false, + }, + + /** + * The common name of the certificate. + */ + 'common_name': { + get: function () { + if (!this.__common_name) { + const proc = new Gio.Subprocess({ + argv: [Config.OPENSSL_PATH, 'x509', '-noout', '-subject', '-inform', 'pem'], + flags: Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE, + }); + proc.init(null); + + const stdout = proc.communicate_utf8(this.certificate_pem, null)[1]; + this.__common_name = /(?:cn|CN) ?= ?([^,\n]*)/.exec(stdout)[1]; + } + + return this.__common_name; + }, + enumerable: true, + }, +}); + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/backends/lan.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/backends/lan.js new file mode 100644 index 0000000..f0d911e --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/backends/lan.js @@ -0,0 +1,985 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Config = imports.config; +const Core = imports.service.core; + + +/** + * TCP Port Constants + */ +const DEFAULT_PORT = 1716; +const TRANSFER_MIN = 1739; +const TRANSFER_MAX = 1764; + + +/* + * One-time check for Linux/FreeBSD socket options + */ +var _LINUX_SOCKETS = true; + +try { + // This should throw on FreeBSD + Gio.Socket.new( + Gio.SocketFamily.IPV4, + Gio.SocketType.STREAM, + Gio.SocketProtocol.TCP + ).get_option(6, 5); +} catch (e) { + _LINUX_SOCKETS = false; +} + + +/** + * Configure a socket connection for the KDE Connect protocol. + * + * @param {Gio.SocketConnection} connection - The connection to configure + */ +function _configureSocket(connection) { + try { + if (_LINUX_SOCKETS) { + connection.socket.set_option(6, 4, 10); // TCP_KEEPIDLE + connection.socket.set_option(6, 5, 5); // TCP_KEEPINTVL + connection.socket.set_option(6, 6, 3); // TCP_KEEPCNT + + // FreeBSD constants + // https://github.com/freebsd/freebsd/blob/master/sys/netinet/tcp.h#L159 + } else { + connection.socket.set_option(6, 256, 10); // TCP_KEEPIDLE + connection.socket.set_option(6, 512, 5); // TCP_KEEPINTVL + connection.socket.set_option(6, 1024, 3); // TCP_KEEPCNT + } + + // Do this last because an error setting the keepalive options would + // result in a socket that never times out + connection.socket.set_keepalive(true); + } catch (e) { + debug(e, 'Configuring Socket'); + } +} + + +/** + * Lan.ChannelService consists of two parts: + * + * The TCP Listener listens on a port and constructs a Channel object from the + * incoming Gio.TcpConnection. + * + * The UDP Listener listens on a port for incoming JSON identity packets which + * include the TCP port, while the IP address is taken from the UDP packet + * itself. We respond by opening a TCP connection to that address. + */ +var ChannelService = GObject.registerClass({ + GTypeName: 'GSConnectLanChannelService', + Properties: { + 'certificate': GObject.ParamSpec.object( + 'certificate', + 'Certificate', + 'The TLS certificate', + GObject.ParamFlags.READWRITE, + Gio.TlsCertificate.$gtype + ), + 'port': GObject.ParamSpec.uint( + 'port', + 'Port', + 'The port used by the service', + GObject.ParamFlags.READWRITE, + 0, GLib.MAXUINT16, + DEFAULT_PORT + ), + }, +}, class LanChannelService extends Core.ChannelService { + + _init(params = {}) { + super._init(params); + + // Track hosts we identify to directly, allowing them to ignore the + // discoverable state of the service. + this._allowed = new Set(); + + // + this._tcp = null; + this._udp4 = null; + this._udp6 = null; + + // Monitor network status + this._networkMonitor = Gio.NetworkMonitor.get_default(); + this._networkAvailable = false; + this._networkChangedId = 0; + } + + get certificate() { + if (this._certificate === undefined) + this._certificate = null; + + return this._certificate; + } + + set certificate(certificate) { + if (this.certificate === certificate) + return; + + this._certificate = certificate; + this.notify('certificate'); + } + + get channels() { + if (this._channels === undefined) + this._channels = new Map(); + + return this._channels; + } + + get port() { + if (this._port === undefined) + this._port = DEFAULT_PORT; + + return this._port; + } + + set port(port) { + if (this.port === port) + return; + + this._port = port; + this.notify('port'); + } + + _onNetworkChanged(monitor, network_available) { + if (this._networkAvailable === network_available) + return; + + this._networkAvailable = network_available; + this.broadcast(); + } + + _initCertificate() { + if (GLib.find_program_in_path(Config.OPENSSL_PATH) === null) { + const error = new Error(); + error.name = _('OpenSSL not found'); + error.url = `${Config.PACKAGE_URL}/wiki/Error#openssl-not-found`; + throw error; + } + + const certPath = GLib.build_filenamev([ + Config.CONFIGDIR, + 'certificate.pem', + ]); + const keyPath = GLib.build_filenamev([ + Config.CONFIGDIR, + 'private.pem', + ]); + + // Ensure a certificate exists with our id as the common name + this._certificate = Gio.TlsCertificate.new_for_paths(certPath, keyPath, + this.id); + + // If the service ID doesn't match the common name, this is probably a + // certificate from an older version and we should amend ours to match + if (this.id !== this._certificate.common_name) + this._id = this._certificate.common_name; + } + + _initTcpListener() { + try { + this._tcp = new Gio.SocketService(); + this._tcp.add_inet_port(this.port, null); + this._tcp.connect('incoming', this._onIncomingChannel.bind(this)); + } catch (e) { + this._tcp = null; + + throw e; + } + } + + async _onIncomingChannel(listener, connection) { + try { + const host = connection.get_remote_address().address.to_string(); + + // Create a channel + const channel = new Channel({ + backend: this, + certificate: this.certificate, + host: host, + port: this.port, + }); + + // Accept the connection + await channel.accept(connection); + channel.identity.body.tcpHost = channel.host; + channel.identity.body.tcpPort = this.port; + channel.allowed = this._allowed.has(host); + + this.channel(channel); + } catch (e) { + debug(e); + } + } + + _initUdpListener() { + // Default broadcast address + this._udp_address = Gio.InetSocketAddress.new_from_string( + '255.255.255.255', + this.port + ); + + try { + this._udp6 = Gio.Socket.new( + Gio.SocketFamily.IPV6, + Gio.SocketType.DATAGRAM, + Gio.SocketProtocol.UDP + ); + this._udp6.set_broadcast(true); + + // Bind the socket + const inetAddr = Gio.InetAddress.new_any(Gio.SocketFamily.IPV6); + const sockAddr = Gio.InetSocketAddress.new(inetAddr, this.port); + this._udp6.bind(sockAddr, false); + + // Input stream + this._udp6_stream = new Gio.DataInputStream({ + base_stream: new Gio.UnixInputStream({ + fd: this._udp6.fd, + close_fd: false, + }), + }); + + // Watch socket for incoming packets + this._udp6_source = this._udp6.create_source(GLib.IOCondition.IN, null); + this._udp6_source.set_callback(this._onIncomingIdentity.bind(this, this._udp6)); + this._udp6_source.attach(null); + } catch (e) { + this._udp6 = null; + } + + // Our IPv6 socket also supports IPv4; we're all done + if (this._udp6 && this._udp6.speaks_ipv4()) { + this._udp4 = null; + return; + } + + try { + this._udp4 = Gio.Socket.new( + Gio.SocketFamily.IPV4, + Gio.SocketType.DATAGRAM, + Gio.SocketProtocol.UDP + ); + this._udp4.set_broadcast(true); + + // Bind the socket + const inetAddr = Gio.InetAddress.new_any(Gio.SocketFamily.IPV4); + const sockAddr = Gio.InetSocketAddress.new(inetAddr, this.port); + this._udp4.bind(sockAddr, false); + + // Input stream + this._udp4_stream = new Gio.DataInputStream({ + base_stream: new Gio.UnixInputStream({ + fd: this._udp4.fd, + close_fd: false, + }), + }); + + // Watch input socket for incoming packets + this._udp4_source = this._udp4.create_source(GLib.IOCondition.IN, null); + this._udp4_source.set_callback(this._onIncomingIdentity.bind(this, this._udp4)); + this._udp4_source.attach(null); + } catch (e) { + this._udp4 = null; + + // We failed to get either an IPv4 or IPv6 socket to bind + if (this._udp6 === null) + throw e; + } + } + + _onIncomingIdentity(socket) { + let host, data, packet; + + // Try to peek the remote address + try { + host = socket.receive_message( + [], + Gio.SocketMsgFlags.PEEK, + null + )[1].address.to_string(); + } catch (e) { + logError(e); + } + + // Whether or not we peeked the address, we need to read the packet + try { + if (socket === this._udp6) + data = this._udp6_stream.read_line_utf8(null)[0]; + else + data = this._udp4_stream.read_line_utf8(null)[0]; + + // Discard the packet if we failed to peek the address + if (host === undefined) + return; + + packet = new Core.Packet(data); + packet.body.tcpHost = host; + this._onIdentity(packet); + } catch (e) { + logError(e); + } + + return GLib.SOURCE_CONTINUE; + } + + async _onIdentity(packet) { + try { + // Bail if the deviceId is missing + if (!packet.body.hasOwnProperty('deviceId')) + return; + + // Silently ignore our own broadcasts + if (packet.body.deviceId === this.identity.body.deviceId) + return; + + debug(packet); + + // Create a new channel + const channel = new Channel({ + backend: this, + certificate: this.certificate, + host: packet.body.tcpHost, + port: packet.body.tcpPort, + identity: packet, + }); + + // Check if channel is already open with this address + if (this.channels.has(channel.address)) + return; + + this._channels.set(channel.address, channel); + + // Open a TCP connection + const connection = await new Promise((resolve, reject) => { + const address = Gio.InetSocketAddress.new_from_string( + packet.body.tcpHost, + packet.body.tcpPort + ); + const client = new Gio.SocketClient({enable_proxy: false}); + + client.connect_async(address, null, (client, res) => { + try { + resolve(client.connect_finish(res)); + } catch (e) { + reject(e); + } + }); + }); + + // Connect the channel and attach it to the device on success + await channel.open(connection); + + this.channel(channel); + } catch (e) { + logError(e); + } + } + + /** + * Broadcast an identity packet + * + * If @address is not %null it may specify an IPv4 or IPv6 address to send + * the identity packet directly to, otherwise it will be broadcast to the + * default address, 255.255.255.255. + * + * @param {string} [address] - An optional target IPv4 or IPv6 address + */ + broadcast(address = null) { + try { + if (!this._networkAvailable) + return; + + // Try to parse strings as : + if (typeof address === 'string') { + const [host, portstr] = address.split(':'); + const port = parseInt(portstr) || this.port; + address = Gio.InetSocketAddress.new_from_string(host, port); + } + + // If we succeed, remember this host + if (address instanceof Gio.InetSocketAddress) { + this._allowed.add(address.address.to_string()); + + // Broadcast to the network if no address is specified + } else { + debug('Broadcasting to LAN'); + address = this._udp_address; + } + + // Broadcast on each open socket + if (this._udp6 !== null) + this._udp6.send_to(address, this.identity.serialize(), null); + + if (this._udp4 !== null) + this._udp4.send_to(address, this.identity.serialize(), null); + } catch (e) { + debug(e, address); + } + } + + buildIdentity() { + // Chain-up, then add the TCP port + super.buildIdentity(); + this.identity.body.tcpPort = this.port; + } + + start() { + if (this.active) + return; + + // Ensure a certificate exists + if (this.certificate === null) + this._initCertificate(); + + // Start TCP/UDP listeners + try { + if (this._tcp === null) + this._initTcpListener(); + + if (this._udp4 === null && this._udp6 === null) + this._initUdpListener(); + } catch (e) { + // Known case of another application using the protocol defined port + if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.ADDRESS_IN_USE)) { + e.name = _('Port already in use'); + e.url = `${Config.PACKAGE_URL}/wiki/Error#port-already-in-use`; + } + + throw e; + } + + // Monitor network changes + if (this._networkChangedId === 0) { + this._networkAvailable = this._networkMonitor.network_available; + this._networkChangedId = this._networkMonitor.connect( + 'network-changed', + this._onNetworkChanged.bind(this) + ); + } + + this._active = true; + this.notify('active'); + } + + stop() { + if (this._networkChangedId) { + this._networkMonitor.disconnect(this._networkChangedId); + this._networkChangedId = 0; + this._networkAvailable = false; + } + + if (this._tcp !== null) { + this._tcp.stop(); + this._tcp.close(); + this._tcp = null; + } + + if (this._udp6 !== null) { + this._udp6_source.destroy(); + this._udp6_stream.close(null); + this._udp6.close(); + this._udp6 = null; + } + + if (this._udp4 !== null) { + this._udp4_source.destroy(); + this._udp4_stream.close(null); + this._udp4.close(); + this._udp4 = null; + } + + for (const channel of this.channels.values()) + channel.close(); + + this._active = false; + this.notify('active'); + } + + destroy() { + try { + this.stop(); + } catch (e) { + debug(e); + } + } +}); + + +/** + * Lan Channel + * + * This class essentially just extends Core.Channel to set TCP socket options + * and negotiate TLS encrypted connections. + */ +var Channel = GObject.registerClass({ + GTypeName: 'GSConnectLanChannel', +}, class LanChannel extends Core.Channel { + + _init(params) { + super._init(); + Object.assign(this, params); + } + + get address() { + return `lan://${this.host}:${this.port}`; + } + + get certificate() { + if (this._certificate === undefined) + this._certificate = null; + + return this._certificate; + } + + set certificate(certificate) { + this._certificate = certificate; + } + + get peer_certificate() { + if (this._connection instanceof Gio.TlsConnection) + return this._connection.get_peer_certificate(); + + return null; + } + + get host() { + if (this._host === undefined) + this._host = null; + + return this._host; + } + + set host(host) { + this._host = host; + } + + get port() { + if (this._port === undefined) { + if (this.identity && this.identity.body.tcpPort) + this._port = this.identity.body.tcpPort; + else + return DEFAULT_PORT; + } + + return this._port; + } + + set port(port) { + this._port = port; + } + + /** + * Handshake Gio.TlsConnection + * + * @param {Gio.TlsConnection} connection - A TLS connection + * @return {Promise} A promise for the operation + */ + _handshake(connection) { + return new Promise((resolve, reject) => { + connection.validation_flags = Gio.TlsCertificateFlags.EXPIRED; + connection.authentication_mode = Gio.TlsAuthenticationMode.REQUIRED; + + connection.handshake_async( + GLib.PRIORITY_DEFAULT, + this.cancellable, + (connection, res) => { + try { + resolve(connection.handshake_finish(res)); + } catch (e) { + reject(e); + } + } + ); + }); + } + + /** + * Authenticate a TLS connection. + * + * @param {Gio.TlsConnection} connection - A TLS connection + * @return {Promise} A promise for the operation + */ + async _authenticate(connection) { + // Standard TLS Handshake + await this._handshake(connection); + + // Get a settings object for the device + let settings; + + if (this.device) { + settings = this.device.settings; + } else { + const id = this.identity.body.deviceId; + settings = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup( + 'org.gnome.Shell.Extensions.GSConnect.Device', + true + ), + path: `/org/gnome/shell/extensions/gsconnect/device/${id}/`, + }); + } + + // If we have a certificate for this deviceId, we can verify it + const cert_pem = settings.get_string('certificate-pem'); + + if (cert_pem !== '') { + let certificate = null; + let verified = false; + + try { + certificate = Gio.TlsCertificate.new_from_pem(cert_pem, -1); + verified = certificate.is_same(connection.peer_certificate); + } catch (e) { + logError(e); + } + + /* The certificate is incorrect for one of two reasons, but both + * result in us resetting the certificate and unpairing the device. + * + * If the certificate failed to load, it is probably corrupted or + * otherwise invalid. In this case, if we try to continue we will + * certainly crash the Android app. + * + * If the certificate did not match what we expected the obvious + * thing to do is to notify the user, however experience tells us + * this is a result of the user doing something masochistic like + * nuking the Android app data or copying settings between machines. + */ + if (verified === false) { + if (this.device) { + this.device.unpair(); + } else { + settings.reset('paired'); + settings.reset('certificate-pem'); + } + + const name = this.identity.body.deviceName; + throw new Error(`${name}: Authentication Failure`); + } + } + + return connection; + } + + /** + * Wrap the connection in Gio.TlsClientConnection and initiate handshake + * + * @param {Gio.TcpConnection} connection - The unauthenticated connection + * @return {Gio.TlsClientConnection} The authenticated connection + */ + _encryptClient(connection) { + _configureSocket(connection); + + connection = Gio.TlsClientConnection.new( + connection, + connection.socket.remote_address + ); + connection.set_certificate(this.certificate); + + return this._authenticate(connection); + } + + /** + * Wrap the connection in Gio.TlsServerConnection and initiate handshake + * + * @param {Gio.TcpConnection} connection - The unauthenticated connection + * @return {Gio.TlsServerConnection} The authenticated connection + */ + _encryptServer(connection) { + _configureSocket(connection); + + connection = Gio.TlsServerConnection.new(connection, this.certificate); + + // We're the server so we trust-on-first-use and verify after + const _id = connection.connect('accept-certificate', (connection) => { + connection.disconnect(_id); + return true; + }); + + return this._authenticate(connection); + } + + /** + * Read the identity packet from the new connection + * + * @param {Gio.SocketConnection} connection - An unencrypted socket + * @return {Promise} A promise for the operation + */ + _receiveIdent(connection) { + return new Promise((resolve, reject) => { + // In principle this disposable wrapper could buffer more than the + // identity packet, but in practice the remote device shouldn't send + // any more data until the TLS connection is negotiated. + const stream = new Gio.DataInputStream({ + base_stream: connection.input_stream, + close_base_stream: false, + }); + + stream.read_line_async( + GLib.PRIORITY_DEFAULT, + this.cancellable, + (stream, res) => { + try { + const data = stream.read_line_finish_utf8(res)[0]; + stream.close(null); + + // Store the identity as an object property + this.identity = new Core.Packet(data); + + // Reject connections without a deviceId + if (!this.identity.body.deviceId) + throw new Error('missing deviceId'); + + resolve(); + } catch (e) { + reject(e); + } + } + ); + }); + } + + /** + * Write our identity packet to the new connection + * + * @param {Gio.SocketConnection} connection - An unencrypted socket + * @return {Promise} A promise for the operation + */ + _sendIdent(connection) { + return new Promise((resolve, reject) => { + connection.get_output_stream().write_all_async( + this.backend.identity.serialize(), + GLib.PRIORITY_DEFAULT, + this.cancellable, + (stream, res) => { + try { + resolve(stream.write_all_finish(res)); + } catch (e) { + reject(e); + } + } + ); + }); + } + + /** + * Negotiate an incoming connection + * + * @param {Gio.TcpConnection} connection - The incoming connection + */ + async accept(connection) { + debug(`${this.address} (${this.uuid})`); + + try { + this._connection = connection; + this.backend.channels.set(this.address, this); + + await this._receiveIdent(this._connection); + this._connection = await this._encryptClient(connection); + } catch (e) { + this.close(); + throw e; + } + } + + /** + * Negotiate an outgoing connection + * + * @param {Gio.SocketConnection} connection - The remote connection + */ + async open(connection) { + debug(`${this.address} (${this.uuid})`); + + try { + this._connection = connection; + this.backend.channels.set(this.address, this); + + await this._sendIdent(this._connection); + this._connection = await this._encryptServer(connection); + } catch (e) { + this.close(); + throw e; + } + } + + /** + * Close all streams associated with this channel, silencing any errors + */ + close() { + if (this.closed) + return; + + debug(`${this.address} (${this.uuid})`); + this._closed = true; + this.notify('closed'); + + this.backend.channels.delete(this.address); + this.cancellable.cancel(); + + if (this._connection) + this._connection.close_async(GLib.PRIORITY_DEFAULT, null, null); + + if (this.input_stream) + this.input_stream.close_async(GLib.PRIORITY_DEFAULT, null, null); + + if (this.output_stream) + this.output_stream.close_async(GLib.PRIORITY_DEFAULT, null, null); + } + + async download(packet, target, cancellable = null) { + const openConnection = new Promise((resolve, reject) => { + const client = new Gio.SocketClient({enable_proxy: false}); + + const address = Gio.InetSocketAddress.new_from_string( + this.host, + packet.payloadTransferInfo.port + ); + + client.connect_async(address, cancellable, (client, res) => { + try { + resolve(client.connect_finish(res)); + } catch (e) { + reject(e); + } + }); + }); + + let connection = await openConnection; + connection = await this._encryptClient(connection); + const source = connection.get_input_stream(); + + // Start the transfer + const transferredSize = await this._transfer(source, target, cancellable); + + // If we get less than expected, we've certainly got corruption + if (transferredSize < packet.payloadSize) { + throw new Gio.IOErrorEnum({ + code: Gio.IOErrorEnum.FAILED, + message: `Incomplete: ${transferredSize}/${packet.payloadSize}`, + }); + + // TODO: sometimes kdeconnect-android under-reports a file's size + // https://github.com/GSConnect/gnome-shell-extension-gsconnect/issues/1157 + } else if (transferredSize > packet.payloadSize) { + logError(new Gio.IOErrorEnum({ + code: Gio.IOErrorEnum.FAILED, + message: `Extra Data: ${transferredSize - packet.payloadSize}`, + })); + } + } + + async upload(packet, source, size, cancellable = null) { + // Start listening on the first available port between 1739-1764 + const listener = new Gio.SocketListener(); + let port = TRANSFER_MIN; + + while (port <= TRANSFER_MAX) { + try { + listener.add_inet_port(port, null); + break; + } catch (e) { + if (port < TRANSFER_MAX) { + port++; + continue; + } else { + throw e; + } + } + } + + // Listen for the incoming connection + const acceptConnection = new Promise((resolve, reject) => { + listener.accept_async( + cancellable, + (listener, res, source_object) => { + try { + resolve(listener.accept_finish(res)[0]); + } catch (e) { + reject(e); + } + } + ); + }); + + // Notify the device we're ready + packet.body.payloadHash = this.checksum; + packet.payloadSize = size; + packet.payloadTransferInfo = {port: port}; + this.sendPacket(new Core.Packet(packet)); + + // Accept the connection and configure the channel + let connection = await acceptConnection; + connection = await this._encryptServer(connection); + const target = connection.get_output_stream(); + + // Start the transfer + const transferredSize = await this._transfer(source, target, cancellable); + + if (transferredSize !== size) { + throw new Gio.IOErrorEnum({ + code: Gio.IOErrorEnum.PARTIAL_INPUT, + message: 'Transfer incomplete', + }); + } + } + + _transfer(source, target, cancellable) { + return new Promise((resolve, reject) => { + target.splice_async( + source, + (Gio.OutputStreamSpliceFlags.CLOSE_SOURCE | + Gio.OutputStreamSpliceFlags.CLOSE_TARGET), + GLib.PRIORITY_DEFAULT, + cancellable, + (target, res) => { + try { + resolve(target.splice_finish(res)); + } catch (e) { + reject(e); + } + } + ); + }); + } + + async rejectTransfer(packet) { + try { + if (!packet || !packet.hasPayload()) + return; + + if (packet.payloadTransferInfo.port === undefined) + return; + + let connection = await new Promise((resolve, reject) => { + const client = new Gio.SocketClient({enable_proxy: false}); + + const address = Gio.InetSocketAddress.new_from_string( + this.host, + packet.payloadTransferInfo.port + ); + + client.connect_async(address, null, (client, res) => { + try { + resolve(client.connect_finish(res)); + } catch (e) { + resolve(); + } + }); + }); + + connection = await this._encryptClient(connection); + connection.close_async(GLib.PRIORITY_DEFAULT, null, null); + } catch (e) { + debug(e, this.device.name); + } + } +}); + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/__init__.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/__init__.js new file mode 100644 index 0000000..2aa5e61 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/__init__.js @@ -0,0 +1,67 @@ +'use strict'; + + +/* + * Singleton Tracker + */ +const Default = new Map(); + + +/** + * Acquire a reference to a component. Calls to this function should always be + * followed by a call to `release()`. + * + * @param {string} name - The module name + * @return {*} The default instance of a component + */ +function acquire(name) { + let component; + + try { + let info = Default.get(name); + + if (info === undefined) { + const module = imports.service.components[name]; + + info = { + instance: new module.Component(), + refcount: 0, + }; + + Default.set(name, info); + } + + info.refcount++; + component = info.instance; + } catch (e) { + debug(e, name); + } + + return component; +} + + +/** + * Release a reference on a component. If the caller was the last reference + * holder, the component will be freed. + * + * @param {string} name - The module name + * @return {null} A %null value, useful for overriding a traced variable + */ +function release(name) { + try { + const info = Default.get(name); + + if (info.refcount === 1) { + info.instance.destroy(); + Default.delete(name); + } + + info.refcount--; + } catch (e) { + debug(e, name); + } + + return null; +} + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/atspi.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/atspi.js new file mode 100644 index 0000000..6ddd180 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/atspi.js @@ -0,0 +1,312 @@ +'use strict'; + +imports.gi.versions.Atspi = '2.0'; + +const Atspi = imports.gi.Atspi; +const Gdk = imports.gi.Gdk; + + +/** + * Printable ASCII range + */ +const _ASCII = /[\x20-\x7E]/; + + +/** + * Modifier Keycode Defaults + */ +const XKeycode = { + Alt_L: 0x40, + Control_L: 0x25, + Shift_L: 0x32, + Super_L: 0x85, +}; + + +/** + * A thin wrapper around Atspi for X11 sessions without Pipewire support. + */ +var Controller = class { + constructor() { + // Atspi.init() return 2 on fail, but still marks itself as inited. We + // uninit before throwing an error otherwise any future call to init() + // will appear successful and other calls will cause GSConnect to exit. + // See: https://gitlab.gnome.org/GNOME/at-spi2-core/blob/master/atspi/atspi-misc.c + if (Atspi.init() === 2) { + this.destroy(); + throw new Error('Failed to start AT-SPI'); + } + + try { + this._display = Gdk.Display.get_default(); + this._seat = this._display.get_default_seat(); + this._pointer = this._seat.get_pointer(); + } catch (e) { + this.destroy(); + throw e; + } + + // Try to read modifier keycodes from Gdk + try { + const keymap = Gdk.Keymap.get_for_display(this._display); + let modifier; + + modifier = keymap.get_entries_for_keyval(Gdk.KEY_Alt_L)[1][0]; + XKeycode.Alt_L = modifier.keycode; + + modifier = keymap.get_entries_for_keyval(Gdk.KEY_Control_L)[1][0]; + XKeycode.Control_L = modifier.keycode; + + modifier = keymap.get_entries_for_keyval(Gdk.KEY_Shift_L)[1][0]; + XKeycode.Shift_L = modifier.keycode; + + modifier = keymap.get_entries_for_keyval(Gdk.KEY_Super_L)[1][0]; + XKeycode.Super_L = modifier.keycode; + } catch (e) { + debug('using default modifier keycodes'); + } + } + + /* + * Pointer events + */ + clickPointer(button) { + try { + const [, x, y] = this._pointer.get_position(); + const monitor = this._display.get_monitor_at_point(x, y); + const scale = monitor.get_scale_factor(); + Atspi.generate_mouse_event(scale * x, scale * y, `b${button}c`); + } catch (e) { + logError(e); + } + } + + doubleclickPointer(button) { + try { + const [, x, y] = this._pointer.get_position(); + const monitor = this._display.get_monitor_at_point(x, y); + const scale = monitor.get_scale_factor(); + Atspi.generate_mouse_event(scale * x, scale * y, `b${button}d`); + } catch (e) { + logError(e); + } + } + + movePointer(dx, dy) { + try { + const [, x, y] = this._pointer.get_position(); + const monitor = this._display.get_monitor_at_point(x, y); + const scale = monitor.get_scale_factor(); + Atspi.generate_mouse_event(scale * dx, scale * dy, 'rel'); + } catch (e) { + logError(e); + } + } + + pressPointer(button) { + try { + const [, x, y] = this._pointer.get_position(); + const monitor = this._display.get_monitor_at_point(x, y); + const scale = monitor.get_scale_factor(); + Atspi.generate_mouse_event(scale * x, scale * y, `b${button}p`); + } catch (e) { + logError(e); + } + } + + releasePointer(button) { + try { + const [, x, y] = this._pointer.get_position(); + const monitor = this._display.get_monitor_at_point(x, y); + const scale = monitor.get_scale_factor(); + Atspi.generate_mouse_event(scale * x, scale * y, `b${button}r`); + } catch (e) { + logError(e); + } + } + + scrollPointer(dx, dy) { + if (dy > 0) + this.clickPointer(4); + else if (dy < 0) + this.clickPointer(5); + } + + /* + * Phony virtual keyboard helpers + */ + _modeLock(keycode) { + Atspi.generate_keyboard_event( + keycode, + null, + Atspi.KeySynthType.PRESS + ); + } + + _modeUnlock(keycode) { + Atspi.generate_keyboard_event( + keycode, + null, + Atspi.KeySynthType.RELEASE + ); + } + + /* + * Simulate a printable-ASCII character. + * + */ + _pressASCII(key, modifiers) { + try { + // Press Modifiers + if (modifiers & Gdk.ModifierType.MOD1_MASK) + this._modeLock(XKeycode.Alt_L); + if (modifiers & Gdk.ModifierType.CONTROL_MASK) + this._modeLock(XKeycode.Control_L); + if (modifiers & Gdk.ModifierType.SHIFT_MASK) + this._modeLock(XKeycode.Shift_L); + if (modifiers & Gdk.ModifierType.SUPER_MASK) + this._modeLock(XKeycode.Super_L); + + Atspi.generate_keyboard_event( + 0, + key, + Atspi.KeySynthType.STRING + ); + + // Release Modifiers + if (modifiers & Gdk.ModifierType.MOD1_MASK) + this._modeUnlock(XKeycode.Alt_L); + if (modifiers & Gdk.ModifierType.CONTROL_MASK) + this._modeUnlock(XKeycode.Control_L); + if (modifiers & Gdk.ModifierType.SHIFT_MASK) + this._modeUnlock(XKeycode.Shift_L); + if (modifiers & Gdk.ModifierType.SUPER_MASK) + this._modeUnlock(XKeycode.Super_L); + } catch (e) { + logError(e); + } + } + + _pressKeysym(keysym, modifiers) { + try { + // Press Modifiers + if (modifiers & Gdk.ModifierType.MOD1_MASK) + this._modeLock(XKeycode.Alt_L); + if (modifiers & Gdk.ModifierType.CONTROL_MASK) + this._modeLock(XKeycode.Control_L); + if (modifiers & Gdk.ModifierType.SHIFT_MASK) + this._modeLock(XKeycode.Shift_L); + if (modifiers & Gdk.ModifierType.SUPER_MASK) + this._modeLock(XKeycode.Super_L); + + Atspi.generate_keyboard_event( + keysym, + null, + Atspi.KeySynthType.PRESSRELEASE | Atspi.KeySynthType.SYM + ); + + // Release Modifiers + if (modifiers & Gdk.ModifierType.MOD1_MASK) + this._modeUnlock(XKeycode.Alt_L); + if (modifiers & Gdk.ModifierType.CONTROL_MASK) + this._modeUnlock(XKeycode.Control_L); + if (modifiers & Gdk.ModifierType.SHIFT_MASK) + this._modeUnlock(XKeycode.Shift_L); + if (modifiers & Gdk.ModifierType.SUPER_MASK) + this._modeUnlock(XKeycode.Super_L); + } catch (e) { + logError(e); + } + } + + /** + * Simulate the composition of a unicode character with: + * Control+Shift+u, [hex], Return + * + * @param {number} key - An XKeycode + * @param {number} modifiers - A modifier mask + */ + _pressUnicode(key, modifiers) { + try { + if (modifiers > 0) + log('GSConnect: ignoring modifiers for unicode keyboard event'); + + // TODO: Using Control and Shift keysym is not working (it triggers + // key release). Probably using LOCKMODIFIERS will not work either + // as unlocking the modifier will not trigger a release + + // Activate compose sequence + this._modeLock(XKeycode.Control_L); + this._modeLock(XKeycode.Shift_L); + + this.pressreleaseKeysym(Gdk.KEY_U); + + this._modeUnlock(XKeycode.Control_L); + this._modeUnlock(XKeycode.Shift_L); + + // Enter the unicode sequence + const ucode = key.charCodeAt(0).toString(16); + let keysym; + + for (let h = 0, len = ucode.length; h < len; h++) { + keysym = Gdk.unicode_to_keyval(ucode.charAt(h).codePointAt(0)); + this.pressreleaseKeysym(keysym); + } + + // Finish the compose sequence + this.pressreleaseKeysym(Gdk.KEY_Return); + } catch (e) { + logError(e); + } + } + + /* + * Keyboard Events + */ + pressKeysym(keysym) { + Atspi.generate_keyboard_event( + keysym, + null, + Atspi.KeySynthType.PRESS | Atspi.KeySynthType.SYM + ); + } + + releaseKeysym(keysym) { + Atspi.generate_keyboard_event( + keysym, + null, + Atspi.KeySynthType.RELEASE | Atspi.KeySynthType.SYM + ); + } + + pressreleaseKeysym(keysym) { + Atspi.generate_keyboard_event( + keysym, + null, + Atspi.KeySynthType.PRESSRELEASE | Atspi.KeySynthType.SYM + ); + } + + pressKey(input, modifiers) { + // We were passed a keysym + if (typeof input === 'number') + this._pressKeysym(input, modifiers); + + // Regular ASCII + else if (_ASCII.test(input)) + this._pressASCII(input, modifiers); + + // Unicode + else + this._pressUnicode(input, modifiers); + } + + destroy() { + try { + Atspi.exit(); + } catch (e) { + // Silence errors + } + } +}; + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/clipboard.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/clipboard.js new file mode 100644 index 0000000..fab6fda --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/clipboard.js @@ -0,0 +1,283 @@ +'use strict'; + +const Gdk = imports.gi.Gdk; +const GLib = imports.gi.GLib; +const Gtk = imports.gi.Gtk; +const Gio = imports.gi.Gio; +const GObject = imports.gi.GObject; + + +const DBUS_NAME = 'org.gnome.Shell.Extensions.GSConnect.Clipboard'; +const DBUS_PATH = '/org/gnome/Shell/Extensions/GSConnect/Clipboard'; + + +var Clipboard = GObject.registerClass({ + GTypeName: 'GSConnectClipboard', + Properties: { + 'text': GObject.ParamSpec.string( + 'text', + 'Text Content', + 'The current text content of the clipboard', + GObject.ParamFlags.READWRITE, + '' + ), + }, +}, class Clipboard extends GObject.Object { + + _init() { + super._init(); + + this._cancellable = new Gio.Cancellable(); + this._clipboard = null; + + this._ownerChangeId = 0; + this._nameWatcherId = Gio.bus_watch_name( + Gio.BusType.SESSION, + DBUS_NAME, + Gio.BusNameWatcherFlags.NONE, + this._onNameAppeared.bind(this), + this._onNameVanished.bind(this) + ); + } + + get text() { + if (this._text === undefined) + this._text = ''; + + return this._text; + } + + set text(content) { + if (this.text === content) + return; + + this._text = content; + this.notify('text'); + + if (typeof content !== 'string') + return; + + if (this._clipboard instanceof Gtk.Clipboard) + this._clipboard.set_text(content, -1); + + if (this._clipboard instanceof Gio.DBusProxy) + this._proxySetText(content); + } + + async _onNameAppeared(connection, name, name_owner) { + try { + // Cleanup the GtkClipboard + if (this._clipboard && this._ownerChangeId > 0) { + this._clipboard.disconnect(this._ownerChangeId); + this._ownerChangeId = 0; + } + + // Create a proxy for the remote clipboard + this._clipboard = new Gio.DBusProxy({ + g_bus_type: Gio.BusType.SESSION, + g_name: DBUS_NAME, + g_object_path: DBUS_PATH, + g_interface_name: DBUS_NAME, + g_flags: Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES, + }); + + await new Promise((resolve, reject) => { + this._clipboard.init_async( + GLib.PRIORITY_DEFAULT, + this._cancellable, + (proxy, res) => { + try { + resolve(proxy.init_finish(res)); + } catch (e) { + reject(e); + } + } + ); + }); + + this._ownerChangeId = this._clipboard.connect( + 'g-signal', + this._onOwnerChange.bind(this) + ); + + this._onOwnerChange(); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { + debug(e); + this._onNameVanished(null, null); + } + } + } + + _onNameVanished(connection, name) { + if (this._clipboard && this._ownerChangeId > 0) { + this._clipboard.disconnect(this._ownerChangeId); + this._clipboardChangedId = 0; + } + + const display = Gdk.Display.get_default(); + this._clipboard = Gtk.Clipboard.get_default(display); + + this._ownerChangeId = this._clipboard.connect( + 'owner-change', + this._onOwnerChange.bind(this) + ); + + this._onOwnerChange(); + } + + async _onOwnerChange() { + try { + if (this._clipboard instanceof Gtk.Clipboard) + await this._gtkUpdateText(); + + else if (this._clipboard instanceof Gio.DBusProxy) + await this._proxyUpdateText(); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + debug(e); + } + } + + _applyUpdate(text) { + if (typeof text !== 'string' || this.text === text) + return; + + this._text = text; + this.notify('text'); + } + + /* + * Proxy Clipboard + */ + _proxyGetMimetypes() { + return new Promise((resolve, reject) => { + this._clipboard.call( + 'GetMimetypes', + null, + Gio.DBusCallFlags.NO_AUTO_START, + -1, + this._cancellable, + (proxy, res) => { + try { + const reply = proxy.call_finish(res); + resolve(reply.deepUnpack()[0]); + } catch (e) { + Gio.DBusError.strip_remote_error(e); + reject(e); + } + } + ); + }); + } + + _proxyGetText() { + return new Promise((resolve, reject) => { + this._clipboard.call( + 'GetText', + null, + Gio.DBusCallFlags.NO_AUTO_START, + -1, + this._cancellable, + (proxy, res) => { + try { + const reply = proxy.call_finish(res); + resolve(reply.deepUnpack()[0]); + } catch (e) { + Gio.DBusError.strip_remote_error(e); + reject(e); + } + } + ); + }); + } + + _proxySetText(text) { + this._clipboard.call( + 'SetText', + new GLib.Variant('(s)', [text]), + Gio.DBusCallFlags.NO_AUTO_START, + -1, + this._cancellable, + (proxy, res) => { + try { + proxy.call_finish(res); + } catch (e) { + Gio.DBusError.strip_remote_error(e); + debug(e); + } + } + ); + } + + async _proxyUpdateText() { + const mimetypes = await this._proxyGetMimetypes(); + + // Special case for a cleared clipboard + if (mimetypes.length === 0) + return this._applyUpdate(''); + + // Special case to ignore copied files + if (mimetypes.includes('text/uri-list')) + return; + + const text = await this._proxyGetText(); + + this._applyUpdate(text); + } + + /* + * GtkClipboard + */ + _gtkGetMimetypes() { + return new Promise((resolve, reject) => { + this._clipboard.request_targets((clipboard, atoms) => resolve(atoms)); + }); + } + + _gtkGetText() { + return new Promise((resolve, reject) => { + this._clipboard.request_text((clipboard, text) => resolve(text)); + }); + } + + async _gtkUpdateText() { + const mimetypes = await this._gtkGetMimetypes(); + + // Special case for a cleared clipboard + if (mimetypes.length === 0) + return this._applyUpdate(''); + + // Special case to ignore copied files + if (mimetypes.includes('text/uri-list')) + return; + + const text = await this._gtkGetText(); + + this._applyUpdate(text); + } + + destroy() { + if (this._cancellable.is_cancelled()) + return; + + this._cancellable.cancel(); + + if (this._clipboard && this._ownerChangeId > 0) { + this._clipboard.disconnect(this._ownerChangeId); + this._ownerChangedId = 0; + } + + if (this._nameWatcherId > 0) { + Gio.bus_unwatch_name(this._nameWatcherId); + this._nameWatcherId = 0; + } + } +}); + + +/** + * The service class for this component + */ +var Component = Clipboard; + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/contacts.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/contacts.js new file mode 100644 index 0000000..a76898d --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/contacts.js @@ -0,0 +1,703 @@ +'use strict'; + +const ByteArray = imports.byteArray; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Config = imports.config; + +var HAVE_EDS = true; +var EBook = null; +var EBookContacts = null; +var EDataServer = null; + +try { + EBook = imports.gi.EBook; + EBookContacts = imports.gi.EBookContacts; + EDataServer = imports.gi.EDataServer; +} catch (e) { + HAVE_EDS = false; +} + + +/** + * A store for contacts + */ +var Store = GObject.registerClass({ + GTypeName: 'GSConnectContactsStore', + Properties: { + 'context': GObject.ParamSpec.string( + 'context', + 'Context', + 'Used as the cache directory, relative to Config.CACHEDIR', + GObject.ParamFlags.CONSTRUCT_ONLY | GObject.ParamFlags.READWRITE, + null + ), + }, + Signals: { + 'contact-added': { + flags: GObject.SignalFlags.RUN_FIRST, + param_types: [GObject.TYPE_STRING], + }, + 'contact-removed': { + flags: GObject.SignalFlags.RUN_FIRST, + param_types: [GObject.TYPE_STRING], + }, + 'contact-changed': { + flags: GObject.SignalFlags.RUN_FIRST, + param_types: [GObject.TYPE_STRING], + }, + }, +}, class Store extends GObject.Object { + + _init(context = null) { + super._init({ + context: context, + }); + + this._cacheData = {}; + this._edsPrepared = false; + } + + /** + * Parse an EContact and add it to the store. + * + * @param {EBookContacts.Contact} econtact - an EContact to parse + * @param {string} [origin] - an optional origin string + */ + async _parseEContact(econtact, origin = 'desktop') { + try { + const contact = { + id: econtact.id, + name: _('Unknown Contact'), + numbers: [], + origin: origin, + timestamp: 0, + }; + + // Try to get a contact name + if (econtact.full_name) + contact.name = econtact.full_name; + + // Parse phone numbers + const nums = econtact.get_attributes(EBookContacts.ContactField.TEL); + + for (const attr of nums) { + const number = { + value: attr.get_value(), + type: 'unknown', + }; + + if (attr.has_type('CELL')) + number.type = 'cell'; + else if (attr.has_type('HOME')) + number.type = 'home'; + else if (attr.has_type('WORK')) + number.type = 'work'; + + contact.numbers.push(number); + } + + // Try and get a contact photo + const photo = econtact.photo; + + if (photo) { + if (photo.type === EBookContacts.ContactPhotoType.INLINED) { + const data = photo.get_inlined()[0]; + contact.avatar = await this.storeAvatar(data); + + } else if (photo.type === EBookContacts.ContactPhotoType.URI) { + const uri = econtact.photo.get_uri(); + contact.avatar = uri.replace('file://', ''); + } + } + + this.add(contact, false); + } catch (e) { + logError(e, `Failed to parse VCard contact ${econtact.id}`); + } + } + + /* + * EDS Helpers + */ + _getEBookClient(source, cancellable = null) { + return new Promise((resolve, reject) => { + EBook.BookClient.connect(source, 0, cancellable, (source, res) => { + try { + resolve(EBook.BookClient.connect_finish(res)); + } catch (e) { + reject(e); + } + }); + }); + } + + _getEBookView(client, query = '', cancellable = null) { + return new Promise((resolve, reject) => { + client.get_view(query, cancellable, (client, res) => { + try { + resolve(client.get_view_finish(res)[1]); + } catch (e) { + reject(e); + } + }); + }); + } + + _getEContacts(client, query = '', cancellable = null) { + return new Promise((resolve, reject) => { + client.get_contacts(query, cancellable, (client, res) => { + try { + resolve(client.get_contacts_finish(res)[1]); + } catch (e) { + debug(e); + resolve([]); + } + }); + }); + } + + _getESourceRegistry(cancellable = null) { + return new Promise((resolve, reject) => { + EDataServer.SourceRegistry.new(cancellable, (registry, res) => { + try { + resolve(EDataServer.SourceRegistry.new_finish(res)); + } catch (e) { + reject(e); + } + }); + }); + } + + /* + * AddressBook DBus callbacks + */ + _onObjectsAdded(connection, sender, path, iface, signal, params) { + try { + const adds = params.get_child_value(0).get_strv(); + + // NOTE: sequential pairs of vcard, id + for (let i = 0, len = adds.length; i < len; i += 2) { + try { + const vcard = adds[i]; + const econtact = EBookContacts.Contact.new_from_vcard(vcard); + this._parseEContact(econtact); + } catch (e) { + debug(e); + } + } + } catch (e) { + debug(e); + } + } + + _onObjectsRemoved(connection, sender, path, iface, signal, params) { + try { + const changes = params.get_child_value(0).get_strv(); + + for (const id of changes) { + try { + this.remove(id, false); + } catch (e) { + debug(e); + } + } + } catch (e) { + debug(e); + } + } + + _onObjectsModified(connection, sender, path, iface, signal, params) { + try { + const changes = params.get_child_value(0).get_strv(); + + // NOTE: sequential pairs of vcard, id + for (let i = 0, len = changes.length; i < len; i += 2) { + try { + const vcard = changes[i]; + const econtact = EBookContacts.Contact.new_from_vcard(vcard); + this._parseEContact(econtact); + } catch (e) { + debug(e); + } + } + } catch (e) { + debug(e); + } + } + + /* + * SourceRegistryWatcher callbacks + */ + async _onAppeared(watcher, source) { + try { + // Get an EBookClient and EBookView + const uid = source.get_uid(); + const client = await this._getEBookClient(source); + const view = await this._getEBookView(client, 'exists "tel"'); + + // Watch the view for changes to the address book + const connection = view.get_connection(); + const objectPath = view.get_object_path(); + + view._objectsAddedId = connection.signal_subscribe( + null, + 'org.gnome.evolution.dataserver.AddressBookView', + 'ObjectsAdded', + objectPath, + null, + Gio.DBusSignalFlags.NONE, + this._onObjectsAdded.bind(this) + ); + + view._objectsRemovedId = connection.signal_subscribe( + null, + 'org.gnome.evolution.dataserver.AddressBookView', + 'ObjectsRemoved', + objectPath, + null, + Gio.DBusSignalFlags.NONE, + this._onObjectsRemoved.bind(this) + ); + + view._objectsModifiedId = connection.signal_subscribe( + null, + 'org.gnome.evolution.dataserver.AddressBookView', + 'ObjectsModified', + objectPath, + null, + Gio.DBusSignalFlags.NONE, + this._onObjectsModified.bind(this) + ); + + view.start(); + + // Store the EBook in a map + this._ebooks.set(uid, { + source: source, + client: client, + view: view, + }); + } catch (e) { + debug(e); + } + } + + _onDisappeared(watcher, source) { + try { + const uid = source.get_uid(); + const ebook = this._ebooks.get(uid); + + if (ebook === undefined) + return; + + // Disconnect the EBookView + if (ebook.view) { + const connection = ebook.view.get_connection(); + connection.signal_unsubscribe(ebook.view._objectsAddedId); + connection.signal_unsubscribe(ebook.view._objectsRemovedId); + connection.signal_unsubscribe(ebook.view._objectsModifiedId); + + ebook.view.stop(); + } + + this._ebooks.delete(uid); + } catch (e) { + debug(e); + } + } + + async _initEvolutionDataServer() { + try { + if (this._edsPrepared) + return; + + this._edsPrepared = true; + this._ebooks = new Map(); + + // Get the current EBooks + const registry = await this._getESourceRegistry(); + + for (const source of registry.list_sources('Address Book')) + await this._onAppeared(null, source); + + // Watch for new and removed sources + this._watcher = new EDataServer.SourceRegistryWatcher({ + registry: registry, + extension_name: 'Address Book', + }); + + this._appearedId = this._watcher.connect( + 'appeared', + this._onAppeared.bind(this) + ); + this._disappearedId = this._watcher.connect( + 'disappeared', + this._onDisappeared.bind(this) + ); + } catch (e) { + const service = Gio.Application.get_default(); + + if (service !== null) + service.notify_error(e); + else + logError(e); + } + } + + *[Symbol.iterator]() { + const contacts = Object.values(this._cacheData); + + for (let i = 0, len = contacts.length; i < len; i++) + yield contacts[i]; + } + + get contacts() { + return Object.values(this._cacheData); + } + + get context() { + if (this._context === undefined) + this._context = null; + + return this._context; + } + + set context(context) { + this._context = context; + this._cacheDir = Gio.File.new_for_path(Config.CACHEDIR); + + if (context !== null) + this._cacheDir = this._cacheDir.get_child(context); + + GLib.mkdir_with_parents(this._cacheDir.get_path(), 448); + this._cacheFile = this._cacheDir.get_child('contacts.json'); + } + + /** + * Save a ByteArray to file and return the path + * + * @param {ByteArray} contents - An image ByteArray + * @return {string|undefined} File path or %undefined on failure + */ + storeAvatar(contents) { + return new Promise((resolve, reject) => { + const md5 = GLib.compute_checksum_for_data( + GLib.ChecksumType.MD5, + contents + ); + const file = this._cacheDir.get_child(`${md5}`); + + if (file.query_exists(null)) { + resolve(file.get_path()); + } else { + file.replace_contents_bytes_async( + new GLib.Bytes(contents), + null, + false, + Gio.FileCreateFlags.REPLACE_DESTINATION, + null, + (file, res) => { + try { + file.replace_contents_finish(res); + resolve(file.get_path()); + } catch (e) { + debug(e, 'Storing avatar'); + resolve(undefined); + } + } + ); + } + }); + } + + /** + * Query the Store for a contact by name and/or number. + * + * @param {Object} query - A query object + * @param {string} [query.name] - The contact's name + * @param {string} query.number - The contact's number + * @return {Object} A contact object + */ + query(query) { + // First look for an existing contact by number + const contacts = this.contacts; + const matches = []; + const qnumber = query.number.toPhoneNumber(); + + for (let i = 0, len = contacts.length; i < len; i++) { + const contact = contacts[i]; + + for (const num of contact.numbers) { + const cnumber = num.value.toPhoneNumber(); + + if (qnumber.endsWith(cnumber) || cnumber.endsWith(qnumber)) { + // If no query name or exact match, return immediately + if (!query.name || query.name === contact.name) + return contact; + + // Otherwise we might find an exact name match that shares + // the number with another contact + matches.push(contact); + } + } + } + + // Return the first match (pretty much what Android does) + if (matches.length > 0) + return matches[0]; + + // No match; return a mock contact with a unique ID + let id = GLib.uuid_string_random(); + + while (this._cacheData.hasOwnProperty(id)) + id = GLib.uuid_string_random(); + + return { + id: id, + name: query.name || query.number, + numbers: [{value: query.number, type: 'unknown'}], + origin: 'gsconnect', + }; + } + + get_contact(position) { + if (this._cacheData[position] !== undefined) + return this._cacheData[position]; + + return null; + } + + /** + * Add a contact, checking for validity + * + * @param {Object} contact - A contact object + * @param {boolean} write - Write to disk + */ + add(contact, write = true) { + // Ensure the contact has a unique id + if (!contact.id) { + let id = GLib.uuid_string_random(); + + while (this._cacheData[id]) + id = GLib.uuid_string_random(); + + contact.id = id; + } + + // Ensure the contact has an origin + if (!contact.origin) + contact.origin = 'gsconnect'; + + // This is an updated contact + if (this._cacheData[contact.id]) { + this._cacheData[contact.id] = contact; + this.emit('contact-changed', contact.id); + + // This is a new contact + } else { + this._cacheData[contact.id] = contact; + this.emit('contact-added', contact.id); + } + + // Write if requested + if (write) + this.save(); + } + + /** + * Remove a contact by id + * + * @param {string} id - The id of the contact to delete + * @param {boolean} write - Write to disk + */ + remove(id, write = true) { + // Only remove if the contact actually exists + if (this._cacheData[id]) { + delete this._cacheData[id]; + this.emit('contact-removed', id); + + // Write if requested + if (write) + this.save(); + } + } + + /** + * Lookup a contact for each address object in @addresses and return a + * dictionary of address (eg. phone number) to contact object. + * + * { "555-5555": { "name": "...", "numbers": [], ... } } + * + * @param {Object[]} addresses - A list of address objects + * @return {Object} A dictionary of phone numbers and contacts + */ + lookupAddresses(addresses) { + const contacts = {}; + + // Lookup contacts for each address + for (let i = 0, len = addresses.length; i < len; i++) { + const address = addresses[i].address; + + contacts[address] = this.query({ + number: address, + }); + } + + return contacts; + } + + async clear() { + try { + const contacts = this.contacts; + + for (let i = 0, len = contacts.length; i < len; i++) + await this.remove(contacts[i].id, false); + + await this.save(); + } catch (e) { + debug(e); + } + } + + /** + * Update the contact store from a dictionary of our custom contact objects. + * + * @param {Object} json - an Object of contact Objects + */ + async update(json = {}) { + try { + let contacts = Object.values(json); + + for (let i = 0, len = contacts.length; i < len; i++) { + const new_contact = contacts[i]; + const contact = this._cacheData[new_contact.id]; + + if (!contact || new_contact.timestamp !== contact.timestamp) + await this.add(new_contact, false); + } + + // Prune contacts + contacts = this.contacts; + + for (let i = 0, len = contacts.length; i < len; i++) { + const contact = contacts[i]; + + if (!json[contact.id]) + await this.remove(contact.id, false); + } + + await this.save(); + } catch (e) { + debug(e, 'Updating contacts'); + } + } + + /** + * Fetch and update the contact store from its source. + * + * The default function initializes the EDS server, or logs a debug message + * if EDS is unavailable. Derived classes should request an update from the + * remote source. + */ + async fetch() { + try { + if (this.context === null && HAVE_EDS) + await this._initEvolutionDataServer(); + else + throw new Error('Evolution Data Server not available'); + } catch (e) { + debug(e); + } + } + + /** + * Load the contacts from disk. + */ + async load() { + try { + this._cacheData = await new Promise((resolve, reject) => { + this._cacheFile.load_contents_async(null, (file, res) => { + try { + const contents = file.load_contents_finish(res)[1]; + + resolve(JSON.parse(ByteArray.toString(contents))); + } catch (e) { + reject(e); + } + }); + }); + } catch (e) { + debug(e); + } finally { + this.notify('context'); + } + } + + /** + * Save the contacts to disk. + */ + async save() { + // EDS is handling storage + if (this.context === null && HAVE_EDS) + return; + + if (this.__cache_lock) { + this.__cache_queue = true; + return; + } + + try { + this.__cache_lock = true; + + await new Promise((resolve, reject) => { + this._cacheFile.replace_contents_bytes_async( + new GLib.Bytes(JSON.stringify(this._cacheData, null, 2)), + null, + false, + Gio.FileCreateFlags.REPLACE_DESTINATION, + null, + (file, res) => { + try { + resolve(file.replace_contents_finish(res)); + } catch (e) { + reject(e); + } + } + ); + }); + } catch (e) { + debug(e); + } finally { + this.__cache_lock = false; + + if (this.__cache_queue) { + this.__cache_queue = false; + this.save(); + } + } + } + + destroy() { + if (this._watcher !== undefined) { + this._watcher.disconnect(this._appearedId); + this._watcher.disconnect(this._disappearedId); + this._watcher = undefined; + + for (const ebook of this._ebooks.values()) + this._onDisappeared(null, ebook.source); + + this._edsPrepared = false; + } + } +}); + + +/** + * The service class for this component + */ +var Component = Store; + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/input.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/input.js new file mode 100644 index 0000000..2ee1d7a --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/input.js @@ -0,0 +1,641 @@ +'use strict'; + +const Gdk = imports.gi.Gdk; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + + +const SESSION_TIMEOUT = 15; + + +const RemoteSession = GObject.registerClass({ + GTypeName: 'GSConnectRemoteSession', + Implements: [Gio.DBusInterface], + Signals: { + 'closed': { + flags: GObject.SignalFlags.RUN_FIRST, + }, + }, +}, class RemoteSession extends Gio.DBusProxy { + + _init(objectPath) { + super._init({ + g_bus_type: Gio.BusType.SESSION, + g_name: 'org.gnome.Mutter.RemoteDesktop', + g_object_path: objectPath, + g_interface_name: 'org.gnome.Mutter.RemoteDesktop.Session', + g_flags: Gio.DBusProxyFlags.NONE, + }); + + this._started = false; + } + + vfunc_g_signal(sender_name, signal_name, parameters) { + if (signal_name === 'Closed') + this.emit('closed'); + } + + _call(name, parameters = null) { + if (!this._started) + return; + + this.call(name, parameters, Gio.DBusCallFlags.NONE, -1, null, null); + } + + get session_id() { + try { + return this.get_cached_property('SessionId').unpack(); + } catch (e) { + return null; + } + } + + async start() { + try { + if (this._started) + return; + + // Initialize the proxy + await new Promise((resolve, reject) => { + this.init_async( + GLib.PRIORITY_DEFAULT, + null, + (proxy, res) => { + try { + proxy.init_finish(res); + resolve(); + } catch (e) { + reject(e); + } + } + ); + }); + + // Start the session + await new Promise((resolve, reject) => { + this.call( + 'Start', + null, + Gio.DBusCallFlags.NONE, + -1, + null, + (proxy, res) => { + try { + resolve(proxy.call_finish(res)); + } catch (e) { + reject(e); + } + } + ); + }); + + this._started = true; + } catch (e) { + this.destroy(); + + Gio.DBusError.strip_remote_error(e); + throw e; + } + } + + stop() { + if (this._started) { + this._started = false; + this.call('Stop', null, Gio.DBusCallFlags.NONE, -1, null, null); + } + } + + _translateButton(button) { + switch (button) { + case Gdk.BUTTON_PRIMARY: + return 0x110; + + case Gdk.BUTTON_MIDDLE: + return 0x112; + + case Gdk.BUTTON_SECONDARY: + return 0x111; + + case 4: + return 0; // FIXME + + case 5: + return 0x10F; // up + } + } + + movePointer(dx, dy) { + this._call( + 'NotifyPointerMotionRelative', + GLib.Variant.new('(dd)', [dx, dy]) + ); + } + + pressPointer(button) { + button = this._translateButton(button); + + this._call( + 'NotifyPointerButton', + GLib.Variant.new('(ib)', [button, true]) + ); + } + + releasePointer(button) { + button = this._translateButton(button); + + this._call( + 'NotifyPointerButton', + GLib.Variant.new('(ib)', [button, false]) + ); + } + + clickPointer(button) { + button = this._translateButton(button); + + this._call( + 'NotifyPointerButton', + GLib.Variant.new('(ib)', [button, true]) + ); + + this._call( + 'NotifyPointerButton', + GLib.Variant.new('(ib)', [button, false]) + ); + } + + doubleclickPointer(button) { + this.clickPointer(button); + this.clickPointer(button); + } + + scrollPointer(dx, dy) { + // NOTE: NotifyPointerAxis only seems to work on Wayland, but maybe + // NotifyPointerAxisDiscrete is the better choice anyways + if (HAVE_WAYLAND) { + this._call( + 'NotifyPointerAxis', + GLib.Variant.new('(ddu)', [dx, dy, 0]) + ); + this._call( + 'NotifyPointerAxis', + GLib.Variant.new('(ddu)', [0, 0, 1]) + ); + } else if (dy > 0) { + this._call( + 'NotifyPointerAxisDiscrete', + GLib.Variant.new('(ui)', [Gdk.ScrollDirection.UP, 1]) + ); + } else if (dy < 0) { + this._call( + 'NotifyPointerAxisDiscrete', + GLib.Variant.new('(ui)', [Gdk.ScrollDirection.UP, -1]) + ); + } + } + + /* + * Keyboard Events + */ + pressKeysym(keysym) { + this._call( + 'NotifyKeyboardKeysym', + GLib.Variant.new('(ub)', [keysym, true]) + ); + } + + releaseKeysym(keysym) { + this._call( + 'NotifyKeyboardKeysym', + GLib.Variant.new('(ub)', [keysym, false]) + ); + } + + pressreleaseKeysym(keysym) { + this._call( + 'NotifyKeyboardKeysym', + GLib.Variant.new('(ub)', [keysym, true]) + ); + this._call( + 'NotifyKeyboardKeysym', + GLib.Variant.new('(ub)', [keysym, false]) + ); + } + + /* + * High-level keyboard input + */ + pressKey(input, modifiers) { + // Press Modifiers + if (modifiers & Gdk.ModifierType.MOD1_MASK) + this.pressKeysym(Gdk.KEY_Alt_L); + if (modifiers & Gdk.ModifierType.CONTROL_MASK) + this.pressKeysym(Gdk.KEY_Control_L); + if (modifiers & Gdk.ModifierType.SHIFT_MASK) + this.pressKeysym(Gdk.KEY_Shift_L); + if (modifiers & Gdk.ModifierType.SUPER_MASK) + this.pressKeysym(Gdk.KEY_Super_L); + + if (typeof input === 'string') { + const keysym = Gdk.unicode_to_keyval(input.codePointAt(0)); + this.pressreleaseKeysym(keysym); + } else { + this.pressreleaseKeysym(input); + } + + // Release Modifiers + if (modifiers & Gdk.ModifierType.MOD1_MASK) + this.releaseKeysym(Gdk.KEY_Alt_L); + if (modifiers & Gdk.ModifierType.CONTROL_MASK) + this.releaseKeysym(Gdk.KEY_Control_L); + if (modifiers & Gdk.ModifierType.SHIFT_MASK) + this.releaseKeysym(Gdk.KEY_Shift_L); + if (modifiers & Gdk.ModifierType.SUPER_MASK) + this.releaseKeysym(Gdk.KEY_Super_L); + } + + destroy() { + if (this.__disposed === undefined) { + this.__disposed = true; + GObject.signal_handlers_destroy(this); + } + } +}); + + +class Controller { + constructor() { + this._nameAppearedId = 0; + this._session = null; + this._sessionCloseId = 0; + this._sessionExpiry = 0; + this._sessionExpiryId = 0; + this._sessionStarting = false; + + // Watch for the RemoteDesktop portal + this._nameWatcherId = Gio.bus_watch_name( + Gio.BusType.SESSION, + 'org.gnome.Mutter.RemoteDesktop', + Gio.BusNameWatcherFlags.NONE, + this._onNameAppeared.bind(this), + this._onNameVanished.bind(this) + ); + } + + get connection() { + if (this._connection === undefined) + this._connection = null; + + return this._connection; + } + + /** + * Check if this is a Wayland session, specifically for distributions that + * don't ship pipewire support (eg. Debian/Ubuntu). + * + * FIXME: this is a super ugly hack that should go away + * + * @return {boolean} %true if wayland is not supported + */ + _checkWayland() { + if (HAVE_WAYLAND) { + // eslint-disable-next-line no-global-assign + HAVE_REMOTEINPUT = false; + const service = Gio.Application.get_default(); + + if (service === null) + return true; + + // First we're going to disabled the affected plugins on all devices + for (const device of service.manager.devices.values()) { + const supported = device.settings.get_strv('supported-plugins'); + let index; + + if ((index = supported.indexOf('mousepad')) > -1) + supported.splice(index, 1); + + if ((index = supported.indexOf('presenter')) > -1) + supported.splice(index, 1); + + device.settings.set_strv('supported-plugins', supported); + } + + // Second we need each backend to rebuild its identity packet and + // broadcast the amended capabilities to the network + for (const backend of service.manager.backends.values()) + backend.buildIdentity(); + + service.manager.identify(); + + return true; + } + + return false; + } + + _onNameAppeared(connection, name, name_owner) { + try { + this._connection = connection; + } catch (e) { + logError(e); + } + } + + _onNameVanished(connection, name) { + try { + if (this._session !== null) + this._onSessionClosed(this._session); + } catch (e) { + logError(e); + } + } + + _onSessionClosed(session) { + // Disconnect from the session + if (this._sessionClosedId > 0) { + session.disconnect(this._sessionClosedId); + this._sessionClosedId = 0; + } + + // Destroy the session + session.destroy(); + this._session = null; + } + + _onSessionExpired() { + // If the session has been used recently, schedule a new expiry + const remainder = Math.floor(this._sessionExpiry - (Date.now() / 1000)); + + if (remainder > 0) { + this._sessionExpiryId = GLib.timeout_add_seconds( + GLib.PRIORITY_DEFAULT, + remainder, + this._onSessionExpired.bind(this) + ); + + return GLib.SOURCE_REMOVE; + } + + // Otherwise if there's an active session, close it + if (this._session !== null) + this._session.stop(); + + // Reset the GSource Id + this._sessionExpiryId = 0; + + return GLib.SOURCE_REMOVE; + } + + _createRemoteDesktopSession() { + if (this.connection === null) + return Promise.reject(new Error('No DBus connection')); + + return new Promise((resolve, reject) => { + this.connection.call( + 'org.gnome.Mutter.RemoteDesktop', + '/org/gnome/Mutter/RemoteDesktop', + 'org.gnome.Mutter.RemoteDesktop', + 'CreateSession', + null, + null, + Gio.DBusCallFlags.NONE, + -1, + null, + (connection, res) => { + try { + res = connection.call_finish(res); + resolve(res.deepUnpack()[0]); + } catch (e) { + reject(e); + } + } + ); + }); + } + + _createScreenCastSession(sessionId) { + if (this.connection === null) + return Promise.reject(new Error('No DBus connection')); + + return new Promise((resolve, reject) => { + const options = new GLib.Variant('(a{sv})', [{ + 'disable-animations': GLib.Variant.new_boolean(false), + 'remote-desktop-session-id': GLib.Variant.new_string(sessionId), + }]); + + this.connection.call( + 'org.gnome.Mutter.ScreenCast', + '/org/gnome/Mutter/ScreenCast', + 'org.gnome.Mutter.ScreenCast', + 'CreateSession', + options, + null, + Gio.DBusCallFlags.NONE, + -1, + null, + (connection, res) => { + try { + res = connection.call_finish(res); + resolve(res.deepUnpack()[0]); + } catch (e) { + reject(e); + } + } + ); + }); + } + + async _ensureAdapter() { + try { + // Update the timestamp of the last event + this._sessionExpiry = Math.floor((Date.now() / 1000) + SESSION_TIMEOUT); + + // Session is active + if (this._session !== null) + return; + + // Mutter's RemoteDesktop is not available, fall back to Atspi + if (this.connection === null) { + debug('Falling back to Atspi'); + + // If we got here in Wayland, we need to re-adjust and bail + if (this._checkWayland()) + return; + + const fallback = imports.service.components.atspi; + this._session = new fallback.Controller(); + + // Mutter is available and there isn't another session starting + } else if (this._sessionStarting === false) { + this._sessionStarting = true; + + debug('Creating Mutter RemoteDesktop session'); + + // This takes three steps: creating the remote desktop session, + // starting the session, and creating a screencast session for + // the remote desktop session. + const objectPath = await this._createRemoteDesktopSession(); + + this._session = new RemoteSession(objectPath); + await this._session.start(); + + await this._createScreenCastSession(this._session.session_id); + + // Watch for the session ending + this._sessionClosedId = this._session.connect( + 'closed', + this._onSessionClosed.bind(this) + ); + + if (this._sessionExpiryId === 0) { + this._sessionExpiryId = GLib.timeout_add_seconds( + GLib.PRIORITY_DEFAULT, + SESSION_TIMEOUT, + this._onSessionExpired.bind(this) + ); + } + + this._sessionStarting = false; + } + } catch (e) { + logError(e); + + if (this._session !== null) { + this._session.destroy(); + this._session = null; + } + + this._sessionStarting = false; + } + } + + /* + * Pointer Events + */ + movePointer(dx, dy) { + try { + if (dx === 0 && dy === 0) + return; + + this._ensureAdapter(); + this._session.movePointer(dx, dy); + } catch (e) { + debug(e); + } + } + + pressPointer(button) { + try { + this._ensureAdapter(); + this._session.pressPointer(button); + } catch (e) { + debug(e); + } + } + + releasePointer(button) { + try { + this._ensureAdapter(); + this._session.releasePointer(button); + } catch (e) { + debug(e); + } + } + + clickPointer(button) { + try { + this._ensureAdapter(); + this._session.clickPointer(button); + } catch (e) { + debug(e); + } + } + + doubleclickPointer(button) { + try { + this._ensureAdapter(); + this._session.doubleclickPointer(button); + } catch (e) { + debug(e); + } + } + + scrollPointer(dx, dy) { + if (dx === 0 && dy === 0) + return; + + try { + this._ensureAdapter(); + this._session.scrollPointer(dx, dy); + } catch (e) { + debug(e); + } + } + + /* + * Keyboard Events + */ + pressKeysym(keysym) { + try { + this._ensureAdapter(); + this._session.pressKeysym(keysym); + } catch (e) { + debug(e); + } + } + + releaseKeysym(keysym) { + try { + this._ensureAdapter(); + this._session.releaseKeysym(keysym); + } catch (e) { + debug(e); + } + } + + pressreleaseKeysym(keysym) { + try { + this._ensureAdapter(); + this._session.pressreleaseKeysym(keysym); + } catch (e) { + debug(e); + } + } + + /* + * High-level keyboard input + */ + pressKey(input, modifiers) { + try { + this._ensureAdapter(); + this._session.pressKey(input, modifiers); + } catch (e) { + debug(e); + } + } + + destroy() { + if (this._session !== null) { + // Disconnect from the session + if (this._sessionClosedId > 0) { + this._session.disconnect(this._sessionClosedId); + this._sessionClosedId = 0; + } + + this._session.destroy(); + this._session = null; + } + + if (this._nameWatcherId > 0) { + Gio.bus_unwatch_name(this._nameWatcherId); + this._nameWatcherId = 0; + } + } +} + + +/** + * The service class for this component + */ +var Component = Controller; + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/mpris.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/mpris.js new file mode 100644 index 0000000..df9c5ff --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/mpris.js @@ -0,0 +1,1029 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + + +var Player = GObject.registerClass({ + GTypeName: 'GSConnectMediaPlayerInterface', + Properties: { + // Application Properties + 'CanQuit': GObject.ParamSpec.boolean( + 'CanQuit', + 'Can Quit', + 'Whether the client can call the Quit method.', + GObject.ParamFlags.READABLE, + false + ), + 'Fullscreen': GObject.ParamSpec.boolean( + 'Fullscreen', + 'Fullscreen', + 'Whether the player is in fullscreen mode.', + GObject.ParamFlags.READWRITE, + false + ), + 'CanSetFullscreen': GObject.ParamSpec.boolean( + 'CanSetFullscreen', + 'Can Set Fullscreen', + 'Whether the client can set the Fullscreen property.', + GObject.ParamFlags.READABLE, + false + ), + 'CanRaise': GObject.ParamSpec.boolean( + 'CanRaise', + 'Can Raise', + 'Whether the client can call the Raise method.', + GObject.ParamFlags.READABLE, + false + ), + 'HasTrackList': GObject.ParamSpec.boolean( + 'HasTrackList', + 'Has Track List', + 'Whether the player has a track list.', + GObject.ParamFlags.READABLE, + false + ), + 'Identity': GObject.ParamSpec.string( + 'Identity', + 'Identity', + 'The application name.', + GObject.ParamFlags.READABLE, + null + ), + 'DesktopEntry': GObject.ParamSpec.string( + 'DesktopEntry', + 'DesktopEntry', + 'The basename of an installed .desktop file.', + GObject.ParamFlags.READABLE, + null + ), + 'SupportedUriSchemes': GObject.param_spec_variant( + 'SupportedUriSchemes', + 'Supported URI Schemes', + 'The URI schemes supported by the media player.', + new GLib.VariantType('as'), + null, + GObject.ParamFlags.READABLE + ), + 'SupportedMimeTypes': GObject.param_spec_variant( + 'SupportedMimeTypes', + 'Supported MIME Types', + 'The mime-types supported by the media player.', + new GLib.VariantType('as'), + null, + GObject.ParamFlags.READABLE + ), + + // Player Properties + 'PlaybackStatus': GObject.ParamSpec.string( + 'PlaybackStatus', + 'Playback Status', + 'The current playback status.', + GObject.ParamFlags.READABLE, + null + ), + 'LoopStatus': GObject.ParamSpec.string( + 'LoopStatus', + 'Loop Status', + 'The current loop status.', + GObject.ParamFlags.READWRITE, + null + ), + 'Rate': GObject.ParamSpec.double( + 'Rate', + 'Rate', + 'The current playback rate.', + GObject.ParamFlags.READWRITE, + 0.0, 1.0, + 1.0 + ), + 'MinimumRate': GObject.ParamSpec.double( + 'MinimumRate', + 'Minimum Rate', + 'The minimum playback rate.', + GObject.ParamFlags.READWRITE, + 0.0, 1.0, + 1.0 + ), + 'MaximimRate': GObject.ParamSpec.double( + 'MaximumRate', + 'Maximum Rate', + 'The maximum playback rate.', + GObject.ParamFlags.READWRITE, + 0.0, 1.0, + 1.0 + ), + 'Shuffle': GObject.ParamSpec.boolean( + 'Shuffle', + 'Shuffle', + 'Whether track changes are linear.', + GObject.ParamFlags.READWRITE, + null + ), + 'Metadata': GObject.param_spec_variant( + 'Metadata', + 'Metadata', + 'The metadata of the current element.', + new GLib.VariantType('a{sv}'), + null, + GObject.ParamFlags.READABLE + ), + 'Volume': GObject.ParamSpec.double( + 'Volume', + 'Volume', + 'The volume level.', + GObject.ParamFlags.READWRITE, + 0.0, 1.0, + 1.0 + ), + 'Position': GObject.ParamSpec.int64( + 'Position', + 'Position', + 'The current track position in microseconds.', + GObject.ParamFlags.READABLE, + 0, Number.MAX_SAFE_INTEGER, + 0 + ), + 'CanGoNext': GObject.ParamSpec.boolean( + 'CanGoNext', + 'Can Go Next', + 'Whether the client can call the Next method.', + GObject.ParamFlags.READABLE, + false + ), + 'CanGoPrevious': GObject.ParamSpec.boolean( + 'CanGoPrevious', + 'Can Go Previous', + 'Whether the client can call the Previous method.', + GObject.ParamFlags.READABLE, + false + ), + 'CanPlay': GObject.ParamSpec.boolean( + 'CanPlay', + 'Can Play', + 'Whether playback can be started using Play or PlayPause.', + GObject.ParamFlags.READABLE, + false + ), + 'CanPause': GObject.ParamSpec.boolean( + 'CanPause', + 'Can Pause', + 'Whether playback can be paused using Play or PlayPause.', + GObject.ParamFlags.READABLE, + false + ), + 'CanSeek': GObject.ParamSpec.boolean( + 'CanSeek', + 'Can Seek', + 'Whether the client can control the playback position using Seek and SetPosition.', + GObject.ParamFlags.READABLE, + false + ), + 'CanControl': GObject.ParamSpec.boolean( + 'CanControl', + 'Can Control', + 'Whether the media player may be controlled over this interface.', + GObject.ParamFlags.READABLE, + false + ), + }, + Signals: { + 'Seeked': { + flags: GObject.SignalFlags.RUN_FIRST, + param_types: [GObject.TYPE_INT64], + }, + }, +}, class Player extends GObject.Object { + + /* + * The org.mpris.MediaPlayer2 Interface + */ + get CanQuit() { + if (this._CanQuit === undefined) + this._CanQuit = false; + + return this._CanQuit; + } + + get CanRaise() { + if (this._CanRaise === undefined) + this._CanRaise = false; + + return this._CanRaise; + } + + get CanSetFullscreen() { + if (this._CanFullscreen === undefined) + this._CanFullscreen = false; + + return this._CanFullscreen; + } + + get DesktopEntry() { + if (this._DesktopEntry === undefined) + return 'org.gnome.Shell.Extensions.GSConnect'; + + return this._DesktopEntry; + } + + get Fullscreen() { + if (this._Fullscreen === undefined) + this._Fullscreen = false; + + return this._Fullscreen; + } + + set Fullscreen(mode) { + if (this.Fullscreen === mode) + return; + + this._Fullscreen = mode; + this.notify('Fullscreen'); + } + + get HasTrackList() { + if (this._HasTrackList === undefined) + this._HasTrackList = false; + + return this._HasTrackList; + } + + get Identity() { + if (this._Identity === undefined) + this._Identity = ''; + + return this._Identity; + } + + get SupportedMimeTypes() { + if (this._SupportedMimeTypes === undefined) + this._SupportedMimeTypes = []; + + return this._SupportedMimeTypes; + } + + get SupportedUriSchemes() { + if (this._SupportedUriSchemes === undefined) + this._SupportedUriSchemes = []; + + return this._SupportedUriSchemes; + } + + Quit() { + throw new GObject.NotImplementedError(); + } + + Raise() { + throw new GObject.NotImplementedError(); + } + + /* + * The org.mpris.MediaPlayer2.Player Interface + */ + get CanControl() { + if (this._CanControl === undefined) + this._CanControl = false; + + return this._CanControl; + } + + get CanGoNext() { + if (this._CanGoNext === undefined) + this._CanGoNext = false; + + return this._CanGoNext; + } + + get CanGoPrevious() { + if (this._CanGoPrevious === undefined) + this._CanGoPrevious = false; + + return this._CanGoPrevious; + } + + get CanPause() { + if (this._CanPause === undefined) + this._CanPause = false; + + return this._CanPause; + } + + get CanPlay() { + if (this._CanPlay === undefined) + this._CanPlay = false; + + return this._CanPlay; + } + + get CanSeek() { + if (this._CanSeek === undefined) + this._CanSeek = false; + + return this._CanSeek; + } + + get LoopStatus() { + if (this._LoopStatus === undefined) + this._LoopStatus = 'None'; + + return this._LoopStatus; + } + + set LoopStatus(status) { + if (this.LoopStatus === status) + return; + + this._LoopStatus = status; + this.notify('LoopStatus'); + } + + get MaximumRate() { + if (this._MaximumRate === undefined) + this._MaximumRate = 1.0; + + return this._MaximumRate; + } + + get Metadata() { + if (this._Metadata === undefined) { + this._Metadata = { + 'xesam:artist': [_('Unknown')], + 'xesam:album': _('Unknown'), + 'xesam:title': _('Unknown'), + 'mpris:length': 0, + }; + } + + return this._Metadata; + } + + get MinimumRate() { + if (this._MinimumRate === undefined) + this._MinimumRate = 1.0; + + return this._MinimumRate; + } + + get PlaybackStatus() { + if (this._PlaybackStatus === undefined) + this._PlaybackStatus = 'Stopped'; + + return this._PlaybackStatus; + } + + get Position() { + if (this._Position === undefined) + this._Position = 0; + + return this._Position; + } + + get Rate() { + if (this._Rate === undefined) + this._Rate = 1.0; + + return this._Rate; + } + + set Rate(rate) { + if (this.Rate === rate) + return; + + this._Rate = rate; + this.notify('Rate'); + } + + get Shuffle() { + if (this._Shuffle === undefined) + this._Shuffle = false; + + return this._Shuffle; + } + + set Shuffle(mode) { + if (this.Shuffle === mode) + return; + + this._Shuffle = mode; + this.notify('Shuffle'); + } + + get Volume() { + if (this._Volume === undefined) + this._Volume = 1.0; + + return this._Volume; + } + + set Volume(level) { + if (this.Volume === level) + return; + + this._Volume = level; + this.notify('Volume'); + } + + Next() { + throw new GObject.NotImplementedError(); + } + + OpenUri(uri) { + throw new GObject.NotImplementedError(); + } + + Previous() { + throw new GObject.NotImplementedError(); + } + + Pause() { + throw new GObject.NotImplementedError(); + } + + Play() { + throw new GObject.NotImplementedError(); + } + + PlayPause() { + throw new GObject.NotImplementedError(); + } + + Seek(offset) { + throw new GObject.NotImplementedError(); + } + + SetPosition(trackId, position) { + throw new GObject.NotImplementedError(); + } + + Stop() { + throw new GObject.NotImplementedError(); + } +}); + + +/** + * An aggregate of the org.mpris.MediaPlayer2 and org.mpris.MediaPlayer2.Player + * interfaces. + */ +const PlayerProxy = GObject.registerClass({ + GTypeName: 'GSConnectMPRISPlayer', +}, class PlayerProxy extends Player { + + _init(name) { + super._init(); + + this._application = new Gio.DBusProxy({ + g_bus_type: Gio.BusType.SESSION, + g_name: name, + g_object_path: '/org/mpris/MediaPlayer2', + g_interface_name: 'org.mpris.MediaPlayer2', + }); + + this._applicationChangedId = this._application.connect( + 'g-properties-changed', + this._onPropertiesChanged.bind(this) + ); + + this._player = new Gio.DBusProxy({ + g_bus_type: Gio.BusType.SESSION, + g_name: name, + g_object_path: '/org/mpris/MediaPlayer2', + g_interface_name: 'org.mpris.MediaPlayer2.Player', + }); + + this._playerChangedId = this._player.connect( + 'g-properties-changed', + this._onPropertiesChanged.bind(this) + ); + + this._playerSignalId = this._player.connect( + 'g-signal', + this._onSignal.bind(this) + ); + + this._cancellable = new Gio.Cancellable(); + } + + _onSignal(proxy, sender_name, signal_name, parameters) { + try { + if (signal_name !== 'Seeked') + return; + + this.emit('Seeked', parameters.deepUnpack()[0]); + } catch (e) { + debug(e, proxy.g_name); + } + } + + _call(proxy, name, parameters = null) { + proxy.call( + name, + parameters, + Gio.DBusCallFlags.NO_AUTO_START, + -1, + this._cancellable, + (proxy, result) => { + try { + proxy.call_finish(result); + } catch (e) { + Gio.DBusError.strip_remote_error(e); + debug(e, proxy.g_name); + } + } + ); + } + + _get(proxy, name, fallback = null) { + try { + return proxy.get_cached_property(name).recursiveUnpack(); + } catch (e) { + return fallback; + } + } + + _set(proxy, name, value) { + try { + proxy.set_cached_property(name, value); + + proxy.call( + 'org.freedesktop.DBus.Properties.Set', + new GLib.Variant('(ssv)', [proxy.g_interface_name, name, value]), + Gio.DBusCallFlags.NO_AUTO_START, + -1, + this._cancellable, + (proxy, result) => { + try { + proxy.call_finish(result); + } catch (e) { + Gio.DBusError.strip_remote_error(e); + debug(e, proxy.g_name); + } + } + ); + } catch (e) { + debug(e, proxy.g_name); + } + } + + _onPropertiesChanged(proxy, changed, invalidated) { + try { + this.freeze_notify(); + + for (const name in changed.deepUnpack()) + this.notify(name); + + this.thaw_notify(); + } catch (e) { + debug(e, proxy.g_name); + } + } + + initPromise() { + const application = new Promise((resolve, reject) => { + this._application.init_async(0, this._cancellable, (proxy, res) => { + try { + resolve(proxy.init_finish(res)); + } catch (e) { + reject(e); + } + }); + }); + + const player = new Promise((resolve, reject) => { + this._player.init_async(0, this._cancellable, (proxy, res) => { + try { + resolve(proxy.init_finish(res)); + } catch (e) { + reject(e); + } + }); + }); + + return Promise.all([application, player]); + } + + /* + * The org.mpris.MediaPlayer2 Interface + */ + get CanQuit() { + return this._get(this._application, 'CanQuit', false); + } + + get CanRaise() { + return this._get(this._application, 'CanRaise', false); + } + + get CanSetFullscreen() { + return this._get(this._application, 'CanSetFullscreen', false); + } + + get DesktopEntry() { + return this._get(this._application, 'DesktopEntry', null); + } + + get Fullscreen() { + return this._get(this._application, 'Fullscreen', false); + } + + set Fullscreen(mode) { + this._set(this._application, 'Fullscreen', new GLib.Variant('b', mode)); + } + + get HasTrackList() { + return this._get(this._application, 'HasTrackList', false); + } + + get Identity() { + return this._get(this._application, 'Identity', _('Unknown')); + } + + get SupportedMimeTypes() { + return this._get(this._application, 'SupportedMimeTypes', []); + } + + get SupportedUriSchemes() { + return this._get(this._application, 'SupportedUriSchemes', []); + } + + Quit() { + this._call(this._application, 'Quit'); + } + + Raise() { + this._call(this._application, 'Raise'); + } + + /* + * The org.mpris.MediaPlayer2.Player Interface + */ + get CanControl() { + return this._get(this._player, 'CanControl', false); + } + + get CanGoNext() { + return this._get(this._player, 'CanGoNext', false); + } + + get CanGoPrevious() { + return this._get(this._player, 'CanGoPrevious', false); + } + + get CanPause() { + return this._get(this._player, 'CanPause', false); + } + + get CanPlay() { + return this._get(this._player, 'CanPlay', false); + } + + get CanSeek() { + return this._get(this._player, 'CanSeek', false); + } + + get LoopStatus() { + return this._get(this._player, 'LoopStatus', 'None'); + } + + set LoopStatus(status) { + this._set(this._player, 'LoopStatus', new GLib.Variant('s', status)); + } + + get MaximumRate() { + return this._get(this._player, 'MaximumRate', 1.0); + } + + get Metadata() { + if (this._metadata === undefined) { + this._metadata = { + 'xesam:artist': [_('Unknown')], + 'xesam:album': _('Unknown'), + 'xesam:title': _('Unknown'), + 'mpris:length': 0, + }; + } + + return this._get(this._player, 'Metadata', this._metadata); + } + + get MinimumRate() { + return this._get(this._player, 'MinimumRate', 1.0); + } + + get PlaybackStatus() { + return this._get(this._player, 'PlaybackStatus', 'Stopped'); + } + + // g-properties-changed is not emitted for this property + get Position() { + try { + const reply = this._player.call_sync( + 'org.freedesktop.DBus.Properties.Get', + new GLib.Variant('(ss)', [ + 'org.mpris.MediaPlayer2.Player', + 'Position', + ]), + Gio.DBusCallFlags.NONE, + -1, + null + ); + + return reply.recursiveUnpack()[0]; + } catch (e) { + return 0; + } + } + + get Rate() { + return this._get(this._player, 'Rate', 1.0); + } + + set Rate(rate) { + this._set(this._player, 'Rate', new GLib.Variant('d', rate)); + } + + get Shuffle() { + return this._get(this._player, 'Shuffle', false); + } + + set Shuffle(mode) { + this._set(this._player, 'Shuffle', new GLib.Variant('b', mode)); + } + + get Volume() { + return this._get(this._player, 'Volume', 1.0); + } + + set Volume(level) { + this._set(this._player, 'Volume', new GLib.Variant('d', level)); + } + + Next() { + this._call(this._player, 'Next'); + } + + OpenUri(uri) { + this._call(this._player, 'OpenUri', new GLib.Variant('(s)', [uri])); + } + + Previous() { + this._call(this._player, 'Previous'); + } + + Pause() { + this._call(this._player, 'Pause'); + } + + Play() { + this._call(this._player, 'Play'); + } + + PlayPause() { + this._call(this._player, 'PlayPause'); + } + + Seek(offset) { + this._call(this._player, 'Seek', new GLib.Variant('(x)', [offset])); + } + + SetPosition(trackId, position) { + this._call(this._player, 'SetPosition', + new GLib.Variant('(ox)', [trackId, position])); + } + + Stop() { + this._call(this._player, 'Stop'); + } + + destroy() { + if (this._cancellable.is_cancelled()) + return; + + this._cancellable.cancel(); + this._application.disconnect(this._applicationChangedId); + this._player.disconnect(this._playerChangedId); + this._player.disconnect(this._playerSignalId); + } +}); + + +/** + * A manager for media players + */ +var Manager = GObject.registerClass({ + GTypeName: 'GSConnectMPRISManager', + Signals: { + 'player-added': { + param_types: [GObject.TYPE_OBJECT], + }, + 'player-removed': { + param_types: [GObject.TYPE_OBJECT], + }, + 'player-changed': { + param_types: [GObject.TYPE_OBJECT], + }, + 'player-seeked': { + param_types: [GObject.TYPE_OBJECT, GObject.TYPE_INT64], + }, + }, +}, class Manager extends GObject.Object { + + _init() { + super._init(); + + // Asynchronous setup + this._cancellable = new Gio.Cancellable(); + this._connection = Gio.DBus.session; + this._players = new Map(); + this._paused = new Map(); + + this._nameOwnerChangedId = Gio.DBus.session.signal_subscribe( + 'org.freedesktop.DBus', + 'org.freedesktop.DBus', + 'NameOwnerChanged', + '/org/freedesktop/DBus', + 'org.mpris.MediaPlayer2', + Gio.DBusSignalFlags.MATCH_ARG0_NAMESPACE, + this._onNameOwnerChanged.bind(this) + ); + + this._loadPlayers(); + } + + async _loadPlayers() { + try { + const names = await new Promise((resolve, reject) => { + this._connection.call( + 'org.freedesktop.DBus', + '/org/freedesktop/DBus', + 'org.freedesktop.DBus', + 'ListNames', + null, + null, + Gio.DBusCallFlags.NONE, + -1, + this._cancellable, + (connection, res) => { + try { + res = connection.call_finish(res); + resolve(res.deepUnpack()[0]); + } catch (e) { + reject(e); + } + } + ); + }); + + for (let i = 0, len = names.length; i < len; i++) { + const name = names[i]; + + if (!name.startsWith('org.mpris.MediaPlayer2')) + continue; + + if (!name.includes('GSConnect')) + this._addPlayer(name); + } + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + } + } + + _onNameOwnerChanged(connection, sender, object, iface, signal, parameters) { + const [name, oldOwner, newOwner] = parameters.deepUnpack(); + + if (name.includes('GSConnect')) + return; + + if (newOwner.length) + this._addPlayer(name); + else if (oldOwner.length) + this._removePlayer(name); + } + + async _addPlayer(name) { + try { + if (!this._players.has(name)) { + const player = new PlayerProxy(name); + await player.initPromise(); + + player.connect('notify', + (player) => this.emit('player-changed', player)); + + player.connect('Seeked', this.emit.bind(this, 'player-seeked')); + + this._players.set(name, player); + this.emit('player-added', player); + } + } catch (e) { + debug(e, name); + } + } + + _removePlayer(name) { + try { + const player = this._players.get(name); + + if (player !== undefined) { + this._paused.delete(name); + this._players.delete(name); + this.emit('player-removed', player); + + player.destroy(); + } + } catch (e) { + debug(e, name); + } + } + + /** + * Check for a player by its Identity. + * + * @param {string} identity - A player name + * @return {boolean} %true if the player was found + */ + hasPlayer(identity) { + for (const player of this._players.values()) { + if (player.Identity === identity) + return true; + } + + return false; + } + + /** + * Get a player by its Identity. + * + * @param {string} identity - A player name + * @return {GSConnectMPRISPlayer|null} A player or %null + */ + getPlayer(identity) { + for (const player of this._players.values()) { + if (player.Identity === identity) + return player; + } + + return null; + } + + /** + * Get a list of player identities. + * + * @return {string[]} A list of player identities + */ + getIdentities() { + const identities = []; + + for (const player of this._players.values()) { + const identity = player.Identity; + + if (identity) + identities.push(identity); + } + + return identities; + } + + /** + * A convenience function for pausing all players currently playing. + */ + pauseAll() { + for (const [name, player] of this._players) { + if (player.PlaybackStatus === 'Playing' && player.CanPause) { + player.Pause(); + this._paused.set(name, player); + } + } + } + + /** + * A convenience function for restarting all players paused with pauseAll(). + */ + unpauseAll() { + for (const player of this._paused.values()) { + if (player.PlaybackStatus === 'Paused' && player.CanPlay) + player.Play(); + } + + this._paused.clear(); + } + + destroy() { + if (this._cancellable.is_cancelled()) + return; + + this._cancellable.cancel(); + this._connection.signal_unsubscribe(this._nameOwnerChangedId); + + this._paused.clear(); + this._players.forEach(player => player.destroy()); + this._players.clear(); + } +}); + + +/** + * The service class for this component + */ +var Component = Manager; + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/notification.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/notification.js new file mode 100644 index 0000000..43c89ef --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/notification.js @@ -0,0 +1,440 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GjsPrivate = imports.gi.GjsPrivate; +const GObject = imports.gi.GObject; + +const DBus = imports.service.utils.dbus; + + +const _nodeInfo = Gio.DBusNodeInfo.new_for_xml(` + + + + + + + + + + + + + + + + + + + + + + + + + +`); + + +const FDO_IFACE = _nodeInfo.lookup_interface('org.freedesktop.Notifications'); +const FDO_MATCH = "interface='org.freedesktop.Notifications',member='Notify',type='method_call'"; + +const GTK_IFACE = _nodeInfo.lookup_interface('org.gtk.Notifications'); +const GTK_MATCH = "interface='org.gtk.Notifications',member='AddNotification',type='method_call'"; + + +/** + * A class for snooping Freedesktop (libnotify) and Gtk (GNotification) + * notifications and forwarding them to supporting devices. + */ +const Listener = GObject.registerClass({ + GTypeName: 'GSConnectNotificationListener', + Signals: { + 'notification-added': { + flags: GObject.SignalFlags.RUN_LAST, + param_types: [GLib.Variant.$gtype], + }, + }, +}, class Listener extends GObject.Object { + + _init() { + super._init(); + + // Respect desktop notification settings + this._settings = new Gio.Settings({ + schema_id: 'org.gnome.desktop.notifications', + }); + + // Watch for new application policies + this._settingsId = this._settings.connect( + 'changed::application-children', + this._onSettingsChanged.bind(this) + ); + + // Cache for appName->desktop-id lookups + this._names = {}; + + // Asynchronous setup + this._init_async(); + } + + get applications() { + if (this._applications === undefined) + this._onSettingsChanged(); + + return this._applications; + } + + /** + * Update application notification settings + */ + _onSettingsChanged() { + this._applications = {}; + + for (const app of this._settings.get_strv('application-children')) { + const appSettings = new Gio.Settings({ + schema_id: 'org.gnome.desktop.notifications.application', + path: `/org/gnome/desktop/notifications/application/${app}/`, + }); + + const appInfo = Gio.DesktopAppInfo.new( + appSettings.get_string('application-id') + ); + + if (appInfo !== null) + this._applications[appInfo.get_name()] = appSettings; + } + } + + _listNames() { + return new Promise((resolve, reject) => { + this._session.call( + 'org.freedesktop.DBus', + '/org/freedesktop/DBus', + 'org.freedesktop.DBus', + 'ListNames', + null, + null, + Gio.DBusCallFlags.NONE, + -1, + null, + (connection, res) => { + try { + res = connection.call_finish(res); + resolve(res.deepUnpack()[0]); + } catch (e) { + reject(e); + } + } + ); + }); + } + + _getNameOwner(name) { + return new Promise((resolve, reject) => { + this._session.call( + 'org.freedesktop.DBus', + '/org/freedesktop/DBus', + 'org.freedesktop.DBus', + 'GetNameOwner', + new GLib.Variant('(s)', [name]), + null, + Gio.DBusCallFlags.NONE, + -1, + null, + (connection, res) => { + try { + res = connection.call_finish(res); + resolve(res.deepUnpack()[0]); + } catch (e) { + reject(e); + } + } + ); + }); + } + + /** + * Try and find a well-known name for @sender on the session bus + * + * @param {string} sender - A DBus unique name (eg. :1.2282) + * @param {string} appName - @appName passed to Notify() (Optional) + * @return {string} A well-known name or %null + */ + async _getAppId(sender, appName) { + try { + // Get a list of well-known names, ignoring @sender + const names = await this._listNames(); + names.splice(names.indexOf(sender), 1); + + // Make a short list for substring matches (fractal/org.gnome.Fractal) + const appLower = appName.toLowerCase(); + + const shortList = names.filter(name => { + return name.toLowerCase().includes(appLower); + }); + + // Run the short list first + for (const name of shortList) { + const nameOwner = await this._getNameOwner(name); + + if (nameOwner === sender) + return name; + + names.splice(names.indexOf(name), 1); + } + + // Run the full list + for (const name of names) { + const nameOwner = await this._getNameOwner(name); + + if (nameOwner === sender) + return name; + } + + return null; + } catch (e) { + debug(e); + return null; + } + } + + /** + * Try and find the application name for @sender + * + * @param {string} sender - A DBus unique name + * @param {string} [appName] - `appName` supplied by Notify() + * @return {string} A well-known name or %null + */ + async _getAppName(sender, appName = null) { + // Check the cache first + if (appName && this._names.hasOwnProperty(appName)) + return this._names[appName]; + + try { + const appId = await this._getAppId(sender, appName); + const appInfo = Gio.DesktopAppInfo.new(`${appId}.desktop`); + this._names[appName] = appInfo.get_name(); + appName = appInfo.get_name(); + } catch (e) { + // Silence errors + } + + return appName; + } + + /** + * Callback for AddNotification()/Notify() + * + * @param {DBus.Interface} iface - The DBus interface + * @param {string} name - The DBus method name + * @param {GLib.Variant} parameters - The method parameters + * @param {Gio.DBusMethodInvocation} invocation - The method invocation info + */ + async _onHandleMethodCall(iface, name, parameters, invocation) { + try { + // Check if notifications are disabled in desktop settings + if (!this._settings.get_boolean('show-banners')) + return; + + parameters = parameters.full_unpack(); + + // GNotification + if (name === 'AddNotification') { + this.AddNotification(...parameters); + + // libnotify + } else if (name === 'Notify') { + const message = invocation.get_message(); + + if (this._fdoNameOwner === undefined) { + this._fdoNameOwner = await this._getNameOwner( + 'org.freedesktop.Notifications'); + } + + if (this._fdoNameOwner !== message.get_destination()) + return; + + // Try to brute-force an application name using DBus + if (!this.applications.hasOwnProperty(parameters[0])) { + const sender = message.get_sender(); + parameters[0] = await this._getAppName(sender, parameters[0]); + } + + this.Notify(...parameters); + } + } catch (e) { + debug(e); + } + } + + /** + * Export interfaces for proxying notifications and become a monitor + * + * @return {Promise} A promise for the operation + */ + _monitorConnection() { + return new Promise((resolve, reject) => { + // libnotify Interface + this._fdoNotifications = new GjsPrivate.DBusImplementation({ + g_interface_info: FDO_IFACE, + }); + this._fdoMethodCallId = this._fdoNotifications.connect( + 'handle-method-call', + this._onHandleMethodCall.bind(this) + ); + this._fdoNotifications.export( + this._monitor, + '/org/freedesktop/Notifications' + ); + + this._fdoNameOwnerChangedId = this._session.signal_subscribe( + 'org.freedesktop.DBus', + 'org.freedesktop.DBus', + 'NameOwnerChanged', + '/org/freedesktop/DBus', + 'org.freedesktop.Notifications', + Gio.DBusSignalFlags.MATCH_ARG0_NAMESPACE, + this._onFdoNameOwnerChanged.bind(this) + ); + + // GNotification Interface + this._gtkNotifications = new GjsPrivate.DBusImplementation({ + g_interface_info: GTK_IFACE, + }); + this._gtkMethodCallId = this._gtkNotifications.connect( + 'handle-method-call', + this._onHandleMethodCall.bind(this) + ); + this._gtkNotifications.export( + this._monitor, + '/org/gtk/Notifications' + ); + + // Become a monitor for Fdo & Gtk notifications + this._monitor.call( + 'org.freedesktop.DBus', + '/org/freedesktop/DBus', + 'org.freedesktop.DBus.Monitoring', + 'BecomeMonitor', + new GLib.Variant('(asu)', [[FDO_MATCH, GTK_MATCH], 0]), + null, + Gio.DBusCallFlags.NONE, + -1, + null, + (connection, res) => { + try { + resolve(connection.call_finish(res)); + } catch (e) { + reject(e); + } + } + ); + }); + } + + async _init_async() { + try { + this._session = await DBus.getConnection(); + this._monitor = await DBus.newConnection(); + await this._monitorConnection(); + } catch (e) { + const service = Gio.Application.get_default(); + + if (service !== null) + service.notify_error(e); + else + logError(e); + } + } + + _onFdoNameOwnerChanged(connection, sender, object, iface, signal, parameters) { + this._fdoNameOwner = parameters.deepUnpack()[2]; + } + + _sendNotification(notif) { + // Check if this application is disabled in desktop settings + const appSettings = this.applications[notif.appName]; + + if (appSettings && !appSettings.get_boolean('enable')) + return; + + // Send the notification to each supporting device + // TODO: avoid the overhead of the GAction framework with a signal? + const variant = GLib.Variant.full_pack(notif); + this.emit('notification-added', variant); + } + + Notify(appName, replacesId, iconName, summary, body, actions, hints, timeout) { + // Ignore notifications without an appName + if (!appName) + return; + + this._sendNotification({ + appName: appName, + id: `fdo|null|${replacesId}`, + title: summary, + text: body, + ticker: `${summary}: ${body}`, + isClearable: (replacesId !== 0), + icon: iconName, + }); + } + + AddNotification(application, id, notification) { + // Ignore our own notifications or we'll cause a notification loop + if (application === 'org.gnome.Shell.Extensions.GSConnect') + return; + + const appInfo = Gio.DesktopAppInfo.new(`${application}.desktop`); + + // Try to get an icon for the notification + if (!notification.hasOwnProperty('icon')) + notification.icon = appInfo.get_icon() || undefined; + + this._sendNotification({ + appName: appInfo.get_name(), + id: `gtk|${application}|${id}`, + title: notification.title, + text: notification.body, + ticker: `${notification.title}: ${notification.body}`, + isClearable: true, + icon: notification.icon, + }); + } + + destroy() { + try { + if (this._fdoNotifications) { + this._fdoNotifications.disconnect(this._fdoMethodCallId); + this._fdoNotifications.unexport(); + this._session.signal_unsubscribe(this._fdoNameOwnerChangedId); + } + + if (this._gtkNotifications) { + this._gtkNotifications.disconnect(this._gtkMethodCallId); + this._gtkNotifications.unexport(); + } + + if (this._settings) { + this._settings.disconnect(this._settingsId); + this._settings.run_dispose(); + } + + // TODO: Gio.IOErrorEnum: The connection is closed + // this._monitor.close_sync(null); + + GObject.signal_handlers_destroy(this); + } catch (e) { + debug(e); + } + } +}); + + +/** + * The service class for this component + */ +var Component = Listener; + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/pulseaudio.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/pulseaudio.js new file mode 100644 index 0000000..d6016bf --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/pulseaudio.js @@ -0,0 +1,265 @@ +'use strict'; + +const Tweener = imports.tweener.tweener; + +const GIRepository = imports.gi.GIRepository; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Config = imports.config; + + +// Add gnome-shell's typelib dir to the search path +const typelibDir = GLib.build_filenamev([Config.GNOME_SHELL_LIBDIR, 'gnome-shell']); +GIRepository.Repository.prepend_search_path(typelibDir); +GIRepository.Repository.prepend_library_path(typelibDir); + +const Gvc = imports.gi.Gvc; + + +/** + * Extend Gvc.MixerStream with a property for returning a user-visible name + */ +Object.defineProperty(Gvc.MixerStream.prototype, 'display_name', { + get: function () { + try { + if (!this.get_ports().length) + return this.description; + + return `${this.get_port().human_port} (${this.description})`; + } catch (e) { + return this.description; + } + }, +}); + + +/** + * A convenience wrapper for Gvc.MixerStream + */ +class Stream { + constructor(mixer, stream) { + this._mixer = mixer; + this._stream = stream; + + this._max = mixer.get_vol_max_norm(); + } + + get muted() { + return this._stream.is_muted; + } + + set muted(bool) { + this._stream.change_is_muted(bool); + } + + // Volume is a double in the range 0-1 + get volume() { + return Math.floor(100 * this._stream.volume / this._max) / 100; + } + + set volume(num) { + this._stream.volume = Math.floor(num * this._max); + this._stream.push_volume(); + } + + /** + * Gradually raise or lower the stream volume to @value + * + * @param {number} value - A number in the range 0-1 + * @param {number} [duration] - Duration to fade in seconds + */ + fade(value, duration = 1) { + Tweener.removeTweens(this); + + if (this._stream.volume > value) { + this._mixer.fading = true; + + Tweener.addTween(this, { + volume: value, + time: duration, + transition: 'easeOutCubic', + onComplete: () => { + this._mixer.fading = false; + }, + }); + } else if (this._stream.volume < value) { + this._mixer.fading = true; + + Tweener.addTween(this, { + volume: value, + time: duration, + transition: 'easeInCubic', + onComplete: () => { + this._mixer.fading = false; + }, + }); + } + } +} + + +/** + * A subclass of Gvc.MixerControl with convenience functions for controlling the + * default input/output volumes. + * + * The Mixer class uses GNOME Shell's Gvc library to control the system volume + * and offers a few convenience functions. + */ +const Mixer = GObject.registerClass({ + GTypeName: 'GSConnectAudioMixer', +}, class Mixer extends Gvc.MixerControl { + _init(params) { + super._init({name: 'GSConnect'}); + + this._previousVolume = undefined; + this._volumeMuted = false; + this._microphoneMuted = false; + + this.open(); + } + + get fading() { + if (this._fading === undefined) + this._fading = false; + + return this._fading; + } + + set fading(bool) { + if (this.fading === bool) + return; + + this._fading = bool; + + if (this.fading) + this.emit('stream-changed', this._output._stream.id); + } + + get input() { + if (this._input === undefined) + this.vfunc_default_source_changed(); + + return this._input; + } + + get output() { + if (this._output === undefined) + this.vfunc_default_sink_changed(); + + return this._output; + } + + vfunc_default_sink_changed(id) { + try { + const sink = this.get_default_sink(); + this._output = (sink) ? new Stream(this, sink) : null; + } catch (e) { + logError(e); + } + } + + vfunc_default_source_changed(id) { + try { + const source = this.get_default_source(); + this._input = (source) ? new Stream(this, source) : null; + } catch (e) { + logError(e); + } + } + + vfunc_state_changed(new_state) { + try { + if (new_state === Gvc.MixerControlState.READY) { + this.vfunc_default_sink_changed(null); + this.vfunc_default_source_changed(null); + } + } catch (e) { + logError(e); + } + } + + /** + * Store the current output volume then lower it to %15 + * + * @param {number} duration - Duration in seconds to fade + */ + lowerVolume(duration = 1) { + try { + if (this.output.volume > 0.15) { + this._previousVolume = Number(this.output.volume); + this.output.fade(0.15, duration); + } + } catch (e) { + logError(e); + } + } + + /** + * Mute the output volume (speakers) + */ + muteVolume() { + try { + if (this.output.muted) + return; + + this.output.muted = true; + this._volumeMuted = true; + } catch (e) { + logError(e); + } + } + + /** + * Mute the input volume (microphone) + */ + muteMicrophone() { + try { + if (this.input.muted) + return; + + this.input.muted = true; + this._microphoneMuted = true; + } catch (e) { + logError(e); + } + } + + /** + * Restore all mixer levels to their previous state + */ + restore() { + try { + // If we muted the microphone, unmute it before restoring the volume + if (this._microphoneMuted) { + this.input.muted = false; + this._microphoneMuted = false; + } + + // If we muted the volume, unmute it before restoring the volume + if (this._volumeMuted) { + this.output.muted = false; + this._volumeMuted = false; + } + + // If a previous volume is defined, raise it back up to that level + if (this._previousVolume !== undefined) { + this.output.fade(this._previousVolume); + this._previousVolume = undefined; + } + } catch (e) { + logError(e); + } + } + + destroy() { + this.close(); + } +}); + + +/** + * The service class for this component + */ +var Component = Mixer; + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/session.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/session.js new file mode 100644 index 0000000..b906e9b --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/session.js @@ -0,0 +1,116 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; + + +const Session = class { + constructor() { + this._connection = Gio.DBus.system; + this._session = null; + + this._initAsync(); + } + + async _initAsync() { + try { + const userName = GLib.get_user_name(); + const sessions = await this._listSessions(); + let sessionPath = '/org/freedesktop/login1/session/auto'; + + // eslint-disable-next-line no-unused-vars + for (const [num, uid, name, seat, objectPath] of sessions) { + if (name === userName) { + sessionPath = objectPath; + break; + } + } + + this._session = await this._getSession(sessionPath); + } catch (e) { + this._session = null; + logError(e); + } + } + + get idle() { + if (this._session === null) + return false; + + return this._session.get_cached_property('IdleHint').unpack(); + } + + get locked() { + if (this._session === null) + return false; + + return this._session.get_cached_property('LockedHint').unpack(); + } + + get active() { + // Active if not idle and not locked + return !(this.idle || this.locked); + } + + _listSessions() { + return new Promise((resolve, reject) => { + this._connection.call( + 'org.freedesktop.login1', + '/org/freedesktop/login1', + 'org.freedesktop.login1.Manager', + 'ListSessions', + null, + null, + Gio.DBusCallFlags.NONE, + -1, + null, + (connection, res) => { + try { + res = connection.call_finish(res); + resolve(res.deepUnpack()[0]); + } catch (e) { + reject(e); + } + } + ); + }); + } + + async _getSession(objectPath) { + const session = new Gio.DBusProxy({ + g_connection: this._connection, + g_name: 'org.freedesktop.login1', + g_object_path: objectPath, + g_interface_name: 'org.freedesktop.login1.Session', + }); + + // Initialize the proxy + await new Promise((resolve, reject) => { + session.init_async( + GLib.PRIORITY_DEFAULT, + null, + (proxy, res) => { + try { + resolve(proxy.init_finish(res)); + } catch (e) { + Gio.DBusError.strip_remote_error(e); + reject(e); + } + } + ); + }); + + return session; + } + + destroy() { + this._session = null; + } +}; + + +/** + * The service class for this component + */ +var Component = Session; + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/sound.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/sound.js new file mode 100644 index 0000000..4afd647 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/sound.js @@ -0,0 +1,185 @@ +'use strict'; + +const Gdk = imports.gi.Gdk; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; + + +/* + * Used to ensure 'audible-bell' is enabled for fallback + */ +const WM_SETTINGS = new Gio.Settings({ + schema_id: 'org.gnome.desktop.wm.preferences', + path: '/org/gnome/desktop/wm/preferences/', +}); + + +var Player = class Player { + + constructor() { + this._playing = new Set(); + } + + get backend() { + if (this._backend === undefined) { + // Prefer GSound + try { + this._gsound = new imports.gi.GSound.Context(); + this._gsound.init(null); + this._backend = 'gsound'; + + // Try falling back to libcanberra, otherwise just re-run the test + // in case one or the other is installed later + } catch (e) { + if (GLib.find_program_in_path('canberra-gtk-play') !== null) { + this._canberra = new Gio.SubprocessLauncher({ + flags: Gio.SubprocessFlags.NONE, + }); + this._backend = 'libcanberra'; + } else { + return null; + } + } + } + + return this._backend; + } + + _canberraPlaySound(name, cancellable) { + return new Promise((resolve, reject) => { + const proc = this._canberra.spawnv(['canberra-gtk-play', '-i', name]); + + proc.wait_check_async(cancellable, (proc, res) => { + try { + resolve(proc.wait_check_finish(res)); + } catch (e) { + reject(e); + } + }); + }); + } + + async _canberraLoopSound(name, cancellable) { + while (!cancellable.is_cancelled()) + await this._canberraPlaySound(name, cancellable); + } + + _gsoundPlaySound(name, cancellable) { + return new Promise((resolve, reject) => { + this._gsound.play_full( + {'event.id': name}, + cancellable, + (source, res) => { + try { + resolve(source.play_full_finish(res)); + } catch (e) { + reject(e); + } + } + ); + }); + } + + async _gsoundLoopSound(name, cancellable) { + while (!cancellable.is_cancelled()) + await this._gsoundPlaySound(name, cancellable); + } + + _gdkPlaySound(name, cancellable) { + if (this._display === undefined) + this._display = Gdk.Display.get_default(); + + let count = 0; + + GLib.timeout_add(GLib.PRIORITY_DEFAULT, 200, () => { + try { + if (count++ < 4 && !cancellable.is_cancelled()) { + this._display.beep(); + return GLib.SOURCE_CONTINUE; + } + + return GLib.SOURCE_REMOVE; + } catch (e) { + logError(e); + return GLib.SOURCE_REMOVE; + } + }); + + return !cancellable.is_cancelled(); + } + + _gdkLoopSound(name, cancellable) { + this._gdkPlaySound(name, cancellable); + GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + 1500, + this._gdkPlaySound.bind(this, name, cancellable) + ); + } + + async playSound(name, cancellable) { + try { + if (!(cancellable instanceof Gio.Cancellable)) + cancellable = new Gio.Cancellable(); + + this._playing.add(cancellable); + + switch (this.backend) { + case 'gsound': + await this._gsoundPlaySound(name, cancellable); + break; + + case 'canberra': + await this._canberraPlaySound(name, cancellable); + break; + + default: + await this._gdkPlaySound(name, cancellable); + } + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + } finally { + this._playing.delete(cancellable); + } + } + + async loopSound(name, cancellable) { + try { + if (!(cancellable instanceof Gio.Cancellable)) + cancellable = new Gio.Cancellable(); + + this._playing.add(cancellable); + + switch (this.backend) { + case 'gsound': + await this._gsoundLoopSound(name, cancellable); + break; + + case 'canberra': + await this._canberraLoopSound(name, cancellable); + break; + + default: + await this._gdkLoopSound(name, cancellable); + } + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + } finally { + this._playing.delete(cancellable); + } + } + + destroy() { + for (const cancellable of this._playing) + cancellable.cancel(); + } +}; + + +/** + * The service class for this component + */ +var Component = Player; + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/upower.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/upower.js new file mode 100644 index 0000000..20cc20b --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/upower.js @@ -0,0 +1,226 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + + +/** + * The warning level of a battery. + * + * @readonly + * @enum {number} + */ +const DeviceLevel = { + UNKNOWN: 0, + NONE: 1, + DISCHARGING: 2, + LOW: 3, + CRITICAL: 4, + ACTION: 5, + NORMAL: 6, + HIGH: 7, + FULL: 8, + LAST: 9, +}; + +/** + * The device state. + * + * @readonly + * @enum {number} + */ +const DeviceState = { + UNKNOWN: 0, + CHARGING: 1, + DISCHARGING: 2, + EMPTY: 3, + FULLY_CHARGED: 4, + PENDING_CHARGE: 5, + PENDING_DISCHARGE: 6, + LAST: 7, +}; + + +/** + * A class representing the system battery. + */ +var Battery = GObject.registerClass({ + GTypeName: 'GSConnectSystemBattery', + Signals: { + 'changed': { + flags: GObject.SignalFlags.RUN_FIRST, + }, + }, + Properties: { + 'charging': GObject.ParamSpec.boolean( + 'charging', + 'Charging', + 'The current charging state.', + GObject.ParamFlags.READABLE, + false + ), + 'level': GObject.ParamSpec.int( + 'level', + 'Level', + 'The current power level.', + GObject.ParamFlags.READABLE, + -1, 100, + -1 + ), + 'threshold': GObject.ParamSpec.uint( + 'threshold', + 'Threshold', + 'The current threshold state.', + GObject.ParamFlags.READABLE, + 0, 1, + 0 + ), + }, +}, class Battery extends GObject.Object { + + _init() { + super._init(); + + this._cancellable = new Gio.Cancellable(); + this._proxy = null; + this._propertiesChangedId = 0; + + this._loadUPower(); + } + + async _loadUPower() { + try { + this._proxy = new Gio.DBusProxy({ + g_bus_type: Gio.BusType.SYSTEM, + g_name: 'org.freedesktop.UPower', + g_object_path: '/org/freedesktop/UPower/devices/DisplayDevice', + g_interface_name: 'org.freedesktop.UPower.Device', + g_flags: Gio.DBusProxyFlags.DO_NOT_AUTO_START, + }); + + await new Promise((resolve, reject) => { + this._proxy.init_async( + GLib.PRIORITY_DEFAULT, + this._cancellable, + (proxy, res) => { + try { + resolve(proxy.init_finish(res)); + } catch (e) { + reject(e); + } + } + ); + }); + + this._propertiesChangedId = this._proxy.connect( + 'g-properties-changed', + this._onPropertiesChanged.bind(this) + ); + + this._initProperties(this._proxy); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { + const service = Gio.Application.get_default(); + + if (service !== null) + service.notify_error(e); + else + logError(e); + } + + this._proxy = null; + } + } + + _initProperties(proxy) { + if (proxy.g_name_owner === null) + return; + + const percentage = proxy.get_cached_property('Percentage').unpack(); + const state = proxy.get_cached_property('State').unpack(); + const level = proxy.get_cached_property('WarningLevel').unpack(); + + this._level = Math.floor(percentage); + this._charging = (state !== DeviceState.DISCHARGING); + this._threshold = (!this.charging && level >= DeviceLevel.LOW); + + this.emit('changed'); + } + + _onPropertiesChanged(proxy, changed, invalidated) { + let emitChanged = false; + const properties = changed.deepUnpack(); + + if (properties.hasOwnProperty('Percentage')) { + emitChanged = true; + + const value = proxy.get_cached_property('Percentage').unpack(); + this._level = Math.floor(value); + this.notify('level'); + } + + if (properties.hasOwnProperty('State')) { + emitChanged = true; + + const value = proxy.get_cached_property('State').unpack(); + this._charging = (value !== DeviceState.DISCHARGING); + this.notify('charging'); + } + + if (properties.hasOwnProperty('WarningLevel')) { + emitChanged = true; + + const value = proxy.get_cached_property('WarningLevel').unpack(); + this._threshold = (!this.charging && value >= DeviceLevel.LOW); + this.notify('threshold'); + } + + if (emitChanged) + this.emit('changed'); + } + + get charging() { + if (this._charging === undefined) + this._charging = false; + + return this._charging; + } + + get is_present() { + return (this._proxy && this._proxy.g_name_owner); + } + + get level() { + if (this._level === undefined) + this._level = -1; + + return this._level; + } + + get threshold() { + if (this._threshold === undefined) + this._threshold = 0; + + return this._threshold; + } + + destroy() { + if (this._cancellable.is_cancelled()) + return; + + this._cancellable.cancel(); + + if (this._proxy && this._propertiesChangedId > 0) { + this._proxy.disconnect(this._propertiesChangedId); + this._propertiesChangedId = 0; + } + } +}); + + +/** + * The service class for this component + */ +var Component = Battery; + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/core.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/core.js new file mode 100644 index 0000000..ac948fa --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/core.js @@ -0,0 +1,738 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + + +/** + * Get the local device type. + * + * @return {string} A device type string + */ +function _getDeviceType() { + try { + let type = GLib.file_get_contents('/sys/class/dmi/id/chassis_type')[1]; + + type = Number(imports.byteArray.toString(type)); + + if ([8, 9, 10, 14].includes(type)) + return 'laptop'; + + return 'desktop'; + } catch (e) { + return 'desktop'; + } +} + + +/** + * The packet class is a simple Object-derived class, offering some conveniences + * for working with KDE Connect packets. + */ +var Packet = class Packet { + + constructor(data = null) { + this.id = 0; + this.type = undefined; + this.body = {}; + + if (typeof data === 'string') + Object.assign(this, JSON.parse(data)); + else if (data !== null) + Object.assign(this, data); + } + + [Symbol.toPrimitive](hint) { + this.id = Date.now(); + + if (hint === 'string') + return `${JSON.stringify(this)}\n`; + + if (hint === 'number') + return `${JSON.stringify(this)}\n`.length; + + return true; + } + + get [Symbol.toStringTag]() { + return `Packet:${this.type}`; + } + + /** + * Deserialize and return a new Packet from an Object or string. + * + * @param {Object|string} data - A string or dictionary to deserialize + * @return {Core.Packet} A new packet object + */ + static deserialize(data) { + return new Packet(data); + } + + /** + * Serialize the packet as a single line with a terminating new-line (`\n`) + * character, ready to be written to a channel. + * + * @return {string} A serialized packet + */ + serialize() { + this.id = Date.now(); + return `${JSON.stringify(this)}\n`; + } + + /** + * Update the packet from a dictionary or string of JSON + * + * @param {Object|string} source - Source data + */ + update(source) { + try { + if (typeof data === 'string') + Object.assign(this, JSON.parse(source)); + else + Object.assign(this, source); + } catch (e) { + throw Error(`Malformed data: ${e.message}`); + } + } + + /** + * Check if the packet has a payload. + * + * @return {boolean} %true if @packet has a payload + */ + hasPayload() { + if (!this.hasOwnProperty('payloadSize')) + return false; + + if (!this.hasOwnProperty('payloadTransferInfo')) + return false; + + return (Object.keys(this.payloadTransferInfo).length > 0); + } +}; + + +/** + * Channel objects handle KDE Connect packet exchange and data transfers for + * devices. The implementation is responsible for all negotiation of the + * underlying protocol. + */ +var Channel = GObject.registerClass({ + GTypeName: 'GSConnectChannel', + Properties: { + 'closed': GObject.ParamSpec.boolean( + 'closed', + 'Closed', + 'Whether the channel has been closed', + GObject.ParamFlags.READABLE, + false + ), + }, +}, class Channel extends GObject.Object { + + get address() { + throw new GObject.NotImplementedError(); + } + + get backend() { + if (this._backend === undefined) + this._backend = null; + + return this._backend; + } + + set backend(backend) { + this._backend = backend; + } + + get cancellable() { + if (this._cancellable === undefined) + this._cancellable = new Gio.Cancellable(); + + return this._cancellable; + } + + get closed() { + if (this._closed === undefined) + this._closed = false; + + return this._closed; + } + + get input_stream() { + if (this._input_stream === undefined) { + if (this._connection instanceof Gio.IOStream) + return this._connection.get_input_stream(); + + return null; + } + + return this._input_stream; + } + + set input_stream(stream) { + this._input_stream = stream; + } + + get output_stream() { + if (this._output_stream === undefined) { + if (this._connection instanceof Gio.IOStream) + return this._connection.get_output_stream(); + + return null; + } + + return this._output_stream; + } + + set output_stream(stream) { + this._output_stream = stream; + } + + get uuid() { + if (this._uuid === undefined) + this._uuid = GLib.uuid_string_random(); + + return this._uuid; + } + + set uuid(uuid) { + this._uuid = uuid; + } + + /** + * Close the channel. + */ + close() { + throw new GObject.NotImplementedError(); + } + + /** + * Read a packet. + * + * @param {Gio.Cancellable} [cancellable] - A cancellable + * @return {Promise} The packet + */ + readPacket(cancellable = null) { + if (cancellable === null) + cancellable = this.cancellable; + + if (!(this.input_stream instanceof Gio.DataInputStream)) { + this.input_stream = new Gio.DataInputStream({ + base_stream: this.input_stream, + }); + } + + return new Promise((resolve, reject) => { + this.input_stream.read_line_async( + GLib.PRIORITY_DEFAULT, + cancellable, + (stream, res) => { + try { + const data = stream.read_line_finish_utf8(res)[0]; + + if (data === null) { + throw new Gio.IOErrorEnum({ + message: 'End of stream', + code: Gio.IOErrorEnum.CONNECTION_CLOSED, + }); + } + + resolve(new Packet(data)); + } catch (e) { + reject(e); + } + } + ); + }); + } + + /** + * Send a packet. + * + * @param {Core.Packet} packet - The packet to send + * @param {Gio.Cancellable} [cancellable] - A cancellable + * @return {Promise} %true if successful + */ + sendPacket(packet, cancellable = null) { + if (cancellable === null) + cancellable = this.cancellable; + + return new Promise((resolve, reject) => { + this.output_stream.write_all_async( + packet.serialize(), + GLib.PRIORITY_DEFAULT, + cancellable, + (stream, res) => { + try { + resolve(stream.write_all_finish(res)); + } catch (e) { + reject(e); + } + } + ); + }); + } + + /** + * Reject a transfer. + * + * @param {Core.Packet} packet - A packet with payload info + */ + rejectTransfer(packet) { + throw new GObject.NotImplementedError(); + } + + /** + * Download a payload from a device. Typically implementations will override + * this with an async function. + * + * @param {Core.Packet} packet - A packet + * @param {Gio.OutputStream} target - The target stream + * @param {Gio.Cancellable} [cancellable] - A cancellable for the upload + */ + download(packet, target, cancellable = null) { + throw new GObject.NotImplementedError(); + } + + + /** + * Upload a payload to a device. Typically implementations will override + * this with an async function. + * + * @param {Core.Packet} packet - The packet describing the transfer + * @param {Gio.InputStream} source - The source stream + * @param {number} size - The payload size + * @param {Gio.Cancellable} [cancellable] - A cancellable for the upload + */ + upload(packet, source, size, cancellable = null) { + throw new GObject.NotImplementedError(); + } +}); + + +/** + * ChannelService implementations provide Channel objects, emitting the + * ChannelService::channel signal when a new connection has been accepted. + */ +var ChannelService = GObject.registerClass({ + GTypeName: 'GSConnectChannelService', + Properties: { + 'active': GObject.ParamSpec.boolean( + 'active', + 'Active', + 'Whether the service is active', + GObject.ParamFlags.READABLE, + false + ), + 'id': GObject.ParamSpec.string( + 'id', + 'ID', + 'The hostname or other network unique id', + GObject.ParamFlags.READWRITE, + null + ), + 'name': GObject.ParamSpec.string( + 'name', + 'Name', + 'The name of the backend', + GObject.ParamFlags.READWRITE, + null + ), + }, + Signals: { + 'channel': { + flags: GObject.SignalFlags.RUN_LAST, + param_types: [Channel.$gtype], + return_type: GObject.TYPE_BOOLEAN, + }, + }, +}, class ChannelService extends GObject.Object { + + get active() { + if (this._active === undefined) + this._active = false; + + return this._active; + } + + get name() { + if (this._name === undefined) + this._name = GLib.get_host_name(); + + return this._name; + } + + set name(name) { + if (this.name === name) + return; + + this._name = name; + this.notify('name'); + } + + get id() { + if (this._id === undefined) + this._id = GLib.uuid_string_random(); + + return this._id; + } + + set id(id) { + if (this.id === id) + return; + + this._id = id; + } + + get identity() { + if (this._identity === undefined) + this.buildIdentity(); + + return this._identity; + } + + /** + * Broadcast directly to @address or the whole network if %null + * + * @param {string} [address] - A string address + */ + broadcast(address = null) { + throw new GObject.NotImplementedError(); + } + + /** + * Rebuild the identity packet used to identify the local device. An + * implementation may override this to make modifications to the default + * capabilities if necessary (eg. bluez without SFTP support). + */ + buildIdentity() { + this._identity = new Packet({ + id: 0, + type: 'kdeconnect.identity', + body: { + deviceId: this.id, + deviceName: this.name, + deviceType: _getDeviceType(), + protocolVersion: 7, + incomingCapabilities: [], + outgoingCapabilities: [], + }, + }); + + for (const name in imports.service.plugins) { + // Exclude mousepad/presenter capability in unsupported sessions + if (!HAVE_REMOTEINPUT && ['mousepad', 'presenter'].includes(name)) + continue; + + const meta = imports.service.plugins[name].Metadata; + + for (const type of meta.incomingCapabilities) + this._identity.body.incomingCapabilities.push(type); + + for (const type of meta.outgoingCapabilities) + this._identity.body.outgoingCapabilities.push(type); + } + } + + /** + * Emit Core.ChannelService::channel + * + * @param {Core.Channel} channel - The new channel + */ + channel(channel) { + if (!this.emit('channel', channel)) + channel.close(); + } + + /** + * Start the channel service. Implementations should throw an error if the + * service fails to meet any of its requirements for opening or accepting + * connections. + */ + start() { + throw new GObject.NotImplementedError(); + } + + /** + * Stop the channel service. + */ + stop() { + throw new GObject.NotImplementedError(); + } + + /** + * Destroy the channel service. + */ + destroy() { + } +}); + + +/** + * A class representing a file transfer. + */ +var Transfer = GObject.registerClass({ + GTypeName: 'GSConnectTransfer', + Properties: { + 'channel': GObject.ParamSpec.object( + 'channel', + 'Channel', + 'The channel that owns this transfer', + GObject.ParamFlags.READWRITE, + Channel.$gtype + ), + 'completed': GObject.ParamSpec.boolean( + 'completed', + 'Completed', + 'Whether the transfer has completed', + GObject.ParamFlags.READABLE, + false + ), + 'device': GObject.ParamSpec.object( + 'device', + 'Device', + 'The device that created this transfer', + GObject.ParamFlags.READWRITE, + GObject.Object.$gtype + ), + }, +}, class Transfer extends GObject.Object { + + _init(params = {}) { + super._init(params); + + this._cancellable = new Gio.Cancellable(); + this._items = []; + } + + get channel() { + if (this._channel === undefined) + this._channel = null; + + return this._channel; + } + + set channel(channel) { + if (this.channel === channel) + return; + + this._channel = channel; + } + + get completed() { + if (this._completed === undefined) + this._completed = false; + + return this._completed; + } + + get device() { + if (this._device === undefined) + this._device = null; + + return this._device; + } + + set device(device) { + if (this.device === device) + return; + + this._device = device; + } + + get uuid() { + if (this._uuid === undefined) + this._uuid = GLib.uuid_string_random(); + + return this._uuid; + } + + /** + * Ensure there is a stream for the transfer item. + * + * @param {Object} item - A transfer item + * @param {Gio.Cancellable} [cancellable] - A cancellable + */ + async _ensureStream(item, cancellable = null) { + // This is an upload from a remote device + if (item.packet.hasPayload()) { + if (item.target instanceof Gio.OutputStream) + return; + + if (item.file instanceof Gio.File) { + item.target = await new Promise((resolve, reject) => { + item.file.replace_async( + null, + false, + Gio.FileCreateFlags.REPLACE_DESTINATION, + GLib.PRIORITY_DEFAULT, + this._cancellable, + (file, res) => { + try { + resolve(file.replace_finish(res)); + } catch (e) { + reject(e); + } + } + ); + }); + } + } else { + if (item.source instanceof Gio.InputStream) + return; + + if (item.file instanceof Gio.File) { + const read = new Promise((resolve, reject) => { + item.file.read_async( + GLib.PRIORITY_DEFAULT, + cancellable, + (file, res) => { + try { + resolve(file.read_finish(res)); + } catch (e) { + reject(e); + } + } + ); + }); + + const query = new Promise((resolve, reject) => { + item.file.query_info_async( + Gio.FILE_ATTRIBUTE_STANDARD_SIZE, + Gio.FileQueryInfoFlags.NONE, + GLib.PRIORITY_DEFAULT, + cancellable, + (file, res) => { + try { + resolve(file.query_info_finish(res)); + } catch (e) { + reject(e); + } + } + ); + }); + + const [stream, info] = await Promise.all([read, query]); + item.source = stream; + item.size = info.get_size(); + } + } + } + + /** + * Add a file to the transfer. + * + * @param {Core.Packet} packet - A packet + * @param {Gio.File} file - A file to transfer + */ + addFile(packet, file) { + const item = { + packet: new Packet(packet), + file: file, + source: null, + target: null, + }; + + this._items.push(item); + } + + /** + * Add a filepath to the transfer. + * + * @param {Core.Packet} packet - A packet + * @param {string} path - A filepath to transfer + */ + addPath(packet, path) { + const item = { + packet: new Packet(packet), + file: Gio.File.new_for_path(path), + source: null, + target: null, + }; + + this._items.push(item); + } + + /** + * Add a stream to the transfer. + * + * @param {Core.Packet} packet - A packet + * @param {Gio.InputStream|Gio.OutputStream} stream - A stream to transfer + * @param {number} [size] - Payload size + */ + addStream(packet, stream, size = 0) { + const item = { + packet: new Packet(packet), + file: null, + source: null, + target: null, + size: size, + }; + + if (stream instanceof Gio.InputStream) + item.source = stream; + else if (stream instanceof Gio.OutputStream) + item.target = stream; + + this._items.push(item); + } + + /** + * Execute a transfer operation. Implementations may override this, while + * the default uses g_output_stream_splice(). + * + * @param {Gio.Cancellable} [cancellable] - A cancellable + */ + async start(cancellable = null) { + let error = null; + + try { + let item; + + // If a cancellable is passed in, chain to its signal + if (cancellable instanceof Gio.Cancellable) + cancellable.connect(() => this._cancellable.cancel()); + + while ((item = this._items.shift())) { + // If created for a device, ignore connection changes by + // ensuring we have the most recent channel + if (this.device !== null) + this._channel = this.device.channel; + + // TODO: transfer queueing? + if (this.channel === null || this.channel.closed) { + throw new Gio.IOErrorEnum({ + code: Gio.IOErrorEnum.CONNECTION_CLOSED, + message: 'Channel is closed', + }); + } + + await this._ensureStream(item, this._cancellable); + + if (item.packet.hasPayload()) { + await this.channel.download(item.packet, item.target, + this._cancellable); + } else { + await this.channel.upload(item.packet, item.source, + item.size, this._cancellable); + } + } + } catch (e) { + error = e; + } finally { + this._completed = true; + this.notify('completed'); + } + + if (error !== null) + throw error; + } + + cancel() { + if (this._cancellable.is_cancelled() === false) + this._cancellable.cancel(); + } +}); + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/daemon.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/daemon.js new file mode 100755 index 0000000..30bd8c4 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/daemon.js @@ -0,0 +1,728 @@ +#!/usr/bin/env gjs + +'use strict'; + +// Allow TLSv1.0 certificates +// See https://github.com/andyholmes/gnome-shell-extension-gsconnect/issues/930 +imports.gi.GLib.setenv('G_TLS_GNUTLS_PRIORITY', 'NORMAL:%COMPAT:+VERS-TLS1.0', true); + +imports.gi.versions.Gdk = '3.0'; +imports.gi.versions.GdkPixbuf = '2.0'; +imports.gi.versions.Gio = '2.0'; +imports.gi.versions.GIRepository = '2.0'; +imports.gi.versions.GLib = '2.0'; +imports.gi.versions.GObject = '2.0'; +imports.gi.versions.Gtk = '3.0'; +imports.gi.versions.Pango = '1.0'; + +const Gdk = imports.gi.Gdk; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + + +// Bootstrap +function get_datadir() { + const m = /@(.+):\d+/.exec((new Error()).stack.split('\n')[1]); + return Gio.File.new_for_path(m[1]).get_parent().get_parent().get_path(); +} + +imports.searchPath.unshift(get_datadir()); +imports.config.PACKAGE_DATADIR = imports.searchPath[0]; + + +// Local Imports +const Config = imports.config; +const Manager = imports.service.manager; +const ServiceUI = imports.service.ui.service; + + +/** + * Class representing the GSConnect service daemon. + */ +const Service = GObject.registerClass({ + GTypeName: 'GSConnectService', +}, class Service extends Gtk.Application { + + _init() { + super._init({ + application_id: 'org.gnome.Shell.Extensions.GSConnect', + flags: Gio.ApplicationFlags.HANDLES_OPEN, + resource_base_path: '/org/gnome/Shell/Extensions/GSConnect', + }); + + GLib.set_prgname('gsconnect'); + GLib.set_application_name('GSConnect'); + + // Command-line + this._initOptions(); + } + + get settings() { + if (this._settings === undefined) { + this._settings = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup(Config.APP_ID, true), + }); + } + + return this._settings; + } + + /* + * GActions + */ + _initActions() { + const actions = [ + ['connect', this._identify.bind(this), new GLib.VariantType('s')], + ['device', this._device.bind(this), new GLib.VariantType('(ssbv)')], + ['error', this._error.bind(this), new GLib.VariantType('a{ss}')], + ['preferences', this._preferences, null], + ['quit', () => this.quit(), null], + ['refresh', this._identify.bind(this), null], + ]; + + for (const [name, callback, type] of actions) { + const action = new Gio.SimpleAction({ + name: name, + parameter_type: type, + }); + action.connect('activate', callback); + this.add_action(action); + } + } + + /** + * A wrapper for Device GActions. This is used to route device notification + * actions to their device, since GNotifications need an 'app' level action. + * + * @param {Gio.Action} action - The GAction + * @param {GLib.Variant} parameter - The activation parameter + */ + _device(action, parameter) { + try { + parameter = parameter.unpack(); + + // Select the appropriate device(s) + let devices; + const id = parameter[0].unpack(); + + if (id === '*') + devices = this.manager.devices.values(); + else + devices = [this.manager.devices.get(id)]; + + // Unpack the action data and activate the action + const name = parameter[1].unpack(); + const target = parameter[2].unpack() ? parameter[3].unpack() : null; + + for (const device of devices) + device.activate_action(name, target); + } catch (e) { + logError(e); + } + } + + _error(action, parameter) { + try { + const error = parameter.deepUnpack(); + + // If there's a URL, we have better information in the Wiki + if (error.url !== undefined) { + Gio.AppInfo.launch_default_for_uri_async( + error.url, + null, + null, + null + ); + return; + } + + const dialog = new ServiceUI.ErrorDialog(error); + dialog.present(); + } catch (e) { + logError(e); + } + } + + _identify(action, parameter) { + try { + let uri = null; + + if (parameter instanceof GLib.Variant) + uri = parameter.unpack(); + + this.manager.identify(uri); + } catch (e) { + logError(e); + } + } + + _preferences() { + Gio.Subprocess.new( + [`${Config.PACKAGE_DATADIR}/gsconnect-preferences`], + Gio.SubprocessFlags.NONE + ); + } + + /** + * Report a service-level error + * + * @param {Object} error - An Error or object with name, message and stack + */ + notify_error(error) { + try { + // Always log the error + logError(error); + + // Create an new notification + let id, body, priority; + const notif = new Gio.Notification(); + const icon = new Gio.ThemedIcon({name: 'dialog-error'}); + let target = null; + + if (error.name === undefined) + error.name = 'Error'; + + if (error.url !== undefined) { + id = error.url; + body = _('Click for help troubleshooting'); + priority = Gio.NotificationPriority.URGENT; + + target = new GLib.Variant('a{ss}', { + name: error.name.trim(), + message: error.message.trim(), + stack: error.stack.trim(), + url: error.url, + }); + } else { + id = error.message.trim(); + body = _('Click for more information'); + priority = Gio.NotificationPriority.HIGH; + + target = new GLib.Variant('a{ss}', { + name: error.name.trim(), + message: error.message.trim(), + stack: error.stack.trim(), + }); + } + + notif.set_title(`GSConnect: ${error.name.trim()}`); + notif.set_body(body); + notif.set_icon(icon); + notif.set_priority(priority); + notif.set_default_action_and_target('app.error', target); + + this.send_notification(id, notif); + } catch (e) { + logError(e); + } + } + + vfunc_activate() { + super.vfunc_activate(); + } + + vfunc_startup() { + super.vfunc_startup(); + + this.hold(); + + // Watch *this* file and stop the service if it's updated/uninstalled + this._serviceMonitor = Gio.File.new_for_path( + `${Config.PACKAGE_DATADIR}/service/daemon.js` + ).monitor(Gio.FileMonitorFlags.WATCH_MOVES, null); + this._serviceMonitor.connect('changed', () => this.quit()); + + // Init some resources + const provider = new Gtk.CssProvider(); + provider.load_from_resource(`${Config.APP_PATH}/application.css`); + Gtk.StyleContext.add_provider_for_screen( + Gdk.Screen.get_default(), + provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ); + + // Ensure our handlers are registered + try { + const appInfo = Gio.DesktopAppInfo.new(`${Config.APP_ID}.desktop`); + appInfo.add_supports_type('x-scheme-handler/sms'); + appInfo.add_supports_type('x-scheme-handler/tel'); + } catch (e) { + debug(e); + } + + // GActions & GSettings + this._initActions(); + + this.manager.start(); + } + + vfunc_dbus_register(connection, object_path) { + if (!super.vfunc_dbus_register(connection, object_path)) + return false; + + this.manager = new Manager.Manager({ + connection: connection, + object_path: object_path, + }); + + return true; + } + + vfunc_dbus_unregister(connection, object_path) { + this.manager.destroy(); + + super.vfunc_dbus_unregister(connection, object_path); + } + + vfunc_open(files, hint) { + super.vfunc_open(files, hint); + + for (const file of files) { + let action, parameter, title; + + try { + switch (file.get_uri_scheme()) { + case 'sms': + title = _('Send SMS'); + action = 'uriSms'; + parameter = new GLib.Variant('s', file.get_uri()); + break; + + case 'tel': + title = _('Dial Number'); + action = 'shareUri'; + parameter = new GLib.Variant('s', file.get_uri()); + break; + + case 'file': + title = _('Share File'); + action = 'shareFile'; + parameter = new GLib.Variant('(sb)', [file.get_uri(), false]); + break; + + default: + throw new Error(`Unsupported URI: ${file.get_uri()}`); + } + + // Show chooser dialog + new ServiceUI.DeviceChooser({ + title: title, + action_name: action, + action_target: parameter, + }); + } catch (e) { + logError(e, `GSConnect: Opening ${file.get_uri()}`); + } + } + } + + vfunc_shutdown() { + // Dispose GSettings + if (this._settings !== undefined) + this.settings.run_dispose(); + + this.manager.stop(); + + // Exhaust the event loop to ensure any pending operations complete + const context = GLib.MainContext.default(); + + while (context.iteration(false)) + continue; + + // Force a GC to prevent any more calls back into JS, then chain-up + imports.system.gc(); + super.vfunc_shutdown(); + } + + /* + * CLI + */ + _initOptions() { + /* + * Device Listings + */ + this.add_main_option( + 'list-devices', + 'l'.charCodeAt(0), + GLib.OptionFlags.NONE, + GLib.OptionArg.NONE, + _('List available devices'), + null + ); + + this.add_main_option( + 'list-all', + 'a'.charCodeAt(0), + GLib.OptionFlags.NONE, + GLib.OptionArg.NONE, + _('List all devices'), + null + ); + + this.add_main_option( + 'device', + 'd'.charCodeAt(0), + GLib.OptionFlags.NONE, + GLib.OptionArg.STRING, + _('Target Device'), + '' + ); + + /** + * Pairing + */ + this.add_main_option( + 'pair', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.NONE, + _('Pair'), + null + ); + + this.add_main_option( + 'unpair', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.NONE, + _('Unpair'), + null + ); + + /* + * Messaging + */ + this.add_main_option( + 'message', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.STRING_ARRAY, + _('Send SMS'), + '' + ); + + this.add_main_option( + 'message-body', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.STRING, + _('Message Body'), + '' + ); + + /* + * Notifications + */ + this.add_main_option( + 'notification', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.STRING, + _('Send Notification'), + '' + ); + + this.add_main_option( + 'notification-appname', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.STRING, + _('Notification App Name'), + '<name>' + ); + + this.add_main_option( + 'notification-body', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.STRING, + _('Notification Body'), + '<text>' + ); + + this.add_main_option( + 'notification-icon', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.STRING, + _('Notification Icon'), + '<icon-name>' + ); + + this.add_main_option( + 'notification-id', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.STRING, + _('Notification ID'), + '<id>' + ); + + this.add_main_option( + 'photo', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.NONE, + _('Photo'), + null + ); + + this.add_main_option( + 'ping', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.NONE, + _('Ping'), + null + ); + + this.add_main_option( + 'ring', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.NONE, + _('Ring'), + null + ); + + /* + * Sharing + */ + this.add_main_option( + 'share-file', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.FILENAME_ARRAY, + _('Share File'), + '<filepath|URI>' + ); + + this.add_main_option( + 'share-link', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.STRING_ARRAY, + _('Share Link'), + '<URL>' + ); + + this.add_main_option( + 'share-text', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.STRING, + _('Share Text'), + '<text>' + ); + + /* + * Misc + */ + this.add_main_option( + 'version', + 'v'.charCodeAt(0), + GLib.OptionFlags.NONE, + GLib.OptionArg.NONE, + _('Show release version'), + null + ); + } + + _cliAction(id, name, parameter = null) { + const parameters = []; + + if (parameter instanceof GLib.Variant) + parameters[0] = parameter; + + id = id.replace(/\W+/g, '_'); + + Gio.DBus.session.call_sync( + 'org.gnome.Shell.Extensions.GSConnect', + `/org/gnome/Shell/Extensions/GSConnect/Device/${id}`, + 'org.gtk.Actions', + 'Activate', + GLib.Variant.new('(sava{sv})', [name, parameters, {}]), + null, + Gio.DBusCallFlags.NONE, + -1, + null + ); + } + + _cliListDevices(full = true) { + const result = Gio.DBus.session.call_sync( + 'org.gnome.Shell.Extensions.GSConnect', + '/org/gnome/Shell/Extensions/GSConnect', + 'org.freedesktop.DBus.ObjectManager', + 'GetManagedObjects', + null, + null, + Gio.DBusCallFlags.NONE, + -1, + null + ); + + const variant = result.unpack()[0].unpack(); + let device; + + for (let object of Object.values(variant)) { + object = object.recursiveUnpack(); + device = object['org.gnome.Shell.Extensions.GSConnect.Device']; + + if (full) + print(`${device.Id}\t${device.Name}\t${device.Connected}\t${device.Paired}`); + else if (device.Connected && device.Paired) + print(device.Id); + } + } + + _cliMessage(id, options) { + if (!options.contains('message-body')) + throw new TypeError('missing --message-body option'); + + // TODO: currently we only support single-recipient messaging + const addresses = options.lookup_value('message', null).deepUnpack(); + const body = options.lookup_value('message-body', null).deepUnpack(); + + this._cliAction( + id, + 'sendSms', + GLib.Variant.new('(ss)', [addresses[0], body]) + ); + } + + _cliNotify(id, options) { + const title = options.lookup_value('notification', null).unpack(); + let body = ''; + let icon = null; + let nid = `${Date.now()}`; + let appName = 'GSConnect CLI'; + + if (options.contains('notification-id')) + nid = options.lookup_value('notification-id', null).unpack(); + + if (options.contains('notification-body')) + body = options.lookup_value('notification-body', null).unpack(); + + if (options.contains('notification-appname')) + appName = options.lookup_value('notification-appname', null).unpack(); + + if (options.contains('notification-icon')) { + icon = options.lookup_value('notification-icon', null).unpack(); + icon = Gio.Icon.new_for_string(icon); + } else { + icon = new Gio.ThemedIcon({ + name: 'org.gnome.Shell.Extensions.GSConnect', + }); + } + + const notification = new GLib.Variant('a{sv}', { + appName: GLib.Variant.new_string(appName), + id: GLib.Variant.new_string(nid), + title: GLib.Variant.new_string(title), + text: GLib.Variant.new_string(body), + ticker: GLib.Variant.new_string(`${title}: ${body}`), + time: GLib.Variant.new_string(`${Date.now()}`), + isClearable: GLib.Variant.new_boolean(true), + icon: icon.serialize(), + }); + + this._cliAction(id, 'sendNotification', notification); + } + + _cliShareFile(device, options) { + const files = options.lookup_value('share-file', null).deepUnpack(); + + for (let file of files) { + file = imports.byteArray.toString(file); + this._cliAction(device, 'shareFile', GLib.Variant.new('(sb)', [file, false])); + } + } + + _cliShareLink(device, options) { + const uris = options.lookup_value('share-link', null).unpack(); + + for (const uri of uris) + this._cliAction(device, 'shareUri', uri); + } + + _cliShareText(device, options) { + const text = options.lookup_value('share-text', null).unpack(); + + this._cliAction(device, 'shareText', GLib.Variant.new_string(text)); + } + + vfunc_handle_local_options(options) { + try { + if (options.contains('version')) { + print(`GSConnect ${Config.PACKAGE_VERSION}`); + return 0; + } + + this.register(null); + + if (options.contains('list-devices')) { + this._cliListDevices(false); + return 0; + } + + if (options.contains('list-all')) { + this._cliListDevices(true); + return 0; + } + + // We need a device for anything else; exit since this is probably + // the daemon being started. + if (!options.contains('device')) + return -1; + + const id = options.lookup_value('device', null).unpack(); + + // Pairing + if (options.contains('pair')) { + this._cliAction(id, 'pair'); + return 0; + } + + if (options.contains('unpair')) { + this._cliAction(id, 'unpair'); + return 0; + } + + // Plugins + if (options.contains('message')) + this._cliMessage(id, options); + + if (options.contains('notification')) + this._cliNotify(id, options); + + if (options.contains('photo')) + this._cliAction(id, 'photo'); + + if (options.contains('ping')) + this._cliAction(id, 'ping', GLib.Variant.new_string('')); + + if (options.contains('ring')) + this._cliAction(id, 'ring'); + + if (options.contains('share-file')) + this._cliShareFile(id, options); + + if (options.contains('share-link')) + this._cliShareLink(id, options); + + if (options.contains('share-text')) + this._cliShareText(id, options); + + return 0; + } catch (e) { + logError(e); + return 1; + } + } +}); + +(new Service()).run([imports.system.programInvocationName].concat(ARGV)); + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/device.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/device.js new file mode 100644 index 0000000..eb0e915 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/device.js @@ -0,0 +1,1051 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Config = imports.config; +const Components = imports.service.components; +const Core = imports.service.core; + + +/** + * An object representing a remote device. + * + * Device class is subclassed from Gio.SimpleActionGroup so it implements the + * GActionGroup and GActionMap interfaces, like Gio.Application. + * + */ +var Device = GObject.registerClass({ + GTypeName: 'GSConnectDevice', + Properties: { + 'connected': GObject.ParamSpec.boolean( + 'connected', + 'Connected', + 'Whether the device is connected', + GObject.ParamFlags.READABLE, + false + ), + 'contacts': GObject.ParamSpec.object( + 'contacts', + 'Contacts', + 'The contacts store for this device', + GObject.ParamFlags.READABLE, + GObject.Object + ), + 'encryption-info': GObject.ParamSpec.string( + 'encryption-info', + 'Encryption Info', + 'A formatted string with the local and remote fingerprints', + GObject.ParamFlags.READABLE, + null + ), + 'icon-name': GObject.ParamSpec.string( + 'icon-name', + 'Icon Name', + 'Icon name representing the device', + GObject.ParamFlags.READABLE, + null + ), + 'id': GObject.ParamSpec.string( + 'id', + 'Id', + 'The device hostname or other network unique id', + GObject.ParamFlags.READABLE, + '' + ), + 'name': GObject.ParamSpec.string( + 'name', + 'Name', + 'The device name', + GObject.ParamFlags.READABLE, + null + ), + 'paired': GObject.ParamSpec.boolean( + 'paired', + 'Paired', + 'Whether the device is paired', + GObject.ParamFlags.READABLE, + false + ), + 'type': GObject.ParamSpec.string( + 'type', + 'Type', + 'The device type', + GObject.ParamFlags.READABLE, + null + ), + }, +}, class Device extends Gio.SimpleActionGroup { + + _init(identity) { + super._init(); + + this._id = identity.body.deviceId; + + // GLib.Source timeout id's for pairing requests + this._incomingPairRequest = 0; + this._outgoingPairRequest = 0; + + // Maps of name->Plugin, packet->Plugin, uuid->Transfer + this._plugins = new Map(); + this._handlers = new Map(); + this._procs = new Set(); + this._transfers = new Map(); + + this._outputLock = false; + this._outputQueue = []; + + // GSettings + this.settings = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup( + 'org.gnome.Shell.Extensions.GSConnect.Device', + true + ), + path: `/org/gnome/shell/extensions/gsconnect/device/${this.id}/`, + }); + + // Watch for changes to supported and disabled plugins + this._disabledPluginsChangedId = this.settings.connect( + 'changed::disabled-plugins', + this._onAllowedPluginsChanged.bind(this) + ); + this._supportedPluginsChangedId = this.settings.connect( + 'changed::supported-plugins', + this._onAllowedPluginsChanged.bind(this) + ); + + this._registerActions(); + this.menu = new Gio.Menu(); + + // Parse identity if initialized with a proper packet, otherwise load + if (identity.id !== undefined) + this._handleIdentity(identity); + else + this._loadPlugins(); + } + + get channel() { + if (this._channel === undefined) + this._channel = null; + + return this._channel; + } + + get connected() { + if (this._connected === undefined) + this._connected = false; + + return this._connected; + } + + get connection_type() { + const lastConnection = this.settings.get_string('last-connection'); + + return lastConnection.split('://')[0]; + } + + get contacts() { + const contacts = this._plugins.get('contacts'); + + if (contacts && contacts.settings.get_boolean('contacts-source')) + return contacts._store; + + if (this._contacts === undefined) + this._contacts = Components.acquire('contacts'); + + return this._contacts; + } + + // FIXME: backend should do this stuff + get encryption_info() { + let remoteFingerprint = _('Not available'); + let localFingerprint = _('Not available'); + + // Bluetooth connections have no certificate so we use the host address + if (this.connection_type === 'bluetooth') { + // TRANSLATORS: Bluetooth address for remote device + return _('Bluetooth device at %s').format('???'); + + // If the device is connected use the certificate from the connection + } else if (this.connected) { + remoteFingerprint = this.channel.peer_certificate.sha256(); + + // Otherwise pull it out of the settings + } else if (this.paired) { + remoteFingerprint = Gio.TlsCertificate.new_from_pem( + this.settings.get_string('certificate-pem'), + -1 + ).sha256(); + } + + // FIXME: another ugly reach-around + let lanBackend; + + if (this.service !== null) + lanBackend = this.service.manager.backends.get('lan'); + + if (lanBackend && lanBackend.certificate) + localFingerprint = lanBackend.certificate.sha256(); + + // TRANSLATORS: Label for TLS Certificate fingerprint + // + // Example: + // + // Google Pixel Fingerprint: + // 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 + return _('%s Fingerprint:').format(this.name) + '\n' + + remoteFingerprint + '\n\n' + + _('%s Fingerprint:').format('GSConnect') + '\n' + + localFingerprint; + } + + get id() { + return this._id; + } + + get name() { + return this.settings.get_string('name'); + } + + get paired() { + return this.settings.get_boolean('paired'); + } + + get icon_name() { + switch (this.type) { + case 'laptop': + return 'laptop-symbolic'; + case 'phone': + return 'smartphone-symbolic'; + case 'tablet': + return 'tablet-symbolic'; + case 'tv': + return 'tv-symbolic'; + case 'desktop': + default: + return 'computer-symbolic'; + } + } + + get service() { + if (this._service === undefined) + this._service = Gio.Application.get_default(); + + return this._service; + } + + get type() { + return this.settings.get_string('type'); + } + + _handleIdentity(packet) { + this.freeze_notify(); + + // If we're connected, record the reconnect URI + if (this.channel !== null) + this.settings.set_string('last-connection', this.channel.address); + + // The type won't change, but it might not be properly set yet + if (this.type !== packet.body.deviceType) { + this.settings.set_string('type', packet.body.deviceType); + this.notify('type'); + this.notify('icon-name'); + } + + // The name may change so we check and notify if so + if (this.name !== packet.body.deviceName) { + this.settings.set_string('name', packet.body.deviceName); + this.notify('name'); + } + + // Packets + const incoming = packet.body.incomingCapabilities.sort(); + const outgoing = packet.body.outgoingCapabilities.sort(); + const inc = this.settings.get_strv('incoming-capabilities'); + const out = this.settings.get_strv('outgoing-capabilities'); + + // Only write GSettings if something has changed + if (incoming.join('') !== inc.join('') || outgoing.join('') !== out.join('')) { + this.settings.set_strv('incoming-capabilities', incoming); + this.settings.set_strv('outgoing-capabilities', outgoing); + } + + // Determine supported plugins by matching incoming to outgoing types + const supported = []; + + for (const name in imports.service.plugins) { + // Exclude mousepad/presenter plugins in unsupported sessions + if (!HAVE_REMOTEINPUT && ['mousepad', 'presenter'].includes(name)) + continue; + + const meta = imports.service.plugins[name].Metadata; + + // If we can handle packets it sends or send packets it can handle + if (meta.incomingCapabilities.some(t => outgoing.includes(t)) || + meta.outgoingCapabilities.some(t => incoming.includes(t))) + supported.push(name); + } + + // Only write GSettings if something has changed + const currentSupported = this.settings.get_strv('supported-plugins'); + + if (currentSupported.join('') !== supported.sort().join('')) + this.settings.set_strv('supported-plugins', supported); + + this.thaw_notify(); + } + + /** + * Set the channel and start sending/receiving packets. If %null is passed + * the device becomes disconnected. + * + * @param {Core.Channel} [channel] - The new channel + */ + setChannel(channel = null) { + if (this.channel === channel) + return; + + if (this.channel !== null) + this.channel.close(); + + this._channel = channel; + + // If we've disconnected empty the queue, otherwise restart the read + // loop and update the device metadata + if (this.channel === null) { + this._outputQueue.length = 0; + } else { + this._handleIdentity(this.channel.identity); + this._readLoop(channel); + } + + // The connected state didn't change + if (this.connected === !!this.channel) + return; + + // Notify and trigger plugins + this._connected = !!this.channel; + this.notify('connected'); + this._triggerPlugins(); + } + + async _readLoop(channel) { + try { + let packet = null; + + while ((packet = await this.channel.readPacket())) { + debug(packet, this.name); + this.handlePacket(packet); + } + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + debug(e, this.name); + + if (this.channel === channel) + this.setChannel(null); + } + } + + _processExit(proc, result) { + try { + proc.wait_check_finish(result); + } catch (e) { + debug(e); + } + + this.delete(proc); + } + + /** + * Launch a subprocess for the device. If the device becomes unpaired, it is + * assumed the device is no longer trusted and all subprocesses will be + * killed. + * + * @param {string[]} args - process arguments + * @param {Gio.Cancellable} [cancellable] - optional cancellable + * @return {Gio.Subprocess} The subprocess + */ + launchProcess(args, cancellable = null) { + if (this._launcher === undefined) { + const application = GLib.build_filenamev([ + Config.PACKAGE_DATADIR, + 'service', + 'daemon.js', + ]); + + this._launcher = new Gio.SubprocessLauncher(); + this._launcher.setenv('GSCONNECT', application, false); + this._launcher.setenv('GSCONNECT_DEVICE_ID', this.id, false); + this._launcher.setenv('GSCONNECT_DEVICE_NAME', this.name, false); + this._launcher.setenv('GSCONNECT_DEVICE_ICON', this.icon_name, false); + this._launcher.setenv( + 'GSCONNECT_DEVICE_DBUS', + `${Config.APP_PATH}/Device/${this.id.replace(/\W+/g, '_')}`, + false + ); + } + + // Create and track the process + const proc = this._launcher.spawnv(args); + proc.wait_check_async(cancellable, this._processExit.bind(this._procs)); + this._procs.add(proc); + + return proc; + } + + /** + * Handle a packet and pass it to the appropriate plugin. + * + * @param {Core.Packet} packet - The incoming packet object + * @return {undefined} no return value + */ + handlePacket(packet) { + try { + if (packet.type === 'kdeconnect.pair') + return this._handlePair(packet); + + // The device must think we're paired; inform it we are not + if (!this.paired) + return this.unpair(); + + const handler = this._handlers.get(packet.type); + + if (handler !== undefined) + handler.handlePacket(packet); + else + debug(`Unsupported packet type (${packet.type})`, this.name); + } catch (e) { + debug(e, this.name); + } + } + + /** + * Send a packet to the device. + * + * @param {Object} packet - An object of packet data... + */ + async sendPacket(packet) { + try { + if (!this.connected) + return; + + if (!this.paired && packet.type !== 'kdeconnect.pair') + return; + + this._outputQueue.push(new Core.Packet(packet)); + + if (this._outputLock) + return; + + this._outputLock = true; + let next; + + while ((next = this._outputQueue.shift())) { + await this.channel.sendPacket(next); + debug(next, this.name); + } + + this._outputLock = false; + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + debug(e, this.name); + + this._outputLock = false; + } + } + + /** + * Actions + */ + _registerActions() { + // Pairing notification actions + const acceptPair = new Gio.SimpleAction({name: 'pair'}); + acceptPair.connect('activate', this.pair.bind(this)); + this.add_action(acceptPair); + + const rejectPair = new Gio.SimpleAction({name: 'unpair'}); + rejectPair.connect('activate', this.unpair.bind(this)); + this.add_action(rejectPair); + + // Transfer notification actions + const cancelTransfer = new Gio.SimpleAction({ + name: 'cancelTransfer', + parameter_type: new GLib.VariantType('s'), + }); + cancelTransfer.connect('activate', this.cancelTransfer.bind(this)); + this.add_action(cancelTransfer); + + const openPath = new Gio.SimpleAction({ + name: 'openPath', + parameter_type: new GLib.VariantType('s'), + }); + openPath.connect('activate', this.openPath); + this.add_action(openPath); + + // Preference helpers + const clearCache = new Gio.SimpleAction({ + name: 'clearCache', + parameter_type: null, + }); + clearCache.connect('activate', this._clearCache.bind(this)); + this.add_action(clearCache); + } + + /** + * Get the position of a GMenuItem with @actionName in the top level of the + * device menu. + * + * @param {string} actionName - An action name with scope (eg. device.foo) + * @return {number} An 0-based index or -1 if not found + */ + getMenuAction(actionName) { + for (let i = 0, len = this.menu.get_n_items(); i < len; i++) { + try { + const val = this.menu.get_item_attribute_value(i, 'action', null); + + if (val.unpack() === actionName) + return i; + } catch (e) { + continue; + } + } + + return -1; + } + + /** + * Add a GMenuItem to the top level of the device menu + * + * @param {Gio.MenuItem} menuItem - A GMenuItem + * @param {number} [index] - The position to place the item + * @return {number} The position the item was placed + */ + addMenuItem(menuItem, index = -1) { + try { + if (index > -1) { + this.menu.insert_item(index, menuItem); + return index; + } + + this.menu.append_item(menuItem); + return this.menu.get_n_items(); + } catch (e) { + debug(e, this.name); + return -1; + } + } + + /** + * Add a Device GAction to the top level of the device menu + * + * @param {Gio.Action} action - A GAction + * @param {number} [index] - The position to place the item + * @param {string} label - A label for the item + * @param {string} icon_name - A themed icon name for the item + * @return {number} The position the item was placed + */ + addMenuAction(action, index = -1, label, icon_name) { + try { + const item = new Gio.MenuItem(); + + if (label) + item.set_label(label); + + if (icon_name) + item.set_icon(new Gio.ThemedIcon({name: icon_name})); + + item.set_attribute_value( + 'hidden-when', + new GLib.Variant('s', 'action-disabled') + ); + + item.set_detailed_action(`device.${action.name}`); + + return this.addMenuItem(item, index); + } catch (e) { + debug(e, this.name); + return -1; + } + } + + /** + * Remove a GAction from the top level of the device menu by action name + * + * @param {string} actionName - A GAction name, including scope + * @return {number} The position the item was removed from or -1 + */ + removeMenuAction(actionName) { + try { + const index = this.getMenuAction(actionName); + + if (index > -1) + this.menu.remove(index); + + return index; + } catch (e) { + debug(e, this.name); + return -1; + } + } + + /** + * Withdraw a device notification. + * + * @param {string} id - Id for the notification to withdraw + */ + hideNotification(id) { + if (this.service === null) + return; + + this.service.withdraw_notification(`${this.id}|${id}`); + } + + /** + * Show a device notification. + * + * @param {Object} params - A dictionary of notification parameters + * @param {number} [params.id] - A UNIX epoch timestamp (ms) + * @param {string} [params.title] - A title + * @param {string} [params.body] - A body + * @param {Gio.Icon} [params.icon] - An icon + * @param {Gio.NotificationPriority} [params.priority] - The priority + * @param {Array} [params.actions] - A dictionary of action parameters + * @param {Array} [params.buttons] - An Array of buttons + */ + showNotification(params) { + if (this.service === null) + return; + + // KDE Connect on Android can sometimes give an undefined for params.body + Object.keys(params) + .forEach(key => params[key] === undefined && delete params[key]); + + params = Object.assign({ + id: Date.now(), + title: this.name, + body: '', + icon: new Gio.ThemedIcon({name: this.icon_name}), + priority: Gio.NotificationPriority.NORMAL, + action: null, + buttons: [], + }, params); + + const notif = new Gio.Notification(); + notif.set_title(params.title); + notif.set_body(params.body); + notif.set_icon(params.icon); + notif.set_priority(params.priority); + + // Default Action + if (params.action) { + const hasParameter = (params.action.parameter !== null); + + if (!hasParameter) + params.action.parameter = new GLib.Variant('s', ''); + + notif.set_default_action_and_target( + 'app.device', + new GLib.Variant('(ssbv)', [ + this.id, + params.action.name, + hasParameter, + params.action.parameter, + ]) + ); + } + + // Buttons + for (const button of params.buttons) { + const hasParameter = (button.parameter !== null); + + if (!hasParameter) + button.parameter = new GLib.Variant('s', ''); + + notif.add_button_with_target( + button.label, + 'app.device', + new GLib.Variant('(ssbv)', [ + this.id, + button.action, + hasParameter, + button.parameter, + ]) + ); + } + + this.service.send_notification(`${this.id}|${params.id}`, notif); + } + + /** + * Cancel an ongoing file transfer. + * + * @param {Gio.Action} action - The GAction + * @param {GLib.Variant} parameter - The activation parameter + */ + cancelTransfer(action, parameter) { + try { + const uuid = parameter.unpack(); + const transfer = this._transfers.get(uuid); + + if (transfer === undefined) + return; + + this._transfers.delete(uuid); + transfer.cancel(); + } catch (e) { + logError(e, this.name); + } + } + + /** + * Create a transfer object. + * + * @return {Core.Transfer} A new transfer + */ + createTransfer() { + const transfer = new Core.Transfer({device: this}); + + // Track the transfer + this._transfers.set(transfer.uuid, transfer); + + transfer.connect('notify::completed', (transfer) => { + this._transfers.delete(transfer.uuid); + }); + + return transfer; + } + + /** + * Reject the transfer payload described by @packet. + * + * @param {Core.Packet} packet - A packet + * @return {Promise} A promise for the operation + */ + rejectTransfer(packet) { + if (!packet || !packet.hasPayload()) + return; + + return this.channel.rejectTransfer(packet); + } + + openPath(action, parameter) { + const path = parameter.unpack(); + + // Normalize paths to URIs, assuming local file + const uri = path.includes('://') ? path : `file://${path}`; + Gio.AppInfo.launch_default_for_uri_async(uri, null, null, null); + } + + _clearCache(action, parameter) { + for (const plugin of this._plugins.values()) { + try { + plugin.clearCache(); + } catch (e) { + debug(e, this.name); + } + } + } + + /** + * Pair request handler + * + * @param {Core.Packet} packet - A complete kdeconnect.pair packet + */ + _handlePair(packet) { + // A pair has been requested/confirmed + if (packet.body.pair) { + // The device is accepting our request + if (this._outgoingPairRequest) { + this._setPaired(true); + this._loadPlugins(); + + // The device thinks we're unpaired + } else if (this.paired) { + this._setPaired(true); + this.sendPacket({ + type: 'kdeconnect.pair', + body: {pair: true}, + }); + this._triggerPlugins(); + + // The device is requesting pairing + } else { + this._notifyPairRequest(); + } + // Device is requesting unpairing/rejecting our request + } else { + this._setPaired(false); + this._unloadPlugins(); + } + } + + /** + * Notify the user of an incoming pair request and set a 30s timeout + */ + _notifyPairRequest() { + // Reset any active request + this._resetPairRequest(); + + this.showNotification({ + id: 'pair-request', + // TRANSLATORS: eg. Pair Request from Google Pixel + title: _('Pair Request from %s').format(this.name), + body: this.encryption_info, + icon: new Gio.ThemedIcon({name: 'channel-insecure-symbolic'}), + priority: Gio.NotificationPriority.URGENT, + buttons: [ + { + action: 'unpair', + label: _('Reject'), + parameter: null, + }, + { + action: 'pair', + label: _('Accept'), + parameter: null, + }, + ], + }); + + // Start a 30s countdown + this._incomingPairRequest = GLib.timeout_add_seconds( + GLib.PRIORITY_DEFAULT, + 30, + this._setPaired.bind(this, false) + ); + } + + /** + * Reset pair request timeouts and withdraw any notifications + */ + _resetPairRequest() { + this.hideNotification('pair-request'); + + if (this._incomingPairRequest) { + GLib.source_remove(this._incomingPairRequest); + this._incomingPairRequest = 0; + } + + if (this._outgoingPairRequest) { + GLib.source_remove(this._outgoingPairRequest); + this._outgoingPairRequest = 0; + } + } + + /** + * Set the internal paired state of the device and emit ::notify + * + * @param {boolean} paired - The paired state to set + */ + _setPaired(paired) { + this._resetPairRequest(); + + // For TCP connections we store or reset the TLS Certificate + if (this.connection_type === 'lan') { + if (paired) { + this.settings.set_string( + 'certificate-pem', + this.channel.peer_certificate.certificate_pem + ); + } else { + this.settings.reset('certificate-pem'); + } + } + + // If we've become unpaired, stop all subprocesses and transfers + if (!paired) { + for (const proc of this._procs) + proc.force_exit(); + + this._procs.clear(); + + for (const transfer of this._transfers.values()) + transfer.close(); + + this._transfers.clear(); + } + + this.settings.set_boolean('paired', paired); + this.notify('paired'); + } + + /** + * Send or accept an incoming pair request; also exported as a GAction + */ + pair() { + try { + // If we're accepting an incoming pair request, set the internal + // paired state and send the confirmation before loading plugins. + if (this._incomingPairRequest) { + this._setPaired(true); + + this.sendPacket({ + type: 'kdeconnect.pair', + body: {pair: true}, + }); + + this._loadPlugins(); + + // If we're initiating an outgoing pair request, be sure the timer + // is reset before sending the request and setting a 30s timeout. + } else if (!this.paired) { + this._resetPairRequest(); + + this.sendPacket({ + type: 'kdeconnect.pair', + body: {pair: true}, + }); + + this._outgoingPairRequest = GLib.timeout_add_seconds( + GLib.PRIORITY_DEFAULT, + 30, + this._setPaired.bind(this, false) + ); + } + } catch (e) { + logError(e, this.name); + } + } + + /** + * Unpair or reject an incoming pair request; also exported as a GAction + */ + unpair() { + try { + if (this.connected) { + this.sendPacket({ + type: 'kdeconnect.pair', + body: {pair: false}, + }); + } + + this._setPaired(false); + this._unloadPlugins(); + } catch (e) { + logError(e, this.name); + } + } + + /* + * Plugin Functions + */ + _onAllowedPluginsChanged(settings) { + const disabled = this.settings.get_strv('disabled-plugins'); + const supported = this.settings.get_strv('supported-plugins'); + const allowed = supported.filter(name => !disabled.includes(name)); + + // Unload any plugins that are disabled or unsupported + this._plugins.forEach(plugin => { + if (!allowed.includes(plugin.name)) + this._unloadPlugin(plugin.name); + }); + + // Make sure we change the contacts store if the plugin was disabled + if (!allowed.includes('contacts')) + this.notify('contacts'); + + // Load allowed plugins + for (const name of allowed) + this._loadPlugin(name); + } + + _loadPlugin(name) { + let handler, plugin; + + try { + if (this.paired && !this._plugins.has(name)) { + // Instantiate the handler + handler = imports.service.plugins[name]; + plugin = new handler.Plugin(this); + + // Register packet handlers + for (const packetType of handler.Metadata.incomingCapabilities) + this._handlers.set(packetType, plugin); + + // Register plugin + this._plugins.set(name, plugin); + + // Run the connected()/disconnected() handler + if (this.connected) + plugin.connected(); + else + plugin.disconnected(); + } + } catch (e) { + if (plugin !== undefined) + plugin.destroy(); + + if (this.service !== null) + this.service.notify_error(e); + else + logError(e, this.name); + } + } + + async _loadPlugins() { + const disabled = this.settings.get_strv('disabled-plugins'); + + for (const name of this.settings.get_strv('supported-plugins')) { + if (!disabled.includes(name)) + await this._loadPlugin(name); + } + } + + _unloadPlugin(name) { + let handler, plugin; + + try { + if (this._plugins.has(name)) { + // Unregister packet handlers + handler = imports.service.plugins[name]; + + for (const type of handler.Metadata.incomingCapabilities) + this._handlers.delete(type); + + // Unregister plugin + plugin = this._plugins.get(name); + this._plugins.delete(name); + plugin.destroy(); + } + } catch (e) { + logError(e, this.name); + } + } + + async _unloadPlugins() { + for (const name of this._plugins.keys()) + await this._unloadPlugin(name); + } + + _triggerPlugins() { + for (const plugin of this._plugins.values()) { + if (this.connected) + plugin.connected(); + else + plugin.disconnected(); + } + } + + destroy() { + // Drop the default contacts store if we were using it + if (this._contacts !== undefined) + this._contacts = Components.release('contacts'); + + // Close the channel if still connected + if (this.channel !== null) + this.channel.close(); + + // Synchronously destroy plugins + this._plugins.forEach(plugin => plugin.destroy()); + this._plugins.clear(); + + // Dispose GSettings + this.settings.disconnect(this._disabledPluginsChangedId); + this.settings.disconnect(this._supportedPluginsChangedId); + this.settings.run_dispose(); + + GObject.signal_handlers_destroy(this); + } +}); + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/manager.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/manager.js new file mode 100644 index 0000000..b254837 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/manager.js @@ -0,0 +1,500 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Config = imports.config; +const DBus = imports.service.utils.dbus; +const Device = imports.service.device; + +const DEVICE_NAME = 'org.gnome.Shell.Extensions.GSConnect.Device'; +const DEVICE_PATH = '/org/gnome/Shell/Extensions/GSConnect/Device'; +const DEVICE_IFACE = Config.DBUS.lookup_interface(DEVICE_NAME); + + +/** + * A manager for devices. + */ +var Manager = GObject.registerClass({ + GTypeName: 'GSConnectManager', + Properties: { + 'active': GObject.ParamSpec.boolean( + 'active', + 'Active', + 'Whether the manager is active', + GObject.ParamFlags.READABLE, + false + ), + 'discoverable': GObject.ParamSpec.boolean( + 'discoverable', + 'Discoverable', + 'Whether the service responds to discovery requests', + GObject.ParamFlags.READWRITE, + false + ), + 'id': GObject.ParamSpec.string( + 'id', + 'Id', + 'The hostname or other network unique id', + GObject.ParamFlags.READWRITE, + null + ), + 'name': GObject.ParamSpec.string( + 'name', + 'Name', + 'The name announced to the network', + GObject.ParamFlags.READWRITE, + 'GSConnect' + ), + }, +}, class Manager extends Gio.DBusObjectManagerServer { + + _init(params = {}) { + super._init(params); + + this._exported = new WeakMap(); + this._reconnectId = 0; + + this._settings = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup(Config.APP_ID, true), + }); + this._initSettings(); + } + + get active() { + if (this._active === undefined) + this._active = false; + + return this._active; + } + + get backends() { + if (this._backends === undefined) + this._backends = new Map(); + + return this._backends; + } + + get devices() { + if (this._devices === undefined) + this._devices = new Map(); + + return this._devices; + } + + get discoverable() { + if (this._discoverable === undefined) + this._discoverable = this.settings.get_boolean('discoverable'); + + return this._discoverable; + } + + set discoverable(value) { + if (this.discoverable === value) + return; + + this._discoverable = value; + this.notify('discoverable'); + + // FIXME: This whole thing just keeps getting uglier + const application = Gio.Application.get_default(); + + if (application === null) + return; + + if (this.discoverable) { + Gio.Application.prototype.withdraw_notification.call( + application, + 'discovery-warning' + ); + } else { + const notif = new Gio.Notification(); + notif.set_title(_('Discovery Disabled')); + notif.set_body(_('Discovery has been disabled due to the number of devices on this network.')); + notif.set_icon(new Gio.ThemedIcon({name: 'dialog-warning'})); + notif.set_priority(Gio.NotificationPriority.HIGH); + notif.set_default_action('app.preferences'); + + Gio.Application.prototype.withdraw_notification.call( + application, + 'discovery-warning', + notif + ); + } + } + + get id() { + if (this._id === undefined) + this._id = this.settings.get_string('id'); + + return this._id; + } + + set id(value) { + if (this.id === value) + return; + + this._id = value; + this.notify('id'); + } + + get name() { + if (this._name === undefined) + this._name = this.settings.get_string('name'); + + return this._name; + } + + set name(value) { + if (this.name === value) + return; + + this._name = value; + this.notify('name'); + + // Broadcast changes to the network + for (const backend of this.backends.values()) { + backend.name = this.name; + backend.buildIdentity(); + } + + this.identify(); + } + + get settings() { + if (this._settings === undefined) { + this._settings = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup(Config.APP_ID, true), + }); + } + + return this._settings; + } + + vfunc_notify(pspec) { + if (pspec.name !== 'connection') + return; + + if (this.connection !== null) + this._exportDevices(); + else + this._unexportDevices(); + } + + /* + * GSettings + */ + _initSettings() { + // Initialize the ID and name of the service + if (this.settings.get_string('id').length === 0) + this.settings.set_string('id', GLib.uuid_string_random()); + + if (this.settings.get_string('name').length === 0) + this.settings.set_string('name', GLib.get_host_name()); + + // Bound Properties + this.settings.bind('discoverable', this, 'discoverable', 0); + this.settings.bind('id', this, 'id', 0); + this.settings.bind('name', this, 'name', 0); + } + + /* + * Backends + */ + _onChannel(backend, channel) { + try { + let device = this.devices.get(channel.identity.body.deviceId); + + switch (true) { + // Proceed if this is an existing device... + case (device !== undefined): + break; + + // Or the connection is allowed... + case this.discoverable || channel.allowed: + device = this._ensureDevice(channel.identity); + break; + + // ...otherwise bail + default: + debug(`${channel.identity.body.deviceName}: not allowed`); + return false; + } + + device.setChannel(channel); + return true; + } catch (e) { + logError(e, backend.name); + return false; + } + } + + _loadBackends() { + for (const name in imports.service.backends) { + try { + // Try to create the backend and track it if successful + const module = imports.service.backends[name]; + const backend = new module.ChannelService({ + id: this.id, + name: this.name, + }); + this.backends.set(name, backend); + + // Connect to the backend + backend.__channelId = backend.connect( + 'channel', + this._onChannel.bind(this) + ); + + // Now try to start the backend, allowing us to retry if we fail + backend.start(); + } catch (e) { + if (Gio.Application.get_default()) + Gio.Application.get_default().notify_error(e); + } + } + } + + /* + * Devices + */ + _loadDevices() { + // Load cached devices + for (const id of this.settings.get_strv('devices')) { + const device = new Device.Device({body: {deviceId: id}}); + this._exportDevice(device); + this.devices.set(id, device); + } + } + + _exportDevice(device) { + if (this.connection === null) + return; + + const info = { + object: null, + interface: null, + actions: 0, + menu: 0, + }; + + const objectPath = `${DEVICE_PATH}/${device.id.replace(/\W+/g, '_')}`; + + // Export an object path for the device + info.object = new Gio.DBusObjectSkeleton({ + g_object_path: objectPath, + }); + this.export(info.object); + + // Export GActions & GMenu + info.actions = Gio.DBus.session.export_action_group(objectPath, device); + info.menu = Gio.DBus.session.export_menu_model(objectPath, device.menu); + + // Export the Device interface + info.interface = new DBus.Interface({ + g_instance: device, + g_interface_info: DEVICE_IFACE, + }); + info.object.add_interface(info.interface); + + this._exported.set(device, info); + } + + _exportDevices() { + if (this.connection === null) + return; + + for (const device of this.devices.values()) + this._exportDevice(device); + } + + _unexportDevice(device) { + const info = this._exported.get(device); + + if (info === undefined) + return; + + // Unexport GActions and GMenu + Gio.DBus.session.unexport_action_group(info.actions); + Gio.DBus.session.unexport_menu_model(info.menu); + + // Unexport the Device interface and object + info.interface.flush(); + info.object.remove_interface(info.interface); + info.object.flush(); + this.unexport(info.object.g_object_path); + + this._exported.delete(device); + } + + _unexportDevices() { + for (const device of this.devices.values()) + this._unexportDevice(device); + } + + /** + * Return a device for @packet, creating it and adding it to the list of + * of known devices if it doesn't exist. + * + * @param {Core.Packet} packet - An identity packet for the device + * @return {Device.Device} A device object + */ + _ensureDevice(packet) { + let device = this.devices.get(packet.body.deviceId); + + if (device === undefined) { + debug(`Adding ${packet.body.deviceName}`); + + // TODO: Remove when all clients support bluetooth-like discovery + // + // If this is the third unpaired device to connect, we disable + // discovery to avoid choking on networks with many devices + const unpaired = Array.from(this.devices.values()).filter(dev => { + return !dev.paired; + }); + + if (unpaired.length === 3) + this.discoverable = false; + + device = new Device.Device(packet); + this._exportDevice(device); + this.devices.set(device.id, device); + + // Notify + this.settings.set_strv('devices', Array.from(this.devices.keys())); + } + + return device; + } + + /** + * Permanently remove a device. + * + * Removes the device from the list of known devices, deletes all GSettings + * and files. + * + * @param {string} id - The id of the device to delete + */ + _removeDevice(id) { + // Delete all GSettings + const settings_path = `/org/gnome/shell/extensions/gsconnect/${id}/`; + GLib.spawn_command_line_async(`dconf reset -f ${settings_path}`); + + // Delete the cache + const cache = GLib.build_filenamev([Config.CACHEDIR, id]); + Gio.File.rm_rf(cache); + + // Forget the device + this.devices.delete(id); + this.settings.set_strv('devices', Array.from(this.devices.keys())); + } + + /** + * A GSourceFunc that tries to reconnect to each paired device, while + * pruning unpaired devices that have disconnected. + * + * @return {boolean} Always %true + */ + _reconnect() { + for (const [id, device] of this.devices) { + if (device.connected) + continue; + + if (device.paired) { + this.identify(device.settings.get_string('last-connection')); + continue; + } + + this._unexportDevice(device); + this._removeDevice(id); + device.destroy(); + } + + return GLib.SOURCE_CONTINUE; + } + + /** + * Identify to an address or broadcast to the network. + * + * @param {string} [uri] - An address URI or %null to broadcast + */ + identify(uri = null) { + try { + // If we're passed a parameter, try and find a backend for it + if (uri !== null) { + const [scheme, address] = uri.split('://'); + + const backend = this.backends.get(scheme); + + if (backend !== undefined) + backend.broadcast(address); + + // If we're not discoverable, only try to reconnect known devices + } else if (!this.discoverable) { + this._reconnect(); + + // Otherwise have each backend broadcast to it's network + } else { + this.backends.forEach(backend => backend.broadcast()); + } + } catch (e) { + logError(e); + } + } + + /** + * Start managing devices. + */ + start() { + if (this.active) + return; + + this._loadDevices(); + this._loadBackends(); + + if (this._reconnectId === 0) { + this._reconnectId = GLib.timeout_add_seconds( + GLib.PRIORITY_LOW, + 5, + this._reconnect.bind(this) + ); + } + + this._active = true; + this.notify('active'); + } + + /** + * Stop managing devices. + */ + stop() { + if (!this.active) + return; + + if (this._reconnectId > 0) { + GLib.Source.remove(this._reconnectId); + this._reconnectId = 0; + } + + this._unexportDevices(); + + this.backends.forEach(backend => backend.destroy()); + this.backends.clear(); + + this.devices.forEach(device => device.destroy()); + this.devices.clear(); + + this._active = false; + this.notify('active'); + } + + /** + * Stop managing devices and free any resources. + */ + destroy() { + this.stop(); + this.set_connection(null); + } +}); + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/nativeMessagingHost.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/nativeMessagingHost.js new file mode 100755 index 0000000..5635f33 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/nativeMessagingHost.js @@ -0,0 +1,215 @@ +#!/usr/bin/env gjs + +'use strict'; + +imports.gi.versions.Gio = '2.0'; +imports.gi.versions.GLib = '2.0'; +imports.gi.versions.GObject = '2.0'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const System = imports.system; + + +const NativeMessagingHost = GObject.registerClass({ + GTypeName: 'GSConnectNativeMessagingHost', +}, class NativeMessagingHost extends Gio.Application { + + _init() { + super._init({ + application_id: 'org.gnome.Shell.Extensions.GSConnect.NativeMessagingHost', + flags: Gio.ApplicationFlags.NON_UNIQUE, + }); + } + + get devices() { + if (this._devices === undefined) + this._devices = {}; + + return this._devices; + } + + vfunc_activate() { + super.vfunc_activate(); + } + + vfunc_startup() { + super.vfunc_startup(); + this.hold(); + + // IO Channels + this._stdin = new Gio.DataInputStream({ + base_stream: new Gio.UnixInputStream({fd: 0}), + byte_order: Gio.DataStreamByteOrder.HOST_ENDIAN, + }); + + this._stdout = new Gio.DataOutputStream({ + base_stream: new Gio.UnixOutputStream({fd: 1}), + byte_order: Gio.DataStreamByteOrder.HOST_ENDIAN, + }); + + const source = this._stdin.base_stream.create_source(null); + source.set_callback(this.receive.bind(this)); + source.attach(null); + + // Device Manager + try { + this._manager = Gio.DBusObjectManagerClient.new_for_bus_sync( + Gio.BusType.SESSION, + Gio.DBusObjectManagerClientFlags.DO_NOT_AUTO_START, + 'org.gnome.Shell.Extensions.GSConnect', + '/org/gnome/Shell/Extensions/GSConnect', + null, + null + ); + } catch (e) { + logError(e); + this.quit(); + } + + // Add currently managed devices + for (const object of this._manager.get_objects()) { + for (const iface of object.get_interfaces()) + this._onInterfaceAdded(this._manager, object, iface); + } + + // Watch for new and removed devices + this._manager.connect( + 'interface-added', + this._onInterfaceAdded.bind(this) + ); + this._manager.connect( + 'object-removed', + this._onObjectRemoved.bind(this) + ); + + // Watch for device property changes + this._manager.connect( + 'interface-proxy-properties-changed', + this.sendDeviceList.bind(this) + ); + + // Watch for service restarts + this._manager.connect( + 'notify::name-owner', + this.sendDeviceList.bind(this) + ); + + this.send({ + type: 'connected', + data: (this._manager.name_owner !== null), + }); + } + + receive() { + try { + // Read the message + const length = this._stdin.read_int32(null); + const bytes = this._stdin.read_bytes(length, null).toArray(); + const message = JSON.parse(imports.byteArray.toString(bytes)); + + // A request for a list of devices + if (message.type === 'devices') { + this.sendDeviceList(); + + // A request to invoke an action + } else if (message.type === 'share') { + let actionName; + const device = this.devices[message.data.device]; + + if (device) { + if (message.data.action === 'share') + actionName = 'shareUri'; + else if (message.data.action === 'telephony') + actionName = 'shareSms'; + + device.actions.activate_action( + actionName, + new GLib.Variant('s', message.data.url) + ); + } + } + + return GLib.SOURCE_CONTINUE; + } catch (e) { + this.quit(); + } + } + + send(message) { + try { + const data = JSON.stringify(message); + this._stdout.put_int32(data.length, null); + this._stdout.put_string(data, null); + } catch (e) { + this.quit(); + } + } + + sendDeviceList() { + // Inform the WebExtension we're disconnected from the service + if (this._manager && this._manager.name_owner === null) + return this.send({type: 'connected', data: false}); + + // Collect all the devices with supported actions + const available = []; + + for (const device of Object.values(this.devices)) { + const share = device.actions.get_action_enabled('shareUri'); + const telephony = device.actions.get_action_enabled('shareSms'); + + if (share || telephony) { + available.push({ + id: device.g_object_path, + name: device.name, + type: device.type, + share: share, + telephony: telephony, + }); + } + } + + this.send({type: 'devices', data: available}); + } + + _proxyGetter(name) { + try { + return this.get_cached_property(name).unpack(); + } catch (e) { + return null; + } + } + + _onInterfaceAdded(manager, object, iface) { + Object.defineProperties(iface, { + 'name': { + get: this._proxyGetter.bind(iface, 'Name'), + enumerable: true, + }, + // TODO: phase this out for icon-name + 'type': { + get: this._proxyGetter.bind(iface, 'Type'), + enumerable: true, + }, + }); + + iface.actions = Gio.DBusActionGroup.get( + iface.g_connection, + iface.g_name, + iface.g_object_path + ); + + this.devices[iface.g_object_path] = iface; + this.sendDeviceList(); + } + + _onObjectRemoved(manager, object) { + delete this.devices[object.g_object_path]; + this.sendDeviceList(); + } +}); + +// NOTE: must not pass ARGV +(new NativeMessagingHost()).run([System.programInvocationName]); + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugin.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugin.js new file mode 100644 index 0000000..164abfa --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugin.js @@ -0,0 +1,258 @@ +'use strict'; + +const ByteArray = imports.byteArray; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Config = imports.config; + + +/** + * Base class for device plugins. + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectPlugin', + Properties: { + 'device': GObject.ParamSpec.object( + 'device', + 'Device', + 'The device that owns this plugin', + GObject.ParamFlags.READABLE, + GObject.Object + ), + 'name': GObject.ParamSpec.string( + 'name', + 'Name', + 'The device name', + GObject.ParamFlags.READABLE, + null + ), + }, +}, class Plugin extends GObject.Object { + + _init(device, name, meta = null) { + super._init(); + + this._device = device; + this._name = name; + this._meta = meta; + + if (this._meta === null) + this._meta = imports.service.plugins[name].Metadata; + + // GSettings + const schema = Config.GSCHEMA.lookup(this._meta.id, false); + + if (schema !== null) { + this.settings = new Gio.Settings({ + settings_schema: schema, + path: `${device.settings.path}plugin/${name}/`, + }); + } + + // GActions + this._gactions = []; + + if (this._meta.actions) { + const menu = this.device.settings.get_strv('menu-actions'); + + for (const name in this._meta.actions) { + const info = this._meta.actions[name]; + this._registerAction(name, menu.indexOf(name), info); + } + } + } + + get cancellable() { + if (this._cancellable === undefined) + this._cancellable = new Gio.Cancellable(); + + return this._cancellable; + } + + get device() { + return this._device; + } + + get name() { + return this._name; + } + + _activateAction(action, parameter) { + try { + let args = null; + + if (parameter instanceof GLib.Variant) + args = parameter.full_unpack(); + + if (Array.isArray(args)) + this[action.name](...args); + else + this[action.name](args); + } catch (e) { + logError(e, action.name); + } + } + + _registerAction(name, menuIndex, info) { + try { + // Device Action + const action = new Gio.SimpleAction({ + name: name, + parameter_type: info.parameter_type, + enabled: false, + }); + action.connect('activate', this._activateAction.bind(this)); + + this.device.add_action(action); + + // Menu + if (menuIndex > -1) { + this.device.addMenuAction( + action, + menuIndex, + info.label, + info.icon_name + ); + } + + this._gactions.push(action); + } catch (e) { + logError(e, `${this.device.name}: ${this.name}`); + } + } + + /** + * Called when the device connects. + */ + connected() { + // Enabled based on device capabilities, which might change + const incoming = this.device.settings.get_strv('incoming-capabilities'); + const outgoing = this.device.settings.get_strv('outgoing-capabilities'); + + for (const action of this._gactions) { + const info = this._meta.actions[action.name]; + + if (info.incoming.every(type => outgoing.includes(type)) && + info.outgoing.every(type => incoming.includes(type))) + action.set_enabled(true); + } + } + + /** + * Called when the device disconnects. + */ + disconnected() { + for (const action of this._gactions) + action.set_enabled(false); + } + + /** + * Called when a packet is received that the plugin is a handler for. + * + * @param {Core.Packet} packet - A KDE Connect packet + */ + handlePacket(packet) { + throw new GObject.NotImplementedError(); + } + + /** + * Cache JSON parseable properties on this object for persistence. The + * filename ~/.cache/gsconnect/<device-id>/<plugin-name>.json will be used + * to store the properties and values. + * + * Calling cacheProperties() opens a JSON cache file and reads any stored + * properties and values onto the current instance. When destroy() + * is called the properties are automatically stored in the same file. + * + * @param {Array} names - A list of this object's property names to cache + */ + async cacheProperties(names) { + try { + this._cacheProperties = names; + + // Ensure the device's cache directory exists + const cachedir = GLib.build_filenamev([ + Config.CACHEDIR, + this.device.id, + ]); + GLib.mkdir_with_parents(cachedir, 448); + + this._cacheFile = Gio.File.new_for_path( + GLib.build_filenamev([cachedir, `${this.name}.json`]) + ); + + // Read the cache from disk + await new Promise((resolve, reject) => { + this._cacheFile.load_contents_async(null, (file, res) => { + try { + const contents = file.load_contents_finish(res)[1]; + const cache = JSON.parse(ByteArray.toString(contents)); + Object.assign(this, cache); + + resolve(); + } catch (e) { + reject(e); + } + }); + }); + } catch (e) { + debug(e.message, `${this.device.name}: ${this.name}`); + } finally { + this.cacheLoaded(); + } + } + + /** + * An overridable function that is invoked when the on-disk cache is being + * cleared. Implementations should use this function to clear any in-memory + * cache data. + */ + clearCache() {} + + /** + * An overridable function that is invoked when the cache is done loading + */ + cacheLoaded() {} + + /** + * Unregister plugin actions, write the cache (if applicable) and destroy + * any dangling signal handlers. + */ + destroy() { + // Cancel any pending plugin operations + if (this._cancellable !== undefined) + this._cancellable.cancel(); + + for (const action of this._gactions) { + this.device.removeMenuAction(`device.${action.name}`); + this.device.remove_action(action.name); + } + + // Write the cache to disk synchronously + if (this._cacheFile !== undefined) { + try { + // Build the cache + const cache = {}; + + for (const name of this._cacheProperties) + cache[name] = this[name]; + + this._cacheFile.replace_contents( + JSON.stringify(cache, null, 2), + null, + false, + Gio.FileCreateFlags.REPLACE_DESTINATION, + null + ); + } catch (e) { + debug(e.message, `${this.device.name}: ${this.name}`); + } + } + + GObject.signal_handlers_destroy(this); + } +}); + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/battery.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/battery.js new file mode 100644 index 0000000..6035220 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/battery.js @@ -0,0 +1,429 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Components = imports.service.components; +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('Battery'), + description: _('Exchange battery information'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Battery', + incomingCapabilities: [ + 'kdeconnect.battery', + 'kdeconnect.battery.request', + ], + outgoingCapabilities: [ + 'kdeconnect.battery', + 'kdeconnect.battery.request', + ], + actions: {}, +}; + + +/** + * Battery Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/battery + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectBatteryPlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'battery'); + + // Setup Cache; defaults are 90 minute charge, 1 day discharge + this._chargeState = [54, 0, -1]; + this._dischargeState = [864, 0, -1]; + this._thresholdLevel = 25; + + this.cacheProperties([ + '_chargeState', + '_dischargeState', + '_thresholdLevel', + ]); + + // Export battery state as GAction + this.__state = new Gio.SimpleAction({ + name: 'battery', + parameter_type: new GLib.VariantType('(bsii)'), + state: this.state, + }); + this.device.add_action(this.__state); + + // Local Battery (UPower) + this._upower = null; + this._sendStatisticsId = this.settings.connect( + 'changed::send-statistics', + this._onSendStatisticsChanged.bind(this) + ); + this._onSendStatisticsChanged(this.settings); + } + + get charging() { + if (this._charging === undefined) + this._charging = false; + + return this._charging; + } + + get icon_name() { + let icon; + + if (this.level === -1) + return 'battery-missing-symbolic'; + else if (this.level === 100) + return 'battery-full-charged-symbolic'; + else if (this.level < 3) + icon = 'battery-empty'; + else if (this.level < 10) + icon = 'battery-caution'; + else if (this.level < 30) + icon = 'battery-low'; + else if (this.level < 60) + icon = 'battery-good'; + else if (this.level >= 60) + icon = 'battery-full'; + + if (this.charging) + return `${icon}-charging-symbolic`; + + return `${icon}-symbolic`; + } + + get level() { + // This is what KDE Connect returns if the remote battery plugin is + // disabled or still being loaded + if (this._level === undefined) + this._level = -1; + + return this._level; + } + + get time() { + if (this._time === undefined) + this._time = 0; + + return this._time; + } + + get state() { + return new GLib.Variant( + '(bsii)', + [this.charging, this.icon_name, this.level, this.time] + ); + } + + cacheLoaded() { + this._initEstimate(); + this._sendState(); + } + + clearCache() { + this._chargeState = [54, 0, -1]; + this._dischargeState = [864, 0, -1]; + this._thresholdLevel = 25; + this._initEstimate(); + } + + connected() { + super.connected(); + + this._requestState(); + this._sendState(); + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.battery': + this._receiveState(packet); + break; + + case 'kdeconnect.battery.request': + this._sendState(); + break; + } + } + + _onSendStatisticsChanged() { + if (this.settings.get_boolean('send-statistics')) + this._monitorState(); + else + this._unmonitorState(); + } + + /** + * Recalculate and update the estimated time remaining, but not the rate. + */ + _initEstimate() { + let rate, level; + + // elision of [rate, time, level] + if (this.charging) + [rate,, level] = this._chargeState; + else + [rate,, level] = this._dischargeState; + + if (!Number.isFinite(rate) || rate < 1) + rate = this.charging ? 864 : 90; + + if (!Number.isFinite(level) || level < 0) + level = this.level; + + // Update the time remaining + if (rate && this.charging) + this._time = Math.floor(rate * (100 - level)); + else if (rate && !this.charging) + this._time = Math.floor(rate * level); + + this.__state.state = this.state; + } + + /** + * Recalculate the (dis)charge rate and update the estimated time remaining. + */ + _updateEstimate() { + let rate, time, level; + const newTime = Math.floor(Date.now() / 1000); + const newLevel = this.level; + + // Load the state; ensure we have sane values for calculation + if (this.charging) + [rate, time, level] = this._chargeState; + else + [rate, time, level] = this._dischargeState; + + if (!Number.isFinite(rate) || rate < 1) + rate = this.charging ? 54 : 864; + + if (!Number.isFinite(time) || time <= 0) + time = newTime; + + if (!Number.isFinite(level) || level < 0) + level = newLevel; + + // Update the rate; use a weighted average to account for missed changes + // NOTE: (rate = seconds/percent) + const ldiff = this.charging ? newLevel - level : level - newLevel; + const tdiff = newTime - time; + const newRate = tdiff / ldiff; + + if (newRate && Number.isFinite(newRate)) + rate = Math.floor((rate * 0.4) + (newRate * 0.6)); + + // Store the state for the next recalculation + if (this.charging) + this._chargeState = [rate, newTime, newLevel]; + else + this._dischargeState = [rate, newTime, newLevel]; + + // Update the time remaining + if (rate && this.charging) + this._time = Math.floor(rate * (100 - newLevel)); + else if (rate && !this.charging) + this._time = Math.floor(rate * newLevel); + + this.__state.state = this.state; + } + + /** + * Notify the user the remote battery is full. + */ + _fullBatteryNotification() { + if (!this.settings.get_boolean('full-battery-notification')) + return; + + // Offer the option to ring the device, if available + let buttons = []; + + if (this.device.get_action_enabled('ring')) { + buttons = [{ + label: _('Ring'), + action: 'ring', + parameter: null, + }]; + } + + this.device.showNotification({ + id: 'battery|full', + // TRANSLATORS: eg. Google Pixel: Battery is full + title: _('%s: Battery is full').format(this.device.name), + // TRANSLATORS: when the battery is fully charged + body: _('Fully Charged'), + icon: Gio.ThemedIcon.new('battery-full-charged-symbolic'), + buttons: buttons, + }); + } + + /** + * Notify the user the remote battery is at custom charge level. + */ + _customBatteryNotification() { + if (!this.settings.get_boolean('custom-battery-notification')) + return; + + // Offer the option to ring the device, if available + let buttons = []; + + if (this.device.get_action_enabled('ring')) { + buttons = [{ + label: _('Ring'), + action: 'ring', + parameter: null, + }]; + } + + this.device.showNotification({ + id: 'battery|custom', + // TRANSLATORS: eg. Google Pixel: Battery has reached custom charge level + title: _('%s: Battery has reached custom charge level').format(this.device.name), + // TRANSLATORS: when the battery has reached custom charge level + body: _('%d%% Charged').format(this.level), + icon: Gio.ThemedIcon.new('battery-full-charged-symbolic'), + buttons: buttons, + }); + } + + /** + * Notify the user the remote battery is low. + */ + _lowBatteryNotification() { + if (!this.settings.get_boolean('low-battery-notification')) + return; + + // Offer the option to ring the device, if available + let buttons = []; + + if (this.device.get_action_enabled('ring')) { + buttons = [{ + label: _('Ring'), + action: 'ring', + parameter: null, + }]; + } + + this.device.showNotification({ + id: 'battery|low', + // TRANSLATORS: eg. Google Pixel: Battery is low + title: _('%s: Battery is low').format(this.device.name), + // TRANSLATORS: eg. 15% remaining + body: _('%d%% remaining').format(this.level), + icon: Gio.ThemedIcon.new('battery-caution-symbolic'), + buttons: buttons, + }); + } + + /** + * Handle a remote battery update. + * + * @param {Core.Packet} packet - A kdeconnect.battery packet + */ + _receiveState(packet) { + // Charging state changed + this._charging = packet.body.isCharging; + + // Level changed + if (this._level !== packet.body.currentCharge) { + this._level = packet.body.currentCharge; + + // If the level is above the threshold hide the notification + if (this._level > this._thresholdLevel) + this.device.hideNotification('battery|low'); + + // The level just changed to/from custom level while charging + if ((this._level === this.settings.get_uint('custom-battery-notification-value')) && this._charging) + this._customBatteryNotification(); + else + this.device.hideNotification('battery|custom'); + + // The level just changed to/from full + if (this._level === 100) + this._fullBatteryNotification(); + else + this.device.hideNotification('battery|full'); + } + + // Device considers the level low + if (packet.body.thresholdEvent > 0) { + this._lowBatteryNotification(); + this._thresholdLevel = this.level; + } + + this._updateEstimate(); + } + + /** + * Request the remote battery's current state + */ + _requestState() { + this.device.sendPacket({ + type: 'kdeconnect.battery.request', + body: {request: true}, + }); + } + + /** + * Report the local battery's current state + */ + _sendState() { + if (this._upower === null || !this._upower.is_present) + return; + + this.device.sendPacket({ + type: 'kdeconnect.battery', + body: { + currentCharge: this._upower.level, + isCharging: this._upower.charging, + thresholdEvent: this._upower.threshold, + }, + }); + } + + /* + * UPower monitoring methods + */ + _monitorState() { + try { + // Currently only true if the remote device is a desktop (rare) + const incoming = this.device.settings.get_strv('incoming-capabilities'); + + if (!incoming.includes('kdeconnect.battery')) + return; + + this._upower = Components.acquire('upower'); + + this._upowerId = this._upower.connect( + 'changed', + this._sendState.bind(this) + ); + + this._sendState(); + } catch (e) { + logError(e, this.device.name); + this._unmonitorState(); + } + } + + _unmonitorState() { + try { + if (this._upower === null) + return; + + this._upower.disconnect(this._upowerId); + this._upower = Components.release('upower'); + } catch (e) { + logError(e, this.device.name); + } + } + + destroy() { + this.device.remove_action('battery'); + this.settings.disconnect(this._sendStatisticsId); + this._unmonitorState(); + + super.destroy(); + } +}); diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/clipboard.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/clipboard.js new file mode 100644 index 0000000..49fa725 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/clipboard.js @@ -0,0 +1,178 @@ +'use strict'; + +const GObject = imports.gi.GObject; + +const Components = imports.service.components; +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('Clipboard'), + description: _('Share the clipboard content'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Clipboard', + incomingCapabilities: [ + 'kdeconnect.clipboard', + 'kdeconnect.clipboard.connect', + ], + outgoingCapabilities: [ + 'kdeconnect.clipboard', + 'kdeconnect.clipboard.connect', + ], + actions: { + clipboardPush: { + label: _('Clipboard Push'), + icon_name: 'edit-paste-symbolic', + + parameter_type: null, + incoming: [], + outgoing: ['kdeconnect.clipboard'], + }, + clipboardPull: { + label: _('Clipboard Pull'), + icon_name: 'edit-copy-symbolic', + + parameter_type: null, + incoming: ['kdeconnect.clipboard'], + outgoing: [], + }, + }, +}; + + +/** + * Clipboard Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/clipboard + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectClipboardPlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'clipboard'); + + this._clipboard = Components.acquire('clipboard'); + + // Watch local clipboard for changes + this._textChangedId = this._clipboard.connect( + 'notify::text', + this._onLocalClipboardChanged.bind(this) + ); + + // Buffer content to allow selective sync + this._localBuffer = this._clipboard.text; + this._localTimestamp = 0; + this._remoteBuffer = null; + } + + connected() { + super.connected(); + + // TODO: if we're not auto-syncing local->remote, but we are doing the + // reverse, it's possible older remote content will end up + // overwriting newer local content. + if (!this.settings.get_boolean('send-content')) + return; + + if (this._localBuffer === null && this._localTimestamp === 0) + return; + + this.device.sendPacket({ + type: 'kdeconnect.clipboard.connect', + body: { + content: this._localBuffer, + timestamp: this._localTimestamp, + }, + }); + } + + handlePacket(packet) { + if (!packet.body.hasOwnProperty('content')) + return; + + switch (packet.type) { + case 'kdeconnect.clipboard': + this._handleContent(packet); + break; + + case 'kdeconnect.clipboard.connect': + this._handleConnectContent(packet); + break; + } + } + + _handleContent(packet) { + this._onRemoteClipboardChanged(packet.body.content); + } + + _handleConnectContent(packet) { + if (packet.body.hasOwnProperty('timestamp') && + packet.body.timestamp > this._localTimestamp) + this._onRemoteClipboardChanged(packet.body.content); + } + + /* + * Store the local clipboard content and forward it if enabled + */ + _onLocalClipboardChanged(clipboard, pspec) { + this._localBuffer = clipboard.text; + this._localTimestamp = Date.now(); + + if (this.settings.get_boolean('send-content')) + this.clipboardPush(); + } + + /* + * Store the remote clipboard content and apply it if enabled + */ + _onRemoteClipboardChanged(text) { + this._remoteBuffer = text; + + if (this.settings.get_boolean('receive-content')) + this.clipboardPull(); + } + + /** + * Copy to the remote clipboard; called by _onLocalClipboardChanged() + */ + clipboardPush() { + // Don't sync if the clipboard is empty or not text + if (this._localTimestamp === 0) + return; + + if (this._remoteBuffer !== this._localBuffer) { + this._remoteBuffer = this._localBuffer; + + // If the buffer is %null, the clipboard contains non-text content, + // so we neither clear the remote clipboard nor pass the content + if (this._localBuffer !== null) { + this.device.sendPacket({ + type: 'kdeconnect.clipboard', + body: { + content: this._localBuffer, + }, + }); + } + } + } + + /** + * Copy from the remote clipboard; called by _onRemoteClipboardChanged() + */ + clipboardPull() { + if (this._localBuffer !== this._remoteBuffer) { + this._localBuffer = this._remoteBuffer; + this._localTimestamp = Date.now(); + + this._clipboard.text = this._remoteBuffer; + } + } + + destroy() { + if (this._clipboard && this._textChangedId) { + this._clipboard.disconnect(this._textChangedId); + this._clipboard = Components.release('clipboard'); + } + + super.destroy(); + } +}); diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/connectivity_report.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/connectivity_report.js new file mode 100644 index 0000000..d4b8eff --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/connectivity_report.js @@ -0,0 +1,162 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Components = imports.service.components; +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('Connectivity Report'), + description: _('Display connectivity status'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.ConnectivityReport', + incomingCapabilities: [ + 'kdeconnect.connectivity_report', + ], + outgoingCapabilities: [ + 'kdeconnect.connectivity_report.request', + ], + actions: {}, +}; + + +/** + * Connectivity Report Plugin + * https://invent.kde.org/network/kdeconnect-kde/-/tree/master/plugins/connectivity_report + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectConnectivityReportPlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'connectivity_report'); + + // Export connectivity state as GAction + this.__state = new Gio.SimpleAction({ + name: 'connectivityReport', + // ( + // cellular_network_type, + // cellular_network_type_icon, + // cellular_network_strength(0..4), + // cellular_network_strength_icon, + // ) + parameter_type: new GLib.VariantType('(ssis)'), + state: this.state, + }); + this.device.add_action(this.__state); + } + + get signal_strength() { + if (this._signalStrength === undefined) + this._signalStrength = -1; + + return this._signalStrength; + } + + get network_type() { + if (this._networkType === undefined) + this._networkType = ''; + + return this._networkType; + } + + get signal_strength_icon_name() { + if (this.signal_strength === 0) + return 'network-cellular-signal-none-symbolic'; // SIGNAL_STRENGTH_NONE_OR_UNKNOWN + else if (this.signal_strength === 1) + return 'network-cellular-signal-weak-symbolic'; // SIGNAL_STRENGTH_POOR + else if (this.signal_strength === 2) + return 'network-cellular-signal-ok-symbolic'; // SIGNAL_STRENGTH_MODERATE + else if (this.signal_strength === 3) + return 'network-cellular-signal-good-symbolic'; // SIGNAL_STRENGTH_GOOD + else if (this.signal_strength >= 4) + return 'network-cellular-signal-excellent-symbolic'; // SIGNAL_STRENGTH_GREAT + + return 'network-cellular-offline-symbolic'; // OFF (signal_strength == -1) + } + + get network_type_icon_name() { + if (this.network_type === 'GSM' || this.network_type === 'CDMA' || this.network_type === 'iDEN') + return 'network-cellular-2g-symbolic'; + else if (this.network_type === 'UMTS' || this.network_type === 'CDMA2000') + return 'network-cellular-3g-symbolic'; + else if (this.network_type === 'LTE') + return 'network-cellular-4g-symbolic'; + else if (this.network_type === 'EDGE') + return 'network-cellular-edge-symbolic'; + else if (this.network_type === 'GPRS') + return 'network-cellular-gprs-symbolic'; + else if (this.network_type === 'HSPA') + return 'network-cellular-hspa-symbolic'; + // FIXME: No icon for this! + // https://gitlab.gnome.org/GNOME/adwaita-icon-theme/-/issues/114 + else if (this.network_type === '5G') + return 'network-cellular-symbolic'; + + return 'network-cellular-symbolic'; + } + + get state() { + return new GLib.Variant( + '(ssis)', + [ + this.network_type, + this.network_type_icon_name, + this.signal_strength, + this.signal_strength_icon_name, + ] + ); + } + + connected() { + super.connected(); + + this._requestState(); + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.connectivity_report': + this._receiveState(packet); + break; + } + } + + /** + * Handle a remote state update. + * + * @param {Core.Packet} packet - A kdeconnect.connectivity_report packet + */ + _receiveState(packet) { + if (packet.body.signalStrengths) { + // TODO: Only first SIM (subscriptionID) is supported at the moment + const subs = Object.keys(packet.body.signalStrengths); + const firstSub = Math.min.apply(null, subs); + const data = packet.body.signalStrengths[firstSub]; + + this._networkType = data.networkType; + this._signalStrength = data.signalStrength; + } + + // Update DBus state + this.__state.state = this.state; + } + + /** + * Request the remote device's connectivity state + */ + _requestState() { + this.device.sendPacket({ + type: 'kdeconnect.connectivity_report.request', + body: {}, + }); + } + + destroy() { + this.device.remove_action('connectivity_report'); + + super.destroy(); + } +}); diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/contacts.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/contacts.js new file mode 100644 index 0000000..8559272 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/contacts.js @@ -0,0 +1,456 @@ +'use strict'; + +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const PluginBase = imports.service.plugin; +const Contacts = imports.service.components.contacts; + +/* + * We prefer libebook's vCard parser if it's available + */ +var EBookContacts; + +try { + EBookContacts = imports.gi.EBookContacts; +} catch (e) { + EBookContacts = null; +} + + +var Metadata = { + label: _('Contacts'), + description: _('Access contacts of the paired device'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Contacts', + incomingCapabilities: [ + 'kdeconnect.contacts.response_uids_timestamps', + 'kdeconnect.contacts.response_vcards', + ], + outgoingCapabilities: [ + 'kdeconnect.contacts.request_all_uids_timestamps', + 'kdeconnect.contacts.request_vcards_by_uid', + ], + actions: {}, +}; + + +/* + * vCard 2.1 Patterns + */ +const VCARD_FOLDING = /\r\n |\r |\n |=\n/g; +const VCARD_SUPPORTED = /^fn|tel|photo|x-kdeconnect/i; +const VCARD_BASIC = /^([^:;]+):(.+)$/; +const VCARD_TYPED = /^([^:;]+);([^:]+):(.+)$/; +const VCARD_TYPED_KEY = /item\d{1,2}\./; +const VCARD_TYPED_META = /([a-z]+)=(.*)/i; + + +/** + * Contacts Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/contacts + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectContactsPlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'contacts'); + + this._store = new Contacts.Store(device.id); + this._store.fetch = this._requestUids.bind(this); + + // Notify when the store is ready + this._contactsStoreReadyId = this._store.connect( + 'notify::context', + () => this.device.notify('contacts') + ); + + // Notify if the contacts source changes + this._contactsSourceChangedId = this.settings.connect( + 'changed::contacts-source', + () => this.device.notify('contacts') + ); + + // Load the cache + this._store.load(); + } + + clearCache() { + this._store.clear(); + } + + connected() { + super.connected(); + this._requestUids(); + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.contacts.response_uids_timestamps': + this._handleUids(packet); + break; + + case 'kdeconnect.contacts.response_vcards': + this._handleVCards(packet); + break; + } + } + + _handleUids(packet) { + try { + const contacts = this._store.contacts; + const remote_uids = packet.body.uids; + let removed = false; + delete packet.body.uids; + + // Usually a failed request, so avoid wiping the cache + if (remote_uids.length === 0) + return; + + // Delete any contacts that were removed on the device + for (let i = 0, len = contacts.length; i < len; i++) { + const contact = contacts[i]; + + if (!remote_uids.includes(contact.id)) { + this._store.remove(contact.id, false); + removed = true; + } + } + + // Build a list of new or updated contacts + const uids = []; + + for (const [uid, timestamp] of Object.entries(packet.body)) { + const contact = this._store.get_contact(uid); + + if (!contact || contact.timestamp !== timestamp) + uids.push(uid); + } + + // Send a request for any new or updated contacts + if (uids.length) + this._requestVCards(uids); + + // If we removed any contacts, save the cache + if (removed) + this._store.save(); + } catch (e) { + logError(e); + } + } + + /** + * Decode a string encoded as "QUOTED-PRINTABLE" and return a regular string + * + * See: https://github.com/mathiasbynens/quoted-printable/blob/master/src/quoted-printable.js + * + * @param {string} input - The QUOTED-PRINTABLE string + * @return {string} The decoded string + */ + _decodeQuotedPrintable(input) { + return input + // https://tools.ietf.org/html/rfc2045#section-6.7, rule 3 + .replace(/[\t\x20]$/gm, '') + // Remove hard line breaks preceded by `=` + .replace(/=(?:\r\n?|\n|$)/g, '') + // https://tools.ietf.org/html/rfc2045#section-6.7, note 1. + .replace(/=([a-fA-F0-9]{2})/g, ($0, $1) => { + const codePoint = parseInt($1, 16); + return String.fromCharCode(codePoint); + }); + } + + /** + * Decode a string encoded as "UTF-8" and return a regular string + * + * See: https://github.com/kvz/locutus/blob/master/src/php/xml/utf8_decode.js + * + * @param {string} input - The UTF-8 string + * @return {string} The decoded string + */ + _decodeUTF8(input) { + try { + const output = []; + let i = 0; + let c1 = 0; + let seqlen = 0; + + while (i < input.length) { + c1 = input.charCodeAt(i) & 0xFF; + seqlen = 0; + + if (c1 <= 0xBF) { + c1 &= 0x7F; + seqlen = 1; + } else if (c1 <= 0xDF) { + c1 &= 0x1F; + seqlen = 2; + } else if (c1 <= 0xEF) { + c1 &= 0x0F; + seqlen = 3; + } else { + c1 &= 0x07; + seqlen = 4; + } + + for (let ai = 1; ai < seqlen; ++ai) + c1 = ((c1 << 0x06) | (input.charCodeAt(ai + i) & 0x3F)); + + if (seqlen === 4) { + c1 -= 0x10000; + output.push(String.fromCharCode(0xD800 | ((c1 >> 10) & 0x3FF))); + output.push(String.fromCharCode(0xDC00 | (c1 & 0x3FF))); + } else { + output.push(String.fromCharCode(c1)); + } + + i += seqlen; + } + + return output.join(''); + + // Fallback to old unfaithful + } catch (e) { + try { + return decodeURIComponent(escape(input)); + + // Say "chowdah" frenchie! + } catch (e) { + debug(e, `Failed to decode UTF-8 VCard field ${input}`); + return input; + } + } + } + + /** + * Parse a vCard (v2.1 only) and return a dictionary of the fields + * + * See: http://jsfiddle.net/ARTsinn/P2t2P/ + * + * @param {string} vcard_data - The raw VCard data + * @return {Object} dictionary of vCard data + */ + _parseVCard21(vcard_data) { + // vcard skeleton + const vcard = { + fn: _('Unknown Contact'), + tel: [], + }; + + // Remove line folding and split + const unfolded = vcard_data.replace(VCARD_FOLDING, ''); + const lines = unfolded.split(/\r\n|\r|\n/); + + for (let i = 0, len = lines.length; i < len; i++) { + const line = lines[i]; + let results, key, type, value; + + // Empty line or a property we aren't interested in + if (!line || !line.match(VCARD_SUPPORTED)) + continue; + + // Basic Fields (fn, x-kdeconnect-timestamp, etc) + if ((results = line.match(VCARD_BASIC))) { + [, key, value] = results; + vcard[key.toLowerCase()] = value; + continue; + } + + // Typed Fields (tel, adr, etc) + if ((results = line.match(VCARD_TYPED))) { + [, key, type, value] = results; + key = key.replace(VCARD_TYPED_KEY, '').toLowerCase(); + value = value.split(';'); + type = type.split(';'); + + // Type(s) + const meta = {}; + + for (let i = 0, len = type.length; i < len; i++) { + const res = type[i].match(VCARD_TYPED_META); + + if (res) + meta[res[1]] = res[2]; + else + meta[`type${i === 0 ? '' : i}`] = type[i].toLowerCase(); + } + + // Value(s) + if (vcard[key] === undefined) + vcard[key] = []; + + // Decode QUOTABLE-PRINTABLE + if (meta.ENCODING && meta.ENCODING === 'QUOTED-PRINTABLE') { + delete meta.ENCODING; + value = value.map(v => this._decodeQuotedPrintable(v)); + } + + // Decode UTF-8 + if (meta.CHARSET && meta.CHARSET === 'UTF-8') { + delete meta.CHARSET; + value = value.map(v => this._decodeUTF8(v)); + } + + // Special case for FN (full name) + if (key === 'fn') + vcard[key] = value[0]; + else + vcard[key].push({meta: meta, value: value}); + } + } + + return vcard; + } + + /** + * Parse a vCard (v2.1 only) using native JavaScript and add it to the + * contact store. + * + * @param {string} uid - The contact UID + * @param {string} vcard_data - The raw vCard data + */ + async _parseVCardNative(uid, vcard_data) { + try { + const vcard = this._parseVCard21(vcard_data); + + const contact = { + id: uid, + name: vcard.fn, + numbers: [], + origin: 'device', + timestamp: parseInt(vcard['x-kdeconnect-timestamp']), + }; + + // Phone Numbers + contact.numbers = vcard.tel.map(entry => { + let type = 'unknown'; + + if (entry.meta && entry.meta.type) + type = entry.meta.type; + + return {type: type, value: entry.value[0]}; + }); + + // Avatar + if (vcard.photo) { + const data = GLib.base64_decode(vcard.photo[0].value[0]); + contact.avatar = await this._store.storeAvatar(data); + } + + this._store.add(contact); + } catch (e) { + debug(e, `Failed to parse VCard contact ${uid}`); + } + } + + /** + * Parse a vCard using libebook and add it to the contact store. + * + * @param {string} uid - The contact UID + * @param {string} vcard_data - The raw vCard data + */ + async _parseVCard(uid, vcard_data) { + try { + const contact = { + id: uid, + name: _('Unknown Contact'), + numbers: [], + origin: 'device', + timestamp: 0, + }; + + const evcard = EBookContacts.VCard.new_from_string(vcard_data); + const attrs = evcard.get_attributes(); + + for (let i = 0, len = attrs.length; i < len; i++) { + const attr = attrs[i]; + let data, number; + + switch (attr.get_name().toLowerCase()) { + case 'fn': + contact.name = attr.get_value(); + break; + + case 'tel': + number = {value: attr.get_value(), type: 'unknown'}; + + if (attr.has_type('CELL')) + number.type = 'cell'; + else if (attr.has_type('HOME')) + number.type = 'home'; + else if (attr.has_type('WORK')) + number.type = 'work'; + + contact.numbers.push(number); + break; + + case 'x-kdeconnect-timestamp': + contact.timestamp = parseInt(attr.get_value()); + break; + + case 'photo': + data = GLib.base64_decode(attr.get_value()); + contact.avatar = await this._store.storeAvatar(data); + break; + } + } + + this._store.add(contact); + } catch (e) { + debug(e, `Failed to parse VCard contact ${uid}`); + } + } + + /** + * Handle an incoming list of contact vCards and pass them to the best + * available parser. + * + * @param {Core.Packet} packet - A `kdeconnect.contacts.response_vcards` + */ + _handleVCards(packet) { + try { + // We don't use this + delete packet.body.uids; + + // Parse each vCard and add the contact + for (const [uid, vcard] of Object.entries(packet.body)) { + if (EBookContacts) + this._parseVCard(uid, vcard); + else + this._parseVCardNative(uid, vcard); + } + } catch (e) { + logError(e, this.device.name); + } + } + + /** + * Request a list of contact UIDs with timestamps. + */ + _requestUids() { + this.device.sendPacket({ + type: 'kdeconnect.contacts.request_all_uids_timestamps', + }); + } + + /** + * Request the vCards for @uids. + * + * @param {string[]} uids - A list of contact UIDs + */ + _requestVCards(uids) { + this.device.sendPacket({ + type: 'kdeconnect.contacts.request_vcards_by_uid', + body: { + uids: uids, + }, + }); + } + + destroy() { + this._store.disconnect(this._contactsStoreReadyId); + this.settings.disconnect(this._contactsSourceChangedId); + + super.destroy(); + } +}); diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/findmyphone.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/findmyphone.js new file mode 100644 index 0000000..8362b2a --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/findmyphone.js @@ -0,0 +1,245 @@ +'use strict'; + +const Gdk = imports.gi.Gdk; +const Gio = imports.gi.Gio; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + +const Components = imports.service.components; +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('Find My Phone'), + description: _('Ring your paired device'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.FindMyPhone', + incomingCapabilities: ['kdeconnect.findmyphone.request'], + outgoingCapabilities: ['kdeconnect.findmyphone.request'], + actions: { + ring: { + label: _('Ring'), + icon_name: 'phonelink-ring-symbolic', + + parameter_type: null, + incoming: [], + outgoing: ['kdeconnect.findmyphone.request'], + }, + }, +}; + + +/** + * FindMyPhone Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/findmyphone + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectFindMyPhonePlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'findmyphone'); + + this._dialog = null; + this._player = Components.acquire('sound'); + this._mixer = Components.acquire('pulseaudio'); + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.findmyphone.request': + this._handleRequest(); + break; + } + } + + /** + * Handle an incoming location request. + */ + _handleRequest() { + try { + // If this is a second request, stop announcing and return + if (this._dialog !== null) { + this._dialog.response(Gtk.ResponseType.DELETE_EVENT); + return; + } + + this._dialog = new Dialog({ + device: this.device, + plugin: this, + }); + + this._dialog.connect('response', () => { + this._dialog = null; + }); + } catch (e) { + this._cancelRequest(); + logError(e, this.device.name); + } + } + + /** + * Cancel any ongoing ringing and destroy the dialog. + */ + _cancelRequest() { + if (this._dialog !== null) + this._dialog.response(Gtk.ResponseType.DELETE_EVENT); + } + + /** + * Request that the remote device announce it's location + */ + ring() { + this.device.sendPacket({ + type: 'kdeconnect.findmyphone.request', + body: {}, + }); + } + + destroy() { + this._cancelRequest(); + + if (this._mixer !== undefined) + this._mixer = Components.release('pulseaudio'); + + if (this._player !== undefined) + this._player = Components.release('sound'); + + super.destroy(); + } +}); + + +/* + * Used to ensure 'audible-bell' is enabled for fallback + */ +const _WM_SETTINGS = new Gio.Settings({ + schema_id: 'org.gnome.desktop.wm.preferences', + path: '/org/gnome/desktop/wm/preferences/', +}); + + +/** + * A custom GtkMessageDialog for alerting of incoming requests + */ +const Dialog = GObject.registerClass({ + GTypeName: 'GSConnectFindMyPhoneDialog', + Properties: { + 'device': GObject.ParamSpec.object( + 'device', + 'Device', + 'The device associated with this window', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'plugin': GObject.ParamSpec.object( + 'plugin', + 'Plugin', + 'The plugin providing messages', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + }, +}, class Dialog extends Gtk.MessageDialog { + _init(params) { + super._init({ + buttons: Gtk.ButtonsType.CLOSE, + device: params.device, + image: new Gtk.Image({ + icon_name: 'phonelink-ring-symbolic', + pixel_size: 512, + halign: Gtk.Align.CENTER, + hexpand: true, + valign: Gtk.Align.CENTER, + vexpand: true, + visible: true, + }), + plugin: params.plugin, + urgency_hint: true, + }); + + this.set_keep_above(true); + this.maximize(); + this.message_area.destroy(); + + // If an output stream is available start fading the volume up + if (this.plugin._mixer && this.plugin._mixer.output) { + this._stream = this.plugin._mixer.output; + + this._previousMuted = this._stream.muted; + this._previousVolume = this._stream.volume; + + this._stream.muted = false; + this._stream.fade(0.85, 15); + + // Otherwise ensure audible-bell is enabled + } else { + this._previousBell = _WM_SETTINGS.get_boolean('audible-bell'); + _WM_SETTINGS.set_boolean('audible-bell', true); + } + + // Start the alarm + if (this.plugin._player !== undefined) + this.plugin._player.loopSound('phone-incoming-call', this.cancellable); + + // Show the dialog + this.show_all(); + } + + vfunc_key_press_event(event) { + this.response(Gtk.ResponseType.DELETE_EVENT); + + return Gdk.EVENT_STOP; + } + + vfunc_motion_notify_event(event) { + this.response(Gtk.ResponseType.DELETE_EVENT); + + return Gdk.EVENT_STOP; + } + + vfunc_response(response_id) { + // Stop the alarm + this.cancellable.cancel(); + + // Restore the mixer level + if (this._stream) { + this._stream.muted = this._previousMuted; + this._stream.fade(this._previousVolume); + + // Restore the audible-bell + } else { + _WM_SETTINGS.set_boolean('audible-bell', this._previousBell); + } + + this.destroy(); + } + + get cancellable() { + if (this._cancellable === undefined) + this._cancellable = new Gio.Cancellable(); + + return this._cancellable; + } + + get device() { + if (this._device === undefined) + this._device = null; + + return this._device; + } + + set device(device) { + this._device = device; + } + + get plugin() { + if (this._plugin === undefined) + this._plugin = null; + + return this._plugin; + } + + set plugin(plugin) { + this._plugin = plugin; + } +}); diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/mousepad.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/mousepad.js new file mode 100644 index 0000000..5d8c89f --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/mousepad.js @@ -0,0 +1,319 @@ +'use strict'; + +const Gdk = imports.gi.Gdk; +const GObject = imports.gi.GObject; + +const Components = imports.service.components; +const {InputDialog} = imports.service.ui.mousepad; +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('Mousepad'), + description: _('Enables the paired device to act as a remote mouse and keyboard'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Mousepad', + incomingCapabilities: [ + 'kdeconnect.mousepad.echo', + 'kdeconnect.mousepad.request', + 'kdeconnect.mousepad.keyboardstate', + ], + outgoingCapabilities: [ + 'kdeconnect.mousepad.echo', + 'kdeconnect.mousepad.request', + 'kdeconnect.mousepad.keyboardstate', + ], + actions: { + keyboard: { + label: _('Keyboard'), + icon_name: 'input-keyboard-symbolic', + + parameter_type: null, + incoming: [ + 'kdeconnect.mousepad.echo', + 'kdeconnect.mousepad.keyboardstate', + ], + outgoing: ['kdeconnect.mousepad.request'], + }, + }, +}; + + +/** + * A map of "KDE Connect" keyvals to Gdk + */ +const KeyMap = new Map([ + [1, Gdk.KEY_BackSpace], + [2, Gdk.KEY_Tab], + [3, Gdk.KEY_Linefeed], + [4, Gdk.KEY_Left], + [5, Gdk.KEY_Up], + [6, Gdk.KEY_Right], + [7, Gdk.KEY_Down], + [8, Gdk.KEY_Page_Up], + [9, Gdk.KEY_Page_Down], + [10, Gdk.KEY_Home], + [11, Gdk.KEY_End], + [12, Gdk.KEY_Return], + [13, Gdk.KEY_Delete], + [14, Gdk.KEY_Escape], + [15, Gdk.KEY_Sys_Req], + [16, Gdk.KEY_Scroll_Lock], + [17, 0], + [18, 0], + [19, 0], + [20, 0], + [21, Gdk.KEY_F1], + [22, Gdk.KEY_F2], + [23, Gdk.KEY_F3], + [24, Gdk.KEY_F4], + [25, Gdk.KEY_F5], + [26, Gdk.KEY_F6], + [27, Gdk.KEY_F7], + [28, Gdk.KEY_F8], + [29, Gdk.KEY_F9], + [30, Gdk.KEY_F10], + [31, Gdk.KEY_F11], + [32, Gdk.KEY_F12], +]); + + +/** + * Mousepad Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/mousepad + * + * TODO: support outgoing mouse events? + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectMousepadPlugin', + Properties: { + 'state': GObject.ParamSpec.boolean( + 'state', + 'State', + 'Remote keyboard state', + GObject.ParamFlags.READABLE, + false + ), + }, +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'mousepad'); + + this._input = Components.acquire('input'); + + this._shareControlChangedId = this.settings.connect( + 'changed::share-control', + this._sendState.bind(this) + ); + } + + get state() { + if (this._state === undefined) + this._state = false; + + return this._state; + } + + connected() { + super.connected(); + + this._sendState(); + } + + disconnected() { + super.disconnected(); + + this._state = false; + this.notify('state'); + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.mousepad.request': + this._handleInput(packet.body); + break; + + case 'kdeconnect.mousepad.echo': + this._handleEcho(packet.body); + break; + + case 'kdeconnect.mousepad.keyboardstate': + this._handleState(packet); + break; + } + } + + /** + * Handle a input event. + * + * @param {Object} input - The body of a `kdeconnect.mousepad.request` + */ + _handleInput(input) { + if (!this.settings.get_boolean('share-control')) + return; + + let keysym; + let modifiers = 0; + + // These are ordered, as much as possible, to create the shortest code + // path for high-frequency, low-latency events (eg. mouse movement) + switch (true) { + case input.hasOwnProperty('scroll'): + this._input.scrollPointer(input.dx, input.dy); + break; + + case (input.hasOwnProperty('dx') && input.hasOwnProperty('dy')): + this._input.movePointer(input.dx, input.dy); + break; + + case (input.hasOwnProperty('key') || input.hasOwnProperty('specialKey')): + // NOTE: \u0000 sometimes sent in advance of a specialKey packet + if (input.key && input.key === '\u0000') + return; + + // Modifiers + if (input.alt) + modifiers |= Gdk.ModifierType.MOD1_MASK; + + if (input.ctrl) + modifiers |= Gdk.ModifierType.CONTROL_MASK; + + if (input.shift) + modifiers |= Gdk.ModifierType.SHIFT_MASK; + + if (input.super) + modifiers |= Gdk.ModifierType.SUPER_MASK; + + // Regular key (printable ASCII or Unicode) + if (input.key) { + this._input.pressKey(input.key, modifiers); + this._sendEcho(input); + + // Special key (eg. non-printable ASCII) + } else if (input.specialKey && KeyMap.has(input.specialKey)) { + keysym = KeyMap.get(input.specialKey); + this._input.pressKey(keysym, modifiers); + this._sendEcho(input); + } + break; + + case input.hasOwnProperty('singleclick'): + this._input.clickPointer(Gdk.BUTTON_PRIMARY); + break; + + case input.hasOwnProperty('doubleclick'): + this._input.doubleclickPointer(Gdk.BUTTON_PRIMARY); + break; + + case input.hasOwnProperty('middleclick'): + this._input.clickPointer(Gdk.BUTTON_MIDDLE); + break; + + case input.hasOwnProperty('rightclick'): + this._input.clickPointer(Gdk.BUTTON_SECONDARY); + break; + + case input.hasOwnProperty('singlehold'): + this._input.pressPointer(Gdk.BUTTON_PRIMARY); + break; + + case input.hasOwnProperty('singlerelease'): + this._input.releasePointer(Gdk.BUTTON_PRIMARY); + break; + + default: + logError(new Error('Unknown input')); + } + } + + /** + * Handle an echo/ACK of a event we sent, displaying it the dialog entry. + * + * @param {Object} input - The body of a `kdeconnect.mousepad.echo` + */ + _handleEcho(input) { + if (!this._dialog || !this._dialog.visible) + return; + + // Skip modifiers + if (input.alt || input.ctrl || input.super) + return; + + if (input.key) { + this._dialog._isAck = true; + this._dialog.text.buffer.text += input.key; + this._dialog._isAck = false; + } else if (KeyMap.get(input.specialKey) === Gdk.KEY_BackSpace) { + this._dialog.text.emit('backspace'); + } + } + + /** + * Handle a state change from the remote keyboard. This is an indication + * that the remote keyboard is ready to accept input. + * + * @param {Object} packet - A `kdeconnect.mousepad.keyboardstate` packet + */ + _handleState(packet) { + this._state = !!packet.body.state; + this.notify('state'); + } + + /** + * Send an echo/ACK of @input, if requested + * + * @param {Object} input - The body of a 'kdeconnect.mousepad.request' + */ + _sendEcho(input) { + if (!input.sendAck) + return; + + delete input.sendAck; + input.isAck = true; + + this.device.sendPacket({ + type: 'kdeconnect.mousepad.echo', + body: input, + }); + } + + /** + * Send the local keyboard state + * + * @param {boolean} state - Whether we're ready to accept input + */ + _sendState() { + this.device.sendPacket({ + type: 'kdeconnect.mousepad.keyboardstate', + body: { + state: this.settings.get_boolean('share-control'), + }, + }); + } + + /** + * Open the Keyboard Input dialog + */ + keyboard() { + if (this._dialog === undefined) { + this._dialog = new InputDialog({ + device: this.device, + plugin: this, + }); + } + + this._dialog.present(); + } + + destroy() { + if (this._input !== undefined) + this._input = Components.release('input'); + + if (this._dialog !== undefined) + this._dialog.destroy(); + + this.settings.disconnect(this._shareControlChangedId); + + super.destroy(); + } +}); diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/mpris.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/mpris.js new file mode 100644 index 0000000..56b23b2 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/mpris.js @@ -0,0 +1,902 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Components = imports.service.components; +const Config = imports.config; +const DBus = imports.service.utils.dbus; +const MPRIS = imports.service.components.mpris; +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('MPRIS'), + description: _('Bidirectional remote media playback control'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.MPRIS', + incomingCapabilities: ['kdeconnect.mpris', 'kdeconnect.mpris.request'], + outgoingCapabilities: ['kdeconnect.mpris', 'kdeconnect.mpris.request'], + actions: {}, +}; + + +/** + * MPRIS Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/mpriscontrol + * + * See also: + * https://specifications.freedesktop.org/mpris-spec/latest/ + * https://github.com/GNOME/gnome-shell/blob/master/js/ui/mpris.js + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectMPRISPlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'mpris'); + + this._players = new Map(); + this._transferring = new WeakSet(); + this._updating = new WeakSet(); + + this._mpris = Components.acquire('mpris'); + + this._playerAddedId = this._mpris.connect( + 'player-added', + this._sendPlayerList.bind(this) + ); + + this._playerRemovedId = this._mpris.connect( + 'player-removed', + this._sendPlayerList.bind(this) + ); + + this._playerChangedId = this._mpris.connect( + 'player-changed', + this._onPlayerChanged.bind(this) + ); + + this._playerSeekedId = this._mpris.connect( + 'player-seeked', + this._onPlayerSeeked.bind(this) + ); + } + + connected() { + super.connected(); + + this._requestPlayerList(); + this._sendPlayerList(); + } + + disconnected() { + super.disconnected(); + + for (const [identity, player] of this._players) { + this._players.delete(identity); + player.destroy(); + } + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.mpris': + this._handleUpdate(packet); + break; + + case 'kdeconnect.mpris.request': + this._handleRequest(packet); + break; + } + } + + /** + * Handle a remote player update. + * + * @param {Core.Packet} packet - A `kdeconnect.mpris` + */ + _handleUpdate(packet) { + try { + if (packet.body.hasOwnProperty('playerList')) + this._handlePlayerList(packet.body.playerList); + else if (packet.body.hasOwnProperty('player')) + this._handlePlayerUpdate(packet); + } catch (e) { + debug(e, this.device.name); + } + } + + /** + * Handle an updated list of remote players. + * + * @param {string[]} playerList - A list of remote player names + */ + _handlePlayerList(playerList) { + // Destroy removed players before adding new ones + for (const player of this._players.values()) { + if (!playerList.includes(player.Identity)) { + this._players.delete(player.Identity); + player.destroy(); + } + } + + for (const identity of playerList) { + if (!this._players.has(identity)) { + const player = new PlayerRemote(this.device, identity); + this._players.set(identity, player); + } + + // Always request player updates; packets are cheap + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: identity, + requestNowPlaying: true, + requestVolume: true, + }, + }); + } + } + + /** + * Handle an update for a remote player. + * + * @param {Object} packet - A `kdeconnect.mpris` packet + */ + _handlePlayerUpdate(packet) { + const player = this._players.get(packet.body.player); + + if (player === undefined) + return; + + if (packet.body.hasOwnProperty('transferringAlbumArt')) + player.handleAlbumArt(packet); + else + player.update(packet.body); + } + + /** + * Request a list of remote players. + */ + _requestPlayerList() { + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + requestPlayerList: true, + }, + }); + } + + /** + * Handle a request for player information or action. + * + * @param {Core.Packet} packet - a `kdeconnect.mpris.request` + * @return {undefined} no return value + */ + _handleRequest(packet) { + // A request for the list of players + if (packet.body.hasOwnProperty('requestPlayerList')) + return this._sendPlayerList(); + + // A request for an unknown player; send the list of players + if (!this._mpris.hasPlayer(packet.body.player)) + return this._sendPlayerList(); + + // An album art request + if (packet.body.hasOwnProperty('albumArtUrl')) + return this._sendAlbumArt(packet); + + // A player command + this._handleCommand(packet); + } + + /** + * Handle an incoming player command or information request + * + * @param {Core.Packet} packet - A `kdeconnect.mpris.request` + */ + async _handleCommand(packet) { + if (!this.settings.get_boolean('share-players')) + return; + + let player; + + try { + player = this._mpris.getPlayer(packet.body.player); + + if (player === undefined || this._updating.has(player)) + return; + + this._updating.add(player); + + // Player Actions + if (packet.body.hasOwnProperty('action')) { + switch (packet.body.action) { + case 'PlayPause': + case 'Play': + case 'Pause': + case 'Next': + case 'Previous': + case 'Stop': + player[packet.body.action](); + break; + + default: + debug(`unknown action: ${packet.body.action}`); + } + } + + // Player Properties + if (packet.body.hasOwnProperty('setLoopStatus')) + player.LoopStatus = packet.body.setLoopStatus; + + if (packet.body.hasOwnProperty('setShuffle')) + player.Shuffle = packet.body.setShuffle; + + if (packet.body.hasOwnProperty('setVolume')) + player.Volume = packet.body.setVolume / 100; + + if (packet.body.hasOwnProperty('Seek')) + await player.Seek(packet.body.Seek * 1000); + + if (packet.body.hasOwnProperty('SetPosition')) { + const offset = (packet.body.SetPosition * 1000) - player.Position; + await player.Seek(offset); + } + + // Information Request + let hasResponse = false; + + const response = { + type: 'kdeconnect.mpris', + body: { + player: packet.body.player, + }, + }; + + if (packet.body.hasOwnProperty('requestNowPlaying')) { + hasResponse = true; + + Object.assign(response.body, { + pos: Math.floor(player.Position / 1000), + isPlaying: (player.PlaybackStatus === 'Playing'), + canPause: player.CanPause, + canPlay: player.CanPlay, + canGoNext: player.CanGoNext, + canGoPrevious: player.CanGoPrevious, + canSeek: player.CanSeek, + loopStatus: player.LoopStatus, + shuffle: player.Shuffle, + + // default values for members that will be filled conditionally + albumArtUrl: '', + length: 0, + artist: '', + title: '', + album: '', + nowPlaying: '', + volume: 0, + }); + + const metadata = player.Metadata; + + if (metadata.hasOwnProperty('mpris:artUrl')) { + const file = Gio.File.new_for_uri(metadata['mpris:artUrl']); + response.body.albumArtUrl = file.get_uri(); + } + + if (metadata.hasOwnProperty('mpris:length')) { + const trackLen = Math.floor(metadata['mpris:length'] / 1000); + response.body.length = trackLen; + } + + if (metadata.hasOwnProperty('xesam:artist')) { + const artists = metadata['xesam:artist']; + response.body.artist = artists.join(', '); + } + + if (metadata.hasOwnProperty('xesam:title')) + response.body.title = metadata['xesam:title']; + + if (metadata.hasOwnProperty('xesam:album')) + response.body.album = metadata['xesam:album']; + + // Now Playing + if (response.body.artist && response.body.title) { + response.body.nowPlaying = [ + response.body.artist, + response.body.title, + ].join(' - '); + } else if (response.body.artist) { + response.body.nowPlaying = response.body.artist; + } else if (response.body.title) { + response.body.nowPlaying = response.body.title; + } else { + response.body.nowPlaying = _('Unknown'); + } + } + + if (packet.body.hasOwnProperty('requestVolume')) { + hasResponse = true; + response.body.volume = Math.floor(player.Volume * 100); + } + + if (hasResponse) + this.device.sendPacket(response); + } catch (e) { + debug(e, this.device.name); + } finally { + this._updating.delete(player); + } + } + + _onPlayerChanged(mpris, player) { + if (!this.settings.get_boolean('share-players')) + return; + + this._handleCommand({ + body: { + player: player.Identity, + requestNowPlaying: true, + requestVolume: true, + }, + }); + } + + _onPlayerSeeked(mpris, player, offset) { + // TODO: although we can handle full seeked signals, kdeconnect-android + // does not, and expects a position update instead + this.device.sendPacket({ + type: 'kdeconnect.mpris', + body: { + player: player.Identity, + pos: Math.floor(player.Position / 1000), + // Seek: Math.floor(offset / 1000), + }, + }); + } + + async _sendAlbumArt(packet) { + let player; + + try { + // Reject concurrent requests for album art + player = this._mpris.getPlayer(packet.body.player); + + if (player === undefined || this._transferring.has(player)) + return; + + // Ensure the requested albumArtUrl matches the current mpris:artUrl + const metadata = player.Metadata; + + if (!metadata.hasOwnProperty('mpris:artUrl')) + return; + + const file = Gio.File.new_for_uri(metadata['mpris:artUrl']); + const request = Gio.File.new_for_uri(packet.body.albumArtUrl); + + if (file.get_uri() !== request.get_uri()) + throw RangeError(`invalid URI "${packet.body.albumArtUrl}"`); + + // Transfer the album art + this._transferring.add(player); + + const transfer = this.device.createTransfer(); + + transfer.addFile({ + type: 'kdeconnect.mpris', + body: { + transferringAlbumArt: true, + player: packet.body.player, + albumArtUrl: packet.body.albumArtUrl, + }, + }, file); + + await transfer.start(); + } catch (e) { + debug(e, this.device.name); + } finally { + this._transferring.delete(player); + } + } + + /** + * Send the list of player identities and indicate whether we support + * transferring album art + */ + _sendPlayerList() { + let playerList = []; + + if (this.settings.get_boolean('share-players')) + playerList = this._mpris.getIdentities(); + + this.device.sendPacket({ + type: 'kdeconnect.mpris', + body: { + playerList: playerList, + supportAlbumArtPayload: true, + }, + }); + } + + destroy() { + if (this._mpris !== undefined) { + this._mpris.disconnect(this._playerAddedId); + this._mpris.disconnect(this._playerRemovedId); + this._mpris.disconnect(this._playerChangedId); + this._mpris.disconnect(this._playerSeekedId); + this._mpris = Components.release('mpris'); + } + + for (const [identity, player] of this._players) { + this._players.delete(identity); + player.destroy(); + } + + super.destroy(); + } +}); + + +/* + * A class for mirroring a remote Media Player on DBus + */ +const MPRISIface = Config.DBUS.lookup_interface('org.mpris.MediaPlayer2'); +const MPRISPlayerIface = Config.DBUS.lookup_interface('org.mpris.MediaPlayer2.Player'); + + +const PlayerRemote = GObject.registerClass({ + GTypeName: 'GSConnectMPRISPlayerRemote', +}, class PlayerRemote extends MPRIS.Player { + + _init(device, identity) { + super._init(); + + this._device = device; + this._Identity = identity; + this._isPlaying = false; + + this._artist = null; + this._title = null; + this._album = null; + this._length = 0; + this._artUrl = null; + + this._ownerId = 0; + this._connection = null; + this._applicationIface = null; + this._playerIface = null; + } + + _getFile(albumArtUrl) { + const hash = GLib.compute_checksum_for_string(GLib.ChecksumType.MD5, + albumArtUrl, -1); + const path = GLib.build_filenamev([Config.CACHEDIR, hash]); + + return Gio.File.new_for_uri(`file://${path}`); + } + + _requestAlbumArt(state) { + if (this._artUrl === state.albumArtUrl) + return; + + const file = this._getFile(state.albumArtUrl); + + if (file.query_exists(null)) { + this._artUrl = file.get_uri(); + this._Metadata = undefined; + this.notify('Metadata'); + } else { + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: this.Identity, + albumArtUrl: state.albumArtUrl, + }, + }); + } + } + + _updateMetadata(state) { + let metadataChanged = false; + + if (state.hasOwnProperty('artist')) { + if (this._artist !== state.artist) { + this._artist = state.artist; + metadataChanged = true; + } + } else if (this._artist) { + this._artist = null; + metadataChanged = true; + } + + if (state.hasOwnProperty('title')) { + if (this._title !== state.title) { + this._title = state.title; + metadataChanged = true; + } + } else if (this._title) { + this._title = null; + metadataChanged = true; + } + + if (state.hasOwnProperty('album')) { + if (this._album !== state.album) { + this._album = state.album; + metadataChanged = true; + } + } else if (this._album) { + this._album = null; + metadataChanged = true; + } + + if (state.hasOwnProperty('length')) { + if (this._length !== state.length * 1000) { + this._length = state.length * 1000; + metadataChanged = true; + } + } else if (this._length) { + this._length = 0; + metadataChanged = true; + } + + if (state.hasOwnProperty('albumArtUrl')) { + this._requestAlbumArt(state); + } else if (this._artUrl) { + this._artUrl = null; + metadataChanged = true; + } + + if (metadataChanged) { + this._Metadata = undefined; + this.notify('Metadata'); + } + } + + async export() { + try { + if (this._connection === null) { + this._connection = await DBus.newConnection(); + + if (this._applicationIface === null) { + this._applicationIface = new DBus.Interface({ + g_instance: this, + g_connection: this._connection, + g_object_path: '/org/mpris/MediaPlayer2', + g_interface_info: MPRISIface, + }); + } + + if (this._playerIface === null) { + this._playerIface = new DBus.Interface({ + g_instance: this, + g_connection: this._connection, + g_object_path: '/org/mpris/MediaPlayer2', + g_interface_info: MPRISPlayerIface, + }); + } + } + + if (this._ownerId !== 0) + return; + + const name = [ + this.device.name, + this.Identity, + ].join('').replace(/[\W]*/g, ''); + + this._ownerId = Gio.bus_own_name_on_connection( + this._connection, + `org.mpris.MediaPlayer2.GSConnect.${name}`, + Gio.BusNameOwnerFlags.NONE, + null, + null + ); + } catch (e) { + debug(e, this.Identity); + } + } + + unexport() { + if (this._ownerId === 0) + return; + + Gio.bus_unown_name(this._ownerId); + this._ownerId = 0; + } + + /** + * Download album art for the current track of the remote player. + * + * @param {Core.Packet} packet - A `kdeconnect.mpris` packet + */ + async handleAlbumArt(packet) { + let file; + + try { + file = this._getFile(packet.body.albumArtUrl); + + // Transfer the album art + const transfer = this.device.createTransfer(); + transfer.addFile(packet, file); + + await transfer.start(); + + this._artUrl = file.get_uri(); + this._Metadata = undefined; + this.notify('Metadata'); + } catch (e) { + debug(e, this.device.name); + + if (file) + file.delete_async(GLib.PRIORITY_DEFAULT, null, null); + } + } + + /** + * Update the internal state of the media player. + * + * @param {Core.Packet} state - The body of a `kdeconnect.mpris` packet + */ + update(state) { + this.freeze_notify(); + + // Metadata + if (state.hasOwnProperty('nowPlaying')) + this._updateMetadata(state); + + // Playback Status + if (state.hasOwnProperty('isPlaying')) { + if (this._isPlaying !== state.isPlaying) { + this._isPlaying = state.isPlaying; + this.notify('PlaybackStatus'); + } + } + + if (state.hasOwnProperty('canPlay')) { + if (this.CanPlay !== state.canPlay) { + this._CanPlay = state.canPlay; + this.notify('CanPlay'); + } + } + + if (state.hasOwnProperty('canPause')) { + if (this.CanPause !== state.canPause) { + this._CanPause = state.canPause; + this.notify('CanPause'); + } + } + + if (state.hasOwnProperty('canGoNext')) { + if (this.CanGoNext !== state.canGoNext) { + this._CanGoNext = state.canGoNext; + this.notify('CanGoNext'); + } + } + + if (state.hasOwnProperty('canGoPrevious')) { + if (this.CanGoPrevious !== state.canGoPrevious) { + this._CanGoPrevious = state.canGoPrevious; + this.notify('CanGoPrevious'); + } + } + + if (state.hasOwnProperty('pos')) + this._Position = state.pos * 1000; + + if (state.hasOwnProperty('volume')) { + if (this.Volume !== state.volume / 100) { + this._Volume = state.volume / 100; + this.notify('Volume'); + } + } + + this.thaw_notify(); + + if (!this._isPlaying && !this.CanControl) + this.unexport(); + else + this.export(); + } + + /* + * Native properties + */ + get device() { + return this._device; + } + + /* + * The org.mpris.MediaPlayer2.Player Interface + */ + get CanControl() { + return (this.CanPlay || this.CanPause); + } + + get Metadata() { + if (this._Metadata === undefined) { + this._Metadata = {}; + + if (this._artist) { + this._Metadata['xesam:artist'] = new GLib.Variant('as', + [this._artist]); + } + + if (this._title) { + this._Metadata['xesam:title'] = new GLib.Variant('s', + this._title); + } + + if (this._album) { + this._Metadata['xesam:album'] = new GLib.Variant('s', + this._album); + } + + if (this._artUrl) { + this._Metadata['mpris:artUrl'] = new GLib.Variant('s', + this._artUrl); + } + + this._Metadata['mpris:length'] = new GLib.Variant('x', + this._length); + } + + return this._Metadata; + } + + get PlaybackStatus() { + if (this._isPlaying) + return 'Playing'; + + return 'Stopped'; + } + + get Volume() { + if (this._Volume === undefined) + this._Volume = 0.3; + + return this._Volume; + } + + set Volume(level) { + if (this._Volume === level) + return; + + this._Volume = level; + this.notify('Volume'); + + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: this.Identity, + setVolume: Math.floor(this._Volume * 100), + }, + }); + } + + Next() { + if (!this.CanGoNext) + return; + + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: this.Identity, + action: 'Next', + }, + }); + } + + Pause() { + if (!this.CanPause) + return; + + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: this.Identity, + action: 'Pause', + }, + }); + } + + Play() { + if (!this.CanPlay) + return; + + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: this.Identity, + action: 'Play', + }, + }); + } + + PlayPause() { + if (!this.CanPlay && !this.CanPause) + return; + + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: this.Identity, + action: 'PlayPause', + }, + }); + } + + Previous() { + if (!this.CanGoPrevious) + return; + + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: this.Identity, + action: 'Previous', + }, + }); + } + + Seek(offset) { + if (!this.CanSeek) + return; + + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: this.Identity, + Seek: offset, + }, + }); + } + + SetPosition(trackId, position) { + debug(`${this._Identity}: SetPosition(${trackId}, ${position})`); + + if (!this.CanControl || !this.CanSeek) + return; + + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: this.Identity, + SetPosition: position / 1000, + }, + }); + } + + Stop() { + if (!this.CanControl) + return; + + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: this.Identity, + action: 'Stop', + }, + }); + } + + destroy() { + this.unexport(); + + if (this._connection) { + this._connection.close(null, null); + this._connection = null; + + if (this._applicationIface) { + this._applicationIface.destroy(); + this._applicationIface = null; + } + + if (this._playerIface) { + this._playerIface.destroy(); + this._playerIface = null; + } + } + } +}); diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/notification.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/notification.js new file mode 100644 index 0000000..3765388 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/notification.js @@ -0,0 +1,713 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + +const Components = imports.service.components; +const Config = imports.config; +const PluginBase = imports.service.plugin; +const NotificationUI = imports.service.ui.notification; + + +var Metadata = { + label: _('Notifications'), + description: _('Share notifications with the paired device'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Notification', + incomingCapabilities: [ + 'kdeconnect.notification', + 'kdeconnect.notification.request', + ], + outgoingCapabilities: [ + 'kdeconnect.notification', + 'kdeconnect.notification.action', + 'kdeconnect.notification.reply', + 'kdeconnect.notification.request', + ], + actions: { + withdrawNotification: { + label: _('Cancel Notification'), + icon_name: 'preferences-system-notifications-symbolic', + + parameter_type: new GLib.VariantType('s'), + incoming: [], + outgoing: ['kdeconnect.notification'], + }, + closeNotification: { + label: _('Close Notification'), + icon_name: 'preferences-system-notifications-symbolic', + + parameter_type: new GLib.VariantType('s'), + incoming: [], + outgoing: ['kdeconnect.notification.request'], + }, + replyNotification: { + label: _('Reply Notification'), + icon_name: 'preferences-system-notifications-symbolic', + + parameter_type: new GLib.VariantType('(ssa{ss})'), + incoming: ['kdeconnect.notification'], + outgoing: ['kdeconnect.notification.reply'], + }, + sendNotification: { + label: _('Send Notification'), + icon_name: 'preferences-system-notifications-symbolic', + + parameter_type: new GLib.VariantType('a{sv}'), + incoming: [], + outgoing: ['kdeconnect.notification'], + }, + activateNotification: { + label: _('Activate Notification'), + icon_name: 'preferences-system-notifications-symbolic', + + parameter_type: new GLib.VariantType('(ss)'), + incoming: [], + outgoing: ['kdeconnect.notification.action'], + }, + }, +}; + + +// A regex for our custom notificaiton ids +const ID_REGEX = /^(fdo|gtk)\|([^|]+)\|(.*)$/; + +// A list of known SMS apps +const SMS_APPS = [ + // Popular apps that don't contain the string 'sms' + 'com.android.messaging', // AOSP + 'com.google.android.apps.messaging', // Google Messages + 'com.textra', // Textra + 'xyz.klinker.messenger', // Pulse + 'com.calea.echo', // Mood Messenger + 'com.moez.QKSMS', // QKSMS + 'rpkandrodev.yaata', // YAATA + 'com.tencent.mm', // WeChat + 'com.viber.voip', // Viber + 'com.kakao.talk', // KakaoTalk + 'com.concentriclivers.mms.com.android.mms', // AOSP Clone + 'fr.slvn.mms', // AOSP Clone + 'com.promessage.message', // + 'com.htc.sense.mms', // HTC Messages + + // Known not to work with sms plugin + 'org.thoughtcrime.securesms', // Signal Private Messenger + 'com.samsung.android.messaging', // Samsung Messages +]; + + +/** + * Try to determine if an notification is from an SMS app + * + * @param {Core.Packet} packet - A `kdeconnect.notification` + * @return {boolean} Whether the notification is from an SMS app + */ +function _isSmsNotification(packet) { + const id = packet.body.id; + + if (id.includes('sms')) + return true; + + for (let i = 0, len = SMS_APPS.length; i < len; i++) { + if (id.includes(SMS_APPS[i])) + return true; + } + + return false; +} + + +/** + * Remove a local libnotify or Gtk notification. + * + * @param {String|Number} id - Gtk (string) or libnotify id (uint32) + * @param {String|null} application - Application Id if Gtk or null + */ +function _removeNotification(id, application = null) { + let name, path, method, variant; + + if (application !== null) { + name = 'org.gtk.Notifications'; + method = 'RemoveNotification'; + path = '/org/gtk/Notifications'; + variant = new GLib.Variant('(ss)', [application, id]); + } else { + name = 'org.freedesktop.Notifications'; + path = '/org/freedesktop/Notifications'; + method = 'CloseNotification'; + variant = new GLib.Variant('(u)', [id]); + } + + Gio.DBus.session.call( + name, path, name, method, variant, null, + Gio.DBusCallFlags.NONE, -1, null, + (connection, res) => { + try { + connection.call_finish(res); + } catch (e) { + logError(e); + } + } + ); +} + + +/** + * Notification Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/notifications + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/sendnotifications + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectNotificationPlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'notification'); + + this._listener = Components.acquire('notification'); + this._session = Components.acquire('session'); + + this._notificationAddedId = this._listener.connect( + 'notification-added', + this._onNotificationAdded.bind(this) + ); + + // Load application notification settings + this._applicationsChangedId = this.settings.connect( + 'changed::applications', + this._onApplicationsChanged.bind(this) + ); + this._onApplicationsChanged(this.settings, 'applications'); + this._applicationsChangedSkip = false; + } + + connected() { + super.connected(); + + this._requestNotifications(); + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.notification': + this._handleNotification(packet); + break; + + // TODO + case 'kdeconnect.notification.action': + this._handleNotificationAction(packet); + break; + + // No Linux/BSD desktop notifications are repliable as yet + case 'kdeconnect.notification.reply': + debug(`Not implemented: ${packet.type}`); + break; + + case 'kdeconnect.notification.request': + this._handleNotificationRequest(packet); + break; + + default: + debug(`Unknown notification packet: ${packet.type}`); + } + } + + _onApplicationsChanged(settings, key) { + if (this._applicationsChangedSkip) + return; + + try { + const json = settings.get_string(key); + this._applications = JSON.parse(json); + } catch (e) { + debug(e, this.device.name); + + this._applicationsChangedSkip = true; + settings.set_string(key, '{}'); + this._applicationsChangedSkip = false; + } + } + + _onNotificationAdded(listener, notification) { + try { + const notif = notification.full_unpack(); + + // An unconfigured application + if (notif.appName && !this._applications[notif.appName]) { + this._applications[notif.appName] = { + iconName: 'system-run-symbolic', + enabled: true, + }; + + // Store the themed icons for the device preferences window + if (notif.icon === undefined) { + // Keep default + + } else if (typeof notif.icon === 'string') { + this._applications[notif.appName].iconName = notif.icon; + + } else if (notif.icon instanceof Gio.ThemedIcon) { + const iconName = notif.icon.get_names()[0]; + this._applications[notif.appName].iconName = iconName; + } + + this._applicationsChangedSkip = true; + this.settings.set_string( + 'applications', + JSON.stringify(this._applications) + ); + this._applicationsChangedSkip = false; + } + + // Sending notifications forbidden + if (!this.settings.get_boolean('send-notifications')) + return; + + // Sending when the session is active is forbidden + if (!this.settings.get_boolean('send-active') && this._session.active) + return; + + // Notifications disabled for this application + if (notif.appName && !this._applications[notif.appName].enabled) + return; + + this.sendNotification(notif); + } catch (e) { + debug(e, this.device.name); + } + } + + /** + * Handle an incoming notification or closed report. + * + * FIXME: upstream kdeconnect-android is tagging many notifications as + * `silent`, causing them to never be shown. Since we already handle + * duplicates in the Shell, we ignore that flag for now. + * + * @param {Core.Packet} packet - A `kdeconnect.notification` + */ + _handleNotification(packet) { + // A report that a remote notification has been dismissed + if (packet.body.hasOwnProperty('isCancel')) + this.device.hideNotification(packet.body.id); + + // A normal, remote notification + else + this._receiveNotification(packet); + } + + /** + * Handle an incoming request to activate a notification action. + * + * @param {Core.Packet} packet - A `kdeconnect.notification.action` + */ + _handleNotificationAction(packet) { + throw new GObject.NotImplementedError(); + } + + /** + * Handle an incoming request to close or list notifications. + * + * @param {Core.Packet} packet - A `kdeconnect.notification.request` + */ + _handleNotificationRequest(packet) { + // A request for our notifications. This isn't implemented and would be + // pretty hard to without communicating with GNOME Shell. + if (packet.body.hasOwnProperty('request')) + return; + + // A request to close a local notification + // + // TODO: kdeconnect-android doesn't send these, and will instead send a + // kdeconnect.notification packet with isCancel and an id of "0". + // + // For clients that do support it, we report notification ids in the + // form "type|application-id|notification-id" so we can close it with + // the appropriate service. + if (packet.body.hasOwnProperty('cancel')) { + const [, type, application, id] = ID_REGEX.exec(packet.body.cancel); + + if (type === 'fdo') + _removeNotification(parseInt(id)); + else if (type === 'gtk') + _removeNotification(id, application); + } + } + + /** + * Upload an icon from a GLib.Bytes object. + * + * @param {Core.Packet} packet - The packet for the notification + * @param {GLib.Bytes} bytes - The icon bytes + */ + _uploadBytesIcon(packet, bytes) { + const stream = Gio.MemoryInputStream.new_from_bytes(bytes); + this._uploadIconStream(packet, stream, bytes.get_size()); + } + + /** + * Upload an icon from a Gio.File object. + * + * @param {Core.Packet} packet - A `kdeconnect.notification` + * @param {Gio.File} file - A file object for the icon + */ + async _uploadFileIcon(packet, file) { + const read = new Promise((resolve, reject) => { + file.read_async(GLib.PRIORITY_DEFAULT, null, (file, res) => { + try { + resolve(file.read_finish(res)); + } catch (e) { + reject(e); + } + }); + }); + + const query = new Promise((resolve, reject) => { + file.query_info_async( + 'standard::size', + Gio.FileQueryInfoFlags.NONE, + GLib.PRIORITY_DEFAULT, + null, + (file, res) => { + try { + resolve(file.query_info_finish(res)); + } catch (e) { + reject(e); + } + } + ); + }); + + const [stream, info] = await Promise.all([read, query]); + + this._uploadIconStream(packet, stream, info.get_size()); + } + + /** + * A function for uploading GThemedIcons + * + * @param {Core.Packet} packet - The packet for the notification + * @param {Gio.ThemedIcon} icon - The GIcon to upload + */ + _uploadThemedIcon(packet, icon) { + const theme = Gtk.IconTheme.get_default(); + let file = null; + + for (const name of icon.names) { + // NOTE: kdeconnect-android doesn't support SVGs + const size = Math.max.apply(null, theme.get_icon_sizes(name)); + const info = theme.lookup_icon(name, size, Gtk.IconLookupFlags.NO_SVG); + + // Send the first icon we find from the options + if (info) { + file = Gio.File.new_for_path(info.get_filename()); + break; + } + } + + if (file) + this._uploadFileIcon(packet, file); + else + this.device.sendPacket(packet); + } + + /** + * All icon types end up being uploaded in this function. + * + * @param {Core.Packet} packet - The packet for the notification + * @param {Gio.InputStream} stream - A stream to read the icon bytes from + * @param {number} size - Size of the icon in bytes + */ + async _uploadIconStream(packet, stream, size) { + try { + const transfer = this.device.createTransfer(); + transfer.addStream(packet, stream, size); + + await transfer.start(); + } catch (e) { + debug(e); + + this.device.sendPacket(packet); + } + } + + /** + * Upload an icon from a GIcon or themed icon name. + * + * @param {Core.Packet} packet - A `kdeconnect.notification` + * @param {Gio.Icon|string|null} icon - An icon or %null + * @return {Promise} A promise for the operation + */ + _uploadIcon(packet, icon = null) { + // Normalize strings into GIcons + if (typeof icon === 'string') + icon = Gio.Icon.new_for_string(icon); + + if (icon instanceof Gio.ThemedIcon) + return this._uploadThemedIcon(packet, icon); + + if (icon instanceof Gio.FileIcon) + return this._uploadFileIcon(packet, icon.get_file()); + + if (icon instanceof Gio.BytesIcon) + return this._uploadBytesIcon(packet, icon.get_bytes()); + + return this.device.sendPacket(packet); + } + + /** + * Send a local notification to the remote device. + * + * @param {Object} notif - A dictionary of notification parameters + * @param {string} notif.appName - The notifying application + * @param {string} notif.id - The notification ID + * @param {string} notif.title - The notification title + * @param {string} notif.body - The notification body + * @param {string} notif.ticker - The notification title & body + * @param {boolean} notif.isClearable - If the notification can be closed + * @param {string|Gio.Icon} notif.icon - An icon name or GIcon + */ + async sendNotification(notif) { + try { + const icon = notif.icon || null; + delete notif.icon; + + await this._uploadIcon({ + type: 'kdeconnect.notification', + body: notif, + }, icon); + } catch (e) { + logError(e); + } + } + + async _downloadIcon(packet) { + try { + if (!packet.hasPayload()) + return null; + + // Save the file in the global cache + const path = GLib.build_filenamev([ + Config.CACHEDIR, + packet.body.payloadHash || `${Date.now()}`, + ]); + + // Check if we've already downloaded this icon + // NOTE: if we reject the transfer kdeconnect-android will resend + // the notification packet, which may cause problems wrt #789 + const file = Gio.File.new_for_path(path); + + if (file.query_exists(null)) + return new Gio.FileIcon({file: file}); + + // Open the target path and create a transfer + const transfer = this.device.createTransfer(); + + transfer.addFile(packet, file); + + try { + await transfer.start(); + + return new Gio.FileIcon({file: file}); + } catch (e) { + debug(e, this.device.name); + + file.delete_async(GLib.PRIORITY_DEFAULT, null, null); + return null; + } + } catch (e) { + debug(e, this.device.name); + return null; + } + } + + /** + * Receive an incoming notification. + * + * @param {Core.Packet} packet - A `kdeconnect.notification` + */ + async _receiveNotification(packet) { + try { + // Set defaults + let action = null; + let buttons = []; + let id = packet.body.id; + let title = packet.body.appName; + let body = `${packet.body.title}: ${packet.body.text}`; + let icon = await this._downloadIcon(packet); + + // Repliable Notification + if (packet.body.requestReplyId) { + id = `${packet.body.id}|${packet.body.requestReplyId}`; + action = { + name: 'replyNotification', + parameter: new GLib.Variant('(ssa{ss})', [ + packet.body.requestReplyId, + '', + { + appName: packet.body.appName, + title: packet.body.title, + text: packet.body.text, + }, + ]), + }; + } + + // Notification Actions + if (packet.body.actions) { + buttons = packet.body.actions.map(action => { + return { + label: action, + action: 'activateNotification', + parameter: new GLib.Variant('(ss)', [id, action]), + }; + }); + } + + // Special case for Missed Calls + if (packet.body.id.includes('MissedCall')) { + title = packet.body.title; + body = packet.body.text; + + if (icon === null) + icon = new Gio.ThemedIcon({name: 'call-missed-symbolic'}); + + // Special case for SMS notifications + } else if (_isSmsNotification(packet)) { + title = packet.body.title; + body = packet.body.text; + action = { + name: 'replySms', + parameter: new GLib.Variant('s', packet.body.title), + }; + + if (icon === null) + icon = new Gio.ThemedIcon({name: 'sms-symbolic'}); + + // Special case where 'appName' is the same as 'title' + } else if (packet.body.appName === packet.body.title) { + body = packet.body.text; + } + + // Use the device icon if we still don't have one + if (icon === null) + icon = new Gio.ThemedIcon({name: this.device.icon_name}); + + // Show the notification + this.device.showNotification({ + id: id, + title: title, + body: body, + icon: icon, + action: action, + buttons: buttons, + }); + } catch (e) { + logError(e); + } + } + + /** + * Request the remote notifications be sent + */ + _requestNotifications() { + this.device.sendPacket({ + type: 'kdeconnect.notification.request', + body: {request: true}, + }); + } + + /** + * Report that a local notification has been closed/dismissed. + * TODO: kdeconnect-android doesn't handle incoming isCancel packets. + * + * @param {string} id - The local notification id + */ + withdrawNotification(id) { + this.device.sendPacket({ + type: 'kdeconnect.notification', + body: { + isCancel: true, + id: id, + }, + }); + } + + /** + * Close a remote notification. + * TODO: ignore local notifications + * + * @param {string} id - The remote notification id + */ + closeNotification(id) { + this.device.sendPacket({ + type: 'kdeconnect.notification.request', + body: {cancel: id}, + }); + } + + /** + * Reply to a notification sent with a requestReplyId UUID + * + * @param {string} uuid - The requestReplyId for the repliable notification + * @param {string} message - The message to reply with + * @param {Object} notification - The original notification packet + */ + replyNotification(uuid, message, notification) { + // If this happens for some reason, things will explode + if (!uuid) + throw Error('Missing UUID'); + + // If the message has no content, open a dialog for the user to add one + if (!message) { + const dialog = new NotificationUI.ReplyDialog({ + device: this.device, + uuid: uuid, + notification: notification, + plugin: this, + }); + dialog.present(); + + // Otherwise just send the reply + } else { + this.device.sendPacket({ + type: 'kdeconnect.notification.reply', + body: { + requestReplyId: uuid, + message: message, + }, + }); + } + } + + /** + * Activate a remote notification action + * + * @param {string} id - The remote notification id + * @param {string} action - The notification action (label) + */ + activateNotification(id, action) { + this.device.sendPacket({ + type: 'kdeconnect.notification.action', + body: { + action: action, + key: id, + }, + }); + } + + destroy() { + this.settings.disconnect(this._applicationsChangedId); + + if (this._listener !== undefined) { + this._listener.disconnect(this._notificationAddedId); + this._listener = Components.release('notification'); + } + + if (this._session !== undefined) + this._session = Components.release('session'); + + super.destroy(); + } +}); diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/photo.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/photo.js new file mode 100644 index 0000000..e417c70 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/photo.js @@ -0,0 +1,241 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Config = imports.config; +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('Photo'), + description: _('Request the paired device to take a photo and transfer it to this PC'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Photo', + incomingCapabilities: [ + 'kdeconnect.photo', + 'kdeconnect.photo.request', + ], + outgoingCapabilities: [ + 'kdeconnect.photo', + 'kdeconnect.photo.request', + ], + actions: { + photo: { + label: _('Photo'), + icon_name: 'camera-photo-symbolic', + + parameter_type: null, + incoming: ['kdeconnect.photo'], + outgoing: ['kdeconnect.photo.request'], + }, + }, +}; + + +/** + * Photo Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/photo + * + * TODO: use Cheese? + * check for /dev/video* + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectPhotoPlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'photo'); + + // A reusable launcher for silence procs + this._launcher = new Gio.SubprocessLauncher({ + flags: (Gio.SubprocessFlags.STDOUT_SILENCE | + Gio.SubprocessFlags.STDERR_SILENCE), + }); + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.photo': + this._receivePhoto(packet); + break; + + case 'kdeconnect.photo.request': + this._sendPhoto(packet); + break; + } + } + + /** + * Ensure we have a directory set for storing files that exists. + * + * @return {string} An absolute directory path + */ + _ensureReceiveDirectory() { + if (this._receiveDir !== undefined) + return this._receiveDir; + + // Ensure a directory is set + this._receiveDir = this.settings.get_string('receive-directory'); + + if (this._receiveDir === '') { + this._receiveDir = GLib.get_user_special_dir( + GLib.UserDirectory.DIRECTORY_PICTURES + ); + + // Fallback to ~/Pictures + const homeDir = GLib.get_home_dir(); + + if (!this._receiveDir || this._receiveDir === homeDir) { + this._receiveDir = GLib.build_filenamev([homeDir, 'Pictures']); + this.settings.set_string('receive-directory', this._receiveDir); + } + } + + // Ensure the directory exists + if (!GLib.file_test(this._receiveDir, GLib.FileTest.IS_DIR)) + GLib.mkdir_with_parents(this._receiveDir, 448); + + return this._receiveDir; + } + + /** + * Get a GFile for @filename, while ensuring the directory exists and the + * file is unique. + * + * @param {string} filename - A filename (eg. `image.jpg`) + * @return {Gio.File} a file object + */ + _getFile(filename) { + const dirpath = this._ensureReceiveDirectory(); + const basepath = GLib.build_filenamev([dirpath, filename]); + let filepath = basepath; + let copyNum = 0; + + while (GLib.file_test(filepath, GLib.FileTest.EXISTS)) + filepath = `${basepath} (${++copyNum})`; + + return Gio.File.new_for_path(filepath); + } + + /** + * Receive a photo taken by the remote device. + * + * @param {Core.Packet} packet - a `kdeconnect.photo` + */ + async _receivePhoto(packet) { + let file, transfer; + + try { + // Remote device cancelled the photo operation + if (packet.body.hasOwnProperty('cancel')) + return; + + // Open the target path and create a transfer + file = this._getFile(packet.body.filename); + + transfer = this.device.createTransfer(); + transfer.addFile(packet, file); + + // Open the photo if successful, delete on failure + await transfer.start(); + + const uri = file.get_uri(); + Gio.AppInfo.launch_default_for_uri_async(uri, null, null, null); + } catch (e) { + debug(e, this.device.name); + + if (file) + file.delete_async(GLib.PRIORITY_DEFAULT, null, null); + } + } + + /** + * Take a photo using the Webcam and return the path. + * + * @param {Core.Packet} packet - A `kdeconnect.photo.request` + * @return {Promise<string>} A file path + */ + _takePhoto(packet) { + return new Promise((resolve, reject) => { + const time = GLib.DateTime.new_now_local().format('%T'); + const path = GLib.build_filenamev([GLib.get_tmp_dir(), `${time}.jpg`]); + const proc = this._launcher.spawnv([ + Config.FFMPEG_PATH, + '-f', 'video4linux2', + '-ss', '0:0:2', + '-i', '/dev/video0', + '-frames', '1', + path, + ]); + + proc.wait_check_async(null, (proc, res) => { + try { + proc.wait_check_finish(res); + resolve(path); + } catch (e) { + reject(e); + } + }); + }); + } + + /** + * Send a photo to the remote device. + * + * @param {Core.Packet} packet - A `kdeconnect.photo.request` + */ + async _sendPhoto(packet) { + if (this.settings.get_boolean('share-camera')) + return; + + let file, transfer; + + try { + // Take a photo + const path = await this._takePhoto(); + + if (path.startsWith('file://')) + file = Gio.File.new_for_uri(path); + else + file = Gio.File.new_for_path(path); + + // Create the transfer + transfer = this.device.createTransfer(); + + transfer.addFile({ + type: 'kdeconnect.photo', + body: { + filename: file.get_basename(), + }, + }, file); + + await transfer.start(); + } catch (e) { + debug(e, this.device.name); + + if (transfer) { + this.device.showNotification({ + id: transfer.uuid, + title: _('Transfer Failed'), + // TRANSLATORS: eg. Failed to send "photo.jpg" to Google Pixel + body: _('Failed to send “%s” to %s').format( + file.get_basename(), + this.device.name + ), + icon: new Gio.ThemedIcon({name: 'dialog-warning-symbolic'}), + }); + } + } + } + + /** + * Request the remote device begin a photo operation. + */ + photo() { + this.device.sendPacket({ + type: 'kdeconnect.photo.request', + body: {}, + }); + } +}); diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/ping.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/ping.js new file mode 100644 index 0000000..f14cae7 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/ping.js @@ -0,0 +1,69 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('Ping'), + description: _('Send and receive pings'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Ping', + incomingCapabilities: ['kdeconnect.ping'], + outgoingCapabilities: ['kdeconnect.ping'], + actions: { + ping: { + label: _('Ping'), + icon_name: 'dialog-information-symbolic', + + parameter_type: new GLib.VariantType('s'), + incoming: [], + outgoing: ['kdeconnect.ping'], + }, + }, +}; + + +/** + * Ping Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/ping + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectPingPlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'ping'); + } + + handlePacket(packet) { + // Notification + const notif = { + title: this.device.name, + body: _('Ping'), + icon: new Gio.ThemedIcon({name: `${this.device.icon_name}`}), + }; + + if (packet.body.message) { + // TRANSLATORS: An optional message accompanying a ping, rarely if ever used + // eg. Ping: A message sent with ping + notif.body = _('Ping: %s').format(packet.body.message); + } + + this.device.showNotification(notif); + } + + ping(message = '') { + const packet = { + type: 'kdeconnect.ping', + body: {}, + }; + + if (message.length) + packet.body.message = message; + + this.device.sendPacket(packet); + } +}); diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/presenter.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/presenter.js new file mode 100644 index 0000000..fca5eaf --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/presenter.js @@ -0,0 +1,52 @@ +'use strict'; + +const GObject = imports.gi.GObject; + +const Components = imports.service.components; +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('Presentation'), + description: _('Use the paired device as a presenter'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Presenter', + incomingCapabilities: ['kdeconnect.presenter'], + outgoingCapabilities: [], + actions: {}, +}; + + +/** + * Presenter Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/presenter + * https://github.com/KDE/kdeconnect-android/tree/master/src/org/kde/kdeconnect/Plugins/PresenterPlugin/ + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectPresenterPlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'presenter'); + + this._input = Components.acquire('input'); + } + + handlePacket(packet) { + if (packet.body.hasOwnProperty('dx')) { + this._input.movePointer( + packet.body.dx * 1000, + packet.body.dy * 1000 + ); + } else if (packet.body.stop) { + // Currently unsupported and unnecessary as we just re-use the mouse + // pointer instead of showing an arbitrary window. + } + } + + destroy() { + if (this._input !== undefined) + this._input = Components.release('input'); + + super.destroy(); + } +}); diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/runcommand.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/runcommand.js new file mode 100644 index 0000000..ee6288b --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/runcommand.js @@ -0,0 +1,250 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('Run Commands'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.RunCommand', + description: _('Run commands on your paired device or let the device run predefined commands on this PC'), + incomingCapabilities: [ + 'kdeconnect.runcommand', + 'kdeconnect.runcommand.request', + ], + outgoingCapabilities: [ + 'kdeconnect.runcommand', + 'kdeconnect.runcommand.request', + ], + actions: { + commands: { + label: _('Commands'), + icon_name: 'system-run-symbolic', + + parameter_type: new GLib.VariantType('s'), + incoming: ['kdeconnect.runcommand'], + outgoing: ['kdeconnect.runcommand.request'], + }, + executeCommand: { + label: _('Commands'), + icon_name: 'system-run-symbolic', + + parameter_type: new GLib.VariantType('s'), + incoming: ['kdeconnect.runcommand'], + outgoing: ['kdeconnect.runcommand.request'], + }, + }, +}; + + +/** + * RunCommand Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/remotecommands + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/runcommand + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectRunCommandPlugin', + Properties: { + 'remote-commands': GObject.param_spec_variant( + 'remote-commands', + 'Remote Command List', + 'A list of the device\'s remote commands', + new GLib.VariantType('a{sv}'), + null, + GObject.ParamFlags.READABLE + ), + }, +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'runcommand'); + + // Local Commands + this._commandListChangedId = this.settings.connect( + 'changed::command-list', + this._sendCommandList.bind(this) + ); + + // We cache remote commands so they can be used in the settings even + // when the device is offline. + this._remote_commands = {}; + this.cacheProperties(['_remote_commands']); + } + + get remote_commands() { + return this._remote_commands; + } + + connected() { + super.connected(); + + this._sendCommandList(); + this._requestCommandList(); + this._handleCommandList(this.remote_commands); + } + + clearCache() { + this._remote_commands = {}; + this.notify('remote-commands'); + } + + cacheLoaded() { + if (!this.device.connected) + return; + + this._sendCommandList(); + this._requestCommandList(); + this._handleCommandList(this.remote_commands); + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.runcommand': + this._handleCommandList(packet.body.commandList); + break; + + case 'kdeconnect.runcommand.request': + if (packet.body.hasOwnProperty('key')) + this._handleCommand(packet.body.key); + + else if (packet.body.hasOwnProperty('requestCommandList')) + this._sendCommandList(); + + break; + } + } + + /** + * Handle a request to execute the local command with the UUID @key + * + * @param {string} key - The UUID of the local command + */ + _handleCommand(key) { + try { + const commands = this.settings.get_value('command-list'); + const commandList = commands.recursiveUnpack(); + + if (!commandList.hasOwnProperty(key)) { + throw new Gio.IOErrorEnum({ + code: Gio.IOErrorEnum.PERMISSION_DENIED, + message: `Unknown command: ${key}`, + }); + } + + this.device.launchProcess([ + '/bin/sh', + '-c', + commandList[key].command, + ]); + } catch (e) { + logError(e, this.device.name); + } + } + + /** + * Parse the response to a request for the remote command list. Remove the + * command menu if there are no commands, otherwise amend the menu. + * + * @param {string|Object[]} commandList - A list of remote commands + */ + _handleCommandList(commandList) { + // See: https://github.com/GSConnect/gnome-shell-extension-gsconnect/issues/1051 + if (typeof commandList === 'string') { + try { + commandList = JSON.parse(commandList); + } catch (e) { + commandList = {}; + } + } + + this._remote_commands = commandList; + this.notify('remote-commands'); + + const commandEntries = Object.entries(this.remote_commands); + + // If there are no commands, hide the menu by disabling the action + this.device.lookup_action('commands').enabled = (commandEntries.length > 0); + + // Commands Submenu + const submenu = new Gio.Menu(); + + for (const [uuid, info] of commandEntries) { + const item = new Gio.MenuItem(); + item.set_label(info.name); + item.set_icon( + new Gio.ThemedIcon({name: 'application-x-executable-symbolic'}) + ); + item.set_detailed_action(`device.executeCommand::${uuid}`); + submenu.append_item(item); + } + + // Commands Item + const item = new Gio.MenuItem(); + item.set_detailed_action('device.commands::menu'); + item.set_attribute_value( + 'hidden-when', + new GLib.Variant('s', 'action-disabled') + ); + item.set_icon(new Gio.ThemedIcon({name: 'system-run-symbolic'})); + item.set_label(_('Commands')); + item.set_submenu(submenu); + + // If the submenu item is already present it will be replaced + const menuActions = this.device.settings.get_strv('menu-actions'); + const index = menuActions.indexOf('commands'); + + if (index > -1) { + this.device.removeMenuAction('commands'); + this.device.addMenuItem(item, index); + } + } + + /** + * Send a request for the remote command list + */ + _requestCommandList() { + this.device.sendPacket({ + type: 'kdeconnect.runcommand.request', + body: {requestCommandList: true}, + }); + } + + /** + * Send the local command list + */ + _sendCommandList() { + const commands = this.settings.get_value('command-list').recursiveUnpack(); + const commandList = JSON.stringify(commands); + + this.device.sendPacket({ + type: 'kdeconnect.runcommand', + body: {commandList: commandList}, + }); + } + + /** + * Placeholder function for command action + */ + commands() {} + + /** + * Send a request to execute the remote command with the UUID @key + * + * @param {string} key - The UUID of the remote command + */ + executeCommand(key) { + this.device.sendPacket({ + type: 'kdeconnect.runcommand.request', + body: {key: key}, + }); + } + + destroy() { + this.settings.disconnect(this._commandListChangedId); + + super.destroy(); + } +}); diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/sftp.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/sftp.js new file mode 100644 index 0000000..e5ab3fe --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/sftp.js @@ -0,0 +1,565 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Config = imports.config; +const Lan = imports.service.backends.lan; +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('SFTP'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.SFTP', + description: _('Browse the paired device filesystem'), + incomingCapabilities: ['kdeconnect.sftp'], + outgoingCapabilities: ['kdeconnect.sftp.request'], + actions: { + mount: { + label: _('Mount'), + icon_name: 'folder-remote-symbolic', + + parameter_type: null, + incoming: ['kdeconnect.sftp'], + outgoing: ['kdeconnect.sftp.request'], + }, + unmount: { + label: _('Unmount'), + icon_name: 'media-eject-symbolic', + + parameter_type: null, + incoming: ['kdeconnect.sftp'], + outgoing: ['kdeconnect.sftp.request'], + }, + }, +}; + + +const MAX_MOUNT_DIRS = 12; + + +/** + * SFTP Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/sftp + * https://github.com/KDE/kdeconnect-android/tree/master/src/org/kde/kdeconnect/Plugins/SftpPlugin + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectSFTPPlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'sftp'); + + this._gmount = null; + this._mounting = false; + + // A reusable launcher for ssh processes + this._launcher = new Gio.SubprocessLauncher({ + flags: (Gio.SubprocessFlags.STDOUT_PIPE | + Gio.SubprocessFlags.STDERR_MERGE), + }); + + // Watch the volume monitor + this._volumeMonitor = Gio.VolumeMonitor.get(); + + this._mountAddedId = this._volumeMonitor.connect( + 'mount-added', + this._onMountAdded.bind(this) + ); + + this._mountRemovedId = this._volumeMonitor.connect( + 'mount-removed', + this._onMountRemoved.bind(this) + ); + } + + get gmount() { + if (this._gmount === null && this.device.connected) { + const host = this.device.channel.host; + + const regex = new RegExp( + `sftp://(${host}):(1739|17[4-5][0-9]|176[0-4])` + ); + + for (const mount of this._volumeMonitor.get_mounts()) { + const uri = mount.get_root().get_uri(); + + if (regex.test(uri)) { + this._gmount = mount; + this._addSubmenu(mount); + this._addSymlink(mount); + + break; + } + } + } + + return this._gmount; + } + + connected() { + super.connected(); + + // Only enable for Lan connections + if (this.device.channel instanceof Lan.Channel) { + if (this.settings.get_boolean('automount')) + this.mount(); + } else { + this.device.lookup_action('mount').enabled = false; + this.device.lookup_action('unmount').enabled = false; + } + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.sftp': + if (packet.body.hasOwnProperty('errorMessage')) + this._handleError(packet); + else + this._handleMount(packet); + + break; + } + } + + _onMountAdded(monitor, mount) { + if (this._gmount !== null || !this.device.connected) + return; + + const host = this.device.channel.host; + const regex = new RegExp(`sftp://(${host}):(1739|17[4-5][0-9]|176[0-4])`); + const uri = mount.get_root().get_uri(); + + if (!regex.test(uri)) + return; + + this._gmount = mount; + this._addSubmenu(mount); + this._addSymlink(mount); + } + + _onMountRemoved(monitor, mount) { + if (this.gmount !== mount) + return; + + this._gmount = null; + this._removeSubmenu(); + } + + async _listDirectories(mount) { + const file = mount.get_root(); + + const iter = await new Promise((resolve, reject) => { + file.enumerate_children_async( + Gio.FILE_ATTRIBUTE_STANDARD_NAME, + Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, + GLib.PRIORITY_DEFAULT, + this.cancellable, + (file, res) => { + try { + resolve(file.enumerate_children_finish(res)); + } catch (e) { + reject(e); + } + } + ); + }); + + const infos = await new Promise((resolve, reject) => { + iter.next_files_async( + MAX_MOUNT_DIRS, + GLib.PRIORITY_DEFAULT, + this.cancellable, + (iter, res) => { + try { + resolve(iter.next_files_finish(res)); + } catch (e) { + reject(e); + } + } + ); + }); + + iter.close_async(GLib.PRIORITY_DEFAULT, null, null); + + const directories = {}; + + for (const info of infos) { + const name = info.get_name(); + directories[name] = `${file.get_uri()}${name}/`; + } + + return directories; + } + + _onAskQuestion(op, message, choices) { + op.reply(Gio.MountOperationResult.HANDLED); + } + + _onAskPassword(op, message, user, domain, flags) { + op.reply(Gio.MountOperationResult.HANDLED); + } + + /** + * Handle an error reported by the remote device. + * + * @param {Core.Packet} packet - a `kdeconnect.sftp` + */ + _handleError(packet) { + this.device.showNotification({ + id: 'sftp-error', + title: _('%s reported an error').format(this.device.name), + body: packet.body.errorMessage, + icon: new Gio.ThemedIcon({name: 'dialog-error-symbolic'}), + priority: Gio.NotificationPriority.HIGH, + }); + } + + /** + * Mount the remote device using the provided information. + * + * @param {Core.Packet} packet - a `kdeconnect.sftp` + */ + async _handleMount(packet) { + try { + // Already mounted or mounting + if (this.gmount !== null || this._mounting) + return; + + this._mounting = true; + + // Ensure the private key is in the keyring + await this._addPrivateKey(); + + // Create a new mount operation + const op = new Gio.MountOperation({ + username: packet.body.user || null, + password: packet.body.password || null, + password_save: Gio.PasswordSave.NEVER, + }); + + op.connect('ask-question', this._onAskQuestion); + op.connect('ask-password', this._onAskPassword); + + // This is the actual call to mount the device + const host = this.device.channel.host; + const uri = `sftp://${host}:${packet.body.port}/`; + const file = Gio.File.new_for_uri(uri); + + await new Promise((resolve, reject) => { + file.mount_enclosing_volume(0, op, null, (file, res) => { + try { + resolve(file.mount_enclosing_volume_finish(res)); + } catch (e) { + // Special case when the GMount didn't unmount properly + // but is still on the same port and can be reused. + if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.ALREADY_MOUNTED)) { + resolve(true); + + // There's a good chance this is a host key verification + // error; regardless we'll remove the key for security. + } else { + this._removeHostKey(host); + reject(e); + } + } + }); + }); + } catch (e) { + logError(e, this.device.name); + } finally { + this._mounting = false; + } + } + + /** + * Add GSConnect's private key identity to the authentication agent so our + * identity can be verified by Android during private key authentication. + * + * @return {Promise} A promise for the operation + */ + _addPrivateKey() { + const ssh_add = this._launcher.spawnv([ + Config.SSHADD_PATH, + GLib.build_filenamev([Config.CONFIGDIR, 'private.pem']), + ]); + + return new Promise((resolve, reject) => { + ssh_add.communicate_utf8_async(null, null, (proc, res) => { + try { + const result = proc.communicate_utf8_finish(res)[1].trim(); + + if (proc.get_exit_status() !== 0) + debug(result, this.device.name); + + resolve(); + } catch (e) { + reject(e); + } + }); + }); + } + + /** + * Remove all host keys from ~/.ssh/known_hosts for @host in the port range + * used by KDE Connect (1739-1764). + * + * @param {string} host - A hostname or IP address + */ + async _removeHostKey(host) { + for (let port = 1739; port <= 1764; port++) { + try { + const ssh_keygen = this._launcher.spawnv([ + Config.SSHKEYGEN_PATH, + '-R', + `[${host}]:${port}`, + ]); + + await new Promise((resolve, reject) => { + ssh_keygen.communicate_utf8_async(null, null, (proc, res) => { + try { + const stdout = proc.communicate_utf8_finish(res)[1]; + const status = proc.get_exit_status(); + + if (status !== 0) { + throw new Gio.IOErrorEnum({ + code: Gio.io_error_from_errno(status), + message: `${GLib.strerror(status)}\n${stdout}`.trim(), + }); + } + + resolve(); + } catch (e) { + reject(e); + } + }); + }); + } catch (e) { + logError(e, this.device.name); + } + } + } + + /* + * Mount menu helpers + */ + _getUnmountSection() { + if (this._unmountSection === undefined) { + this._unmountSection = new Gio.Menu(); + + const unmountItem = new Gio.MenuItem(); + unmountItem.set_label(Metadata.actions.unmount.label); + unmountItem.set_icon(new Gio.ThemedIcon({ + name: Metadata.actions.unmount.icon_name, + })); + unmountItem.set_detailed_action('device.unmount'); + this._unmountSection.append_item(unmountItem); + } + + return this._unmountSection; + } + + _getFilesMenuItem() { + if (this._filesMenuItem === undefined) { + // Files menu icon + const emblem = new Gio.Emblem({ + icon: new Gio.ThemedIcon({name: 'emblem-default'}), + }); + + const mountedIcon = new Gio.EmblemedIcon({ + gicon: new Gio.ThemedIcon({name: 'folder-remote-symbolic'}), + }); + mountedIcon.add_emblem(emblem); + + // Files menu item + this._filesMenuItem = new Gio.MenuItem(); + this._filesMenuItem.set_detailed_action('device.mount'); + this._filesMenuItem.set_icon(mountedIcon); + this._filesMenuItem.set_label(_('Files')); + } + + return this._filesMenuItem; + } + + async _addSubmenu(mount) { + try { + const directories = await this._listDirectories(mount); + + // Submenu sections + const dirSection = new Gio.Menu(); + const unmountSection = this._getUnmountSection(); + + for (const [name, uri] of Object.entries(directories)) + dirSection.append(name, `device.openPath::${uri}`); + + // Files submenu + const filesSubmenu = new Gio.Menu(); + filesSubmenu.append_section(null, dirSection); + filesSubmenu.append_section(null, unmountSection); + + // Files menu item + const filesMenuItem = this._getFilesMenuItem(); + filesMenuItem.set_submenu(filesSubmenu); + + // Replace the existing menu item + const index = this.device.removeMenuAction('device.mount'); + this.device.addMenuItem(filesMenuItem, index); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + debug(e, this.device.name); + + // Reset to allow retrying + this._gmount = null; + } + } + + _removeSubmenu() { + try { + const index = this.device.removeMenuAction('device.mount'); + const action = this.device.lookup_action('mount'); + + if (action !== null) { + this.device.addMenuAction( + action, + index, + Metadata.actions.mount.label, + Metadata.actions.mount.icon_name + ); + } + } catch (e) { + logError(e, this.device.name); + } + } + + /** + * Create a symbolic link referring to the device by name + * + * @param {Gio.Mount} mount - A GMount to link to + */ + async _addSymlink(mount) { + try { + const by_name_dir = Gio.File.new_for_path( + `${Config.RUNTIMEDIR}/by-name/` + ); + + try { + by_name_dir.make_directory_with_parents(null); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS)) + throw e; + } + + // Replace path separator with a Unicode lookalike: + let safe_device_name = this.device.name.replace('/', '∕'); + + if (safe_device_name === '.') + safe_device_name = '·'; + else if (safe_device_name === '..') + safe_device_name = '··'; + + const link_target = mount.get_root().get_path(); + const link = Gio.File.new_for_path( + `${by_name_dir.get_path()}/${safe_device_name}` + ); + + // Check for and remove any existing stale link + try { + const link_stat = await new Promise((resolve, reject) => { + link.query_info_async( + 'standard::symlink-target', + Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, + GLib.PRIORITY_DEFAULT, + null, + (link, res) => { + try { + resolve(link.query_info_finish(res)); + } catch (e) { + reject(e); + } + } + ); + }); + + if (link_stat.get_symlink_target() === link_target) + return; + + await new Promise((resolve, reject) => { + link.delete_async( + GLib.PRIORITY_DEFAULT, + null, + (link, res) => { + try { + resolve(link.delete_finish(res)); + } catch (e) { + reject(e); + } + } + ); + }); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) + throw e; + } + + link.make_symbolic_link(link_target, null); + } catch (e) { + debug(e, this.device.name); + } + } + + /** + * Send a request to mount the remote device + */ + mount() { + if (this.gmount !== null) + return; + + this.device.sendPacket({ + type: 'kdeconnect.sftp.request', + body: { + startBrowsing: true, + }, + }); + } + + /** + * Remove the menu items, unmount the filesystem, replace the mount item + */ + async unmount() { + try { + if (this.gmount === null) + return; + + this._removeSubmenu(); + this._mounting = false; + + await new Promise((resolve, reject) => { + this.gmount.unmount_with_operation( + Gio.MountUnmountFlags.FORCE, + new Gio.MountOperation(), + null, + (mount, res) => { + try { + resolve(mount.unmount_with_operation_finish(res)); + } catch (e) { + reject(e); + } + } + ); + }); + } catch (e) { + debug(e, this.device.name); + } + } + + destroy() { + if (this._volumeMonitor) { + this._volumeMonitor.disconnect(this._mountAddedId); + this._volumeMonitor.disconnect(this._mountRemovedId); + this._volumeMonitor = null; + } + + super.destroy(); + } +}); diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/share.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/share.js new file mode 100644 index 0000000..30b53ef --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/share.js @@ -0,0 +1,483 @@ +'use strict'; + +const GdkPixbuf = imports.gi.GdkPixbuf; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + +const PluginBase = imports.service.plugin; +const URI = imports.service.utils.uri; + + +var Metadata = { + label: _('Share'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Share', + description: _('Share files and URLs between devices'), + incomingCapabilities: ['kdeconnect.share.request'], + outgoingCapabilities: ['kdeconnect.share.request'], + actions: { + share: { + label: _('Share'), + icon_name: 'send-to-symbolic', + + parameter_type: null, + incoming: [], + outgoing: ['kdeconnect.share.request'], + }, + shareFile: { + label: _('Share File'), + icon_name: 'document-send-symbolic', + + parameter_type: new GLib.VariantType('(sb)'), + incoming: [], + outgoing: ['kdeconnect.share.request'], + }, + shareText: { + label: _('Share Text'), + icon_name: 'send-to-symbolic', + + parameter_type: new GLib.VariantType('s'), + incoming: [], + outgoing: ['kdeconnect.share.request'], + }, + shareUri: { + label: _('Share Link'), + icon_name: 'send-to-symbolic', + + parameter_type: new GLib.VariantType('s'), + incoming: [], + outgoing: ['kdeconnect.share.request'], + }, + }, +}; + + +/** + * Share Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/share + * + * TODO: receiving 'text' TODO: Window with textview & 'Copy to Clipboard.. + * https://github.com/KDE/kdeconnect-kde/commit/28f11bd5c9a717fb9fbb3f02ddd6cea62021d055 + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectSharePlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'share'); + } + + handlePacket(packet) { + // TODO: composite jobs (lastModified, numberOfFiles, totalPayloadSize) + if (packet.body.hasOwnProperty('filename')) { + if (this.settings.get_boolean('receive-files')) + this._handleFile(packet); + else + this._refuseFile(packet); + } else if (packet.body.hasOwnProperty('text')) { + this._handleText(packet); + } else if (packet.body.hasOwnProperty('url')) { + this._handleUri(packet); + } + } + + _ensureReceiveDirectory() { + let receiveDir = this.settings.get_string('receive-directory'); + + // Ensure a directory is set + if (receiveDir.length === 0) { + receiveDir = GLib.get_user_special_dir( + GLib.UserDirectory.DIRECTORY_DOWNLOAD + ); + + // Fallback to ~/Downloads + const homeDir = GLib.get_home_dir(); + + if (!receiveDir || receiveDir === homeDir) + receiveDir = GLib.build_filenamev([homeDir, 'Downloads']); + + this.settings.set_string('receive-directory', receiveDir); + } + + // Ensure the directory exists + if (!GLib.file_test(receiveDir, GLib.FileTest.IS_DIR)) + GLib.mkdir_with_parents(receiveDir, 448); + + return receiveDir; + } + + _getFile(filename) { + const dirpath = this._ensureReceiveDirectory(); + const basepath = GLib.build_filenamev([dirpath, filename]); + let filepath = basepath; + let copyNum = 0; + + while (GLib.file_test(filepath, GLib.FileTest.EXISTS)) + filepath = `${basepath} (${++copyNum})`; + + return Gio.File.new_for_path(filepath); + } + + _refuseFile(packet) { + try { + this.device.rejectTransfer(packet); + + this.device.showNotification({ + id: `${Date.now()}`, + title: _('Transfer Failed'), + // TRANSLATORS: eg. Google Pixel is not allowed to upload files + body: _('%s is not allowed to upload files').format( + this.device.name + ), + icon: new Gio.ThemedIcon({name: 'dialog-error-symbolic'}), + }); + } catch (e) { + debug(e, this.device.name); + } + } + + async _handleFile(packet) { + try { + const file = this._getFile(packet.body.filename); + + // Create the transfer + const transfer = this.device.createTransfer(); + + transfer.addFile(packet, file); + + // Notify that we're about to start the transfer + this.device.showNotification({ + id: transfer.uuid, + title: _('Transferring File'), + // TRANSLATORS: eg. Receiving 'book.pdf' from Google Pixel + body: _('Receiving “%s” from %s').format( + packet.body.filename, + this.device.name + ), + buttons: [{ + label: _('Cancel'), + action: 'cancelTransfer', + parameter: new GLib.Variant('s', transfer.uuid), + }], + icon: new Gio.ThemedIcon({name: 'document-save-symbolic'}), + }); + + // We'll show a notification (success or failure) + let title, body, iconName; + let buttons = []; + + try { + await transfer.start(); + + title = _('Transfer Successful'); + // TRANSLATORS: eg. Received 'book.pdf' from Google Pixel + body = _('Received “%s” from %s').format( + packet.body.filename, + this.device.name + ); + buttons = [ + { + label: _('Open Folder'), + action: 'openPath', + parameter: new GLib.Variant('s', file.get_parent().get_uri()), + }, + { + label: _('Open File'), + action: 'openPath', + parameter: new GLib.Variant('s', file.get_uri()), + }, + ]; + iconName = 'document-save-symbolic'; + + if (packet.body.open) { + const uri = file.get_uri(); + Gio.AppInfo.launch_default_for_uri_async(uri, null, null, null); + } + } catch (e) { + debug(e, this.device.name); + + title = _('Transfer Failed'); + // TRANSLATORS: eg. Failed to receive 'book.pdf' from Google Pixel + body = _('Failed to receive “%s” from %s').format( + packet.body.filename, + this.device.name + ); + iconName = 'dialog-warning-symbolic'; + + // Clean up the downloaded file on failure + file.delete_async(GLib.PRIORITY_DEAFAULT, null, null); + } + + this.device.hideNotification(transfer.uuid); + this.device.showNotification({ + id: transfer.uuid, + title: title, + body: body, + buttons: buttons, + icon: new Gio.ThemedIcon({name: iconName}), + }); + } catch (e) { + logError(e, this.device.name); + } + } + + _handleUri(packet) { + const uri = packet.body.url; + Gio.AppInfo.launch_default_for_uri_async(uri, null, null, null); + } + + _handleText(packet) { + const dialog = new Gtk.MessageDialog({ + text: _('Text Shared By %s').format(this.device.name), + secondary_text: URI.linkify(packet.body.text), + secondary_use_markup: true, + buttons: Gtk.ButtonsType.CLOSE, + }); + dialog.message_area.get_children()[1].selectable = true; + dialog.set_keep_above(true); + dialog.connect('response', (dialog) => dialog.destroy()); + dialog.show(); + } + + /** + * Open the file chooser dialog for selecting a file or inputing a URI. + */ + share() { + const dialog = new FileChooserDialog(this.device); + dialog.show(); + } + + /** + * Share local file path or URI + * + * @param {string} path - Local file path or URI + * @param {boolean} open - Whether the file should be opened after transfer + */ + async shareFile(path, open = false) { + try { + let file = null; + + if (path.includes('://')) + file = Gio.File.new_for_uri(path); + else + file = Gio.File.new_for_path(path); + + // Create the transfer + const transfer = this.device.createTransfer(); + + transfer.addFile({ + type: 'kdeconnect.share.request', + body: { + filename: file.get_basename(), + open: open, + }, + }, file); + + // Notify that we're about to start the transfer + this.device.showNotification({ + id: transfer.uuid, + title: _('Transferring File'), + // TRANSLATORS: eg. Sending 'book.pdf' to Google Pixel + body: _('Sending “%s” to %s').format( + file.get_basename(), + this.device.name + ), + buttons: [{ + label: _('Cancel'), + action: 'cancelTransfer', + parameter: new GLib.Variant('s', transfer.uuid), + }], + icon: new Gio.ThemedIcon({name: 'document-send-symbolic'}), + }); + + // We'll show a notification (success or failure) + let title, body, iconName; + + try { + await transfer.start(); + + title = _('Transfer Successful'); + // TRANSLATORS: eg. Sent "book.pdf" to Google Pixel + body = _('Sent “%s” to %s').format( + file.get_basename(), + this.device.name + ); + iconName = 'document-send-symbolic'; + } catch (e) { + debug(e, this.device.name); + + title = _('Transfer Failed'); + // TRANSLATORS: eg. Failed to send "book.pdf" to Google Pixel + body = _('Failed to send “%s” to %s').format( + file.get_basename(), + this.device.name + ); + iconName = 'dialog-warning-symbolic'; + } + + this.device.hideNotification(transfer.uuid); + this.device.showNotification({ + id: transfer.uuid, + title: title, + body: body, + icon: new Gio.ThemedIcon({name: iconName}), + }); + } catch (e) { + debug(e, this.device.name); + } + } + + /** + * Share a string of text. Remote behaviour is undefined. + * + * @param {string} text - A string of unicode text + */ + shareText(text) { + this.device.sendPacket({ + type: 'kdeconnect.share.request', + body: {text: text}, + }); + } + + /** + * Share a URI. Generally the remote device opens it with the scheme default + * + * @param {string} uri - A URI to share + */ + shareUri(uri) { + if (GLib.uri_parse_scheme(uri) === 'file') { + this.shareFile(uri); + return; + } + + this.device.sendPacket({ + type: 'kdeconnect.share.request', + body: {url: uri}, + }); + } +}); + + +/** A simple FileChooserDialog for sharing files */ +var FileChooserDialog = GObject.registerClass({ + GTypeName: 'GSConnectShareFileChooserDialog', +}, class FileChooserDialog extends Gtk.FileChooserDialog { + + _init(device) { + super._init({ + // TRANSLATORS: eg. Send files to Google Pixel + title: _('Send files to %s').format(device.name), + select_multiple: true, + extra_widget: new Gtk.CheckButton({ + // TRANSLATORS: Mark the file to be opened once completed + label: _('Open when done'), + visible: true, + }), + use_preview_label: false, + }); + + this.device = device; + + // Align checkbox with sidebar + const box = this.get_content_area().get_children()[0].get_children()[0]; + const paned = box.get_children()[0]; + paned.bind_property( + 'position', + this.extra_widget, + 'margin-left', + GObject.BindingFlags.SYNC_CREATE + ); + + // Preview Widget + this.preview_widget = new Gtk.Image(); + this.preview_widget_active = false; + this.connect('update-preview', this._onUpdatePreview); + + // URI entry + this._uriEntry = new Gtk.Entry({ + placeholder_text: 'https://', + hexpand: true, + visible: true, + }); + this._uriEntry.connect('activate', this._sendLink.bind(this)); + + // URI/File toggle + this._uriButton = new Gtk.ToggleButton({ + image: new Gtk.Image({ + icon_name: 'web-browser-symbolic', + pixel_size: 16, + }), + valign: Gtk.Align.CENTER, + // TRANSLATORS: eg. Send a link to Google Pixel + tooltip_text: _('Send a link to %s').format(device.name), + visible: true, + }); + this._uriButton.connect('toggled', this._onUriButtonToggled.bind(this)); + + this.add_button(_('Cancel'), Gtk.ResponseType.CANCEL); + const sendButton = this.add_button(_('Send'), Gtk.ResponseType.OK); + sendButton.connect('clicked', this._sendLink.bind(this)); + + this.get_header_bar().pack_end(this._uriButton); + this.set_default_response(Gtk.ResponseType.OK); + } + + _onUpdatePreview(chooser) { + try { + const pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size( + chooser.get_preview_filename(), + chooser.get_scale_factor() * 128, + -1 + ); + chooser.preview_widget.pixbuf = pixbuf; + chooser.preview_widget.visible = true; + chooser.preview_widget_active = true; + } catch (e) { + chooser.preview_widget.visible = false; + chooser.preview_widget_active = false; + } + } + + _onUriButtonToggled(button) { + const header = this.get_header_bar(); + + // Show the URL entry + if (button.active) { + this.extra_widget.sensitive = false; + header.set_custom_title(this._uriEntry); + this.set_response_sensitive(Gtk.ResponseType.OK, true); + + // Hide the URL entry + } else { + header.set_custom_title(null); + this.set_response_sensitive( + Gtk.ResponseType.OK, + this.get_uris().length > 1 + ); + this.extra_widget.sensitive = true; + } + } + + _sendLink(widget) { + if (this._uriButton.active && this._uriEntry.text.length) + this.response(1); + } + + vfunc_response(response_id) { + if (response_id === Gtk.ResponseType.OK) { + for (const uri of this.get_uris()) { + const parameter = new GLib.Variant( + '(sb)', + [uri, this.extra_widget.active] + ); + this.device.activate_action('shareFile', parameter); + } + } else if (response_id === 1) { + const parameter = new GLib.Variant('s', this._uriEntry.text); + this.device.activate_action('shareUri', parameter); + } + + this.destroy(); + } +}); diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/sms.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/sms.js new file mode 100644 index 0000000..8273f7e --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/sms.js @@ -0,0 +1,527 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const PluginBase = imports.service.plugin; +const LegacyMessaging = imports.service.ui.legacyMessaging; +const Messaging = imports.service.ui.messaging; +const URI = imports.service.utils.uri; + + +var Metadata = { + label: _('SMS'), + description: _('Send and read SMS of the paired device and be notified of new SMS'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.SMS', + incomingCapabilities: [ + 'kdeconnect.sms.messages', + ], + outgoingCapabilities: [ + 'kdeconnect.sms.request', + 'kdeconnect.sms.request_conversation', + 'kdeconnect.sms.request_conversations', + ], + actions: { + // SMS Actions + sms: { + label: _('Messaging'), + icon_name: 'sms-symbolic', + + parameter_type: null, + incoming: [], + outgoing: ['kdeconnect.sms.request'], + }, + uriSms: { + label: _('New SMS (URI)'), + icon_name: 'sms-symbolic', + + parameter_type: new GLib.VariantType('s'), + incoming: [], + outgoing: ['kdeconnect.sms.request'], + }, + replySms: { + label: _('Reply SMS'), + icon_name: 'sms-symbolic', + + parameter_type: new GLib.VariantType('s'), + incoming: [], + outgoing: ['kdeconnect.sms.request'], + }, + sendMessage: { + label: _('Send Message'), + icon_name: 'sms-send', + + parameter_type: new GLib.VariantType('(aa{sv})'), + incoming: [], + outgoing: ['kdeconnect.sms.request'], + }, + sendSms: { + label: _('Send SMS'), + icon_name: 'sms-send', + + parameter_type: new GLib.VariantType('(ss)'), + incoming: [], + outgoing: ['kdeconnect.sms.request'], + }, + shareSms: { + label: _('Share SMS'), + icon_name: 'sms-send', + + parameter_type: new GLib.VariantType('s'), + incoming: [], + outgoing: ['kdeconnect.sms.request'], + }, + }, +}; + + +/** + * SMS Message event type. Currently all events are TEXT_MESSAGE. + * + * TEXT_MESSAGE: Has a "body" field which contains pure, human-readable text + */ +var MessageEvent = { + TEXT_MESSAGE: 0x1, +}; + + +/** + * SMS Message status. READ/UNREAD match the 'read' field from the Android App + * message packet. + * + * UNREAD: A message not marked as read + * READ: A message marked as read + */ +var MessageStatus = { + UNREAD: 0, + READ: 1, +}; + + +/** + * SMS Message direction. IN/OUT match the 'type' field from the Android App + * message packet. + * + * See: https://developer.android.com/reference/android/provider/Telephony.TextBasedSmsColumns.html + * + * IN: An incoming message + * OUT: An outgoing message + */ +var MessageBox = { + ALL: 0, + INBOX: 1, + SENT: 2, + DRAFT: 3, + OUTBOX: 4, + FAILED: 5, + QUEUED: 6, +}; + + +/** + * SMS Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/sms + * https://github.com/KDE/kdeconnect-android/tree/master/src/org/kde/kdeconnect/Plugins/SMSPlugin/ + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectSMSPlugin', + Properties: { + 'threads': GObject.param_spec_variant( + 'threads', + 'Conversation List', + 'A list of threads', + new GLib.VariantType('aa{sv}'), + null, + GObject.ParamFlags.READABLE + ), + }, +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'sms'); + + this.cacheProperties(['_threads']); + } + + get threads() { + if (this._threads === undefined) + this._threads = {}; + + return this._threads; + } + + get window() { + if (this.settings.get_boolean('legacy-sms')) { + return new LegacyMessaging.Dialog({ + device: this.device, + plugin: this, + }); + } + + if (this._window === undefined) { + this._window = new Messaging.Window({ + application: Gio.Application.get_default(), + device: this.device, + plugin: this, + }); + + this._window.connect('destroy', () => { + this._window = undefined; + }); + } + + return this._window; + } + + clearCache() { + this._threads = {}; + this.notify('threads'); + } + + cacheLoaded() { + this.notify('threads'); + } + + connected() { + super.connected(); + this._requestConversations(); + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.sms.messages': + this._handleMessages(packet.body.messages); + break; + } + } + + /** + * Handle a digest of threads. + * + * @param {Object[]} messages - A list of message objects + * @param {string[]} thread_ids - A list of thread IDs as strings + */ + _handleDigest(messages, thread_ids) { + // Prune threads + for (const thread_id of Object.keys(this.threads)) { + if (!thread_ids.includes(thread_id)) + delete this.threads[thread_id]; + } + + // Request each new or newer thread + for (let i = 0, len = messages.length; i < len; i++) { + const message = messages[i]; + const cache = this.threads[message.thread_id]; + + if (cache === undefined) { + this._requestConversation(message.thread_id); + continue; + } + + // If this message is marked read, mark the rest as read + if (message.read === MessageStatus.READ) { + for (const msg of cache) + msg.read = MessageStatus.READ; + } + + // If we don't have a thread for this message or it's newer + // than the last message in the cache, request the thread + if (!cache.length || cache[cache.length - 1].date < message.date) + this._requestConversation(message.thread_id); + } + + this.notify('threads'); + } + + /** + * Handle a new single message + * + * @param {Object} message - A message object + */ + _handleMessage(message) { + let conversation = null; + + // If the window is open, try and find an active conversation + if (this._window) + conversation = this._window.getConversationForMessage(message); + + // If there's an active conversation, we should log the message now + if (conversation) + conversation.logNext(message); + } + + /** + * Parse a conversation (thread of messages) and sort them + * + * @param {Object[]} thread - A list of sms message objects from a thread + */ + _handleThread(thread) { + // If there are no addresses this will cause major problems... + if (!thread[0].addresses || !thread[0].addresses[0]) + return; + + const thread_id = thread[0].thread_id; + const cache = this.threads[thread_id] || []; + + // Handle each message + for (let i = 0, len = thread.length; i < len; i++) { + const message = thread[i]; + + // TODO: We only cache messages of a known MessageBox since we + // have no reliable way to determine its direction, let alone + // what to do with it. + if (message.type < 0 || message.type > 6) + continue; + + // If the message exists, just update it + const cacheMessage = cache.find(m => m.date === message.date); + + if (cacheMessage) { + Object.assign(cacheMessage, message); + } else { + cache.push(message); + this._handleMessage(message); + } + } + + // Sort the thread by ascending date and notify + this.threads[thread_id] = cache.sort((a, b) => a.date - b.date); + this.notify('threads'); + } + + /** + * Handle a response to telephony.request_conversation(s) + * + * @param {Object[]} messages - A list of sms message objects + */ + _handleMessages(messages) { + try { + // If messages is empty there's nothing to do... + if (messages.length === 0) + return; + + const thread_ids = []; + + // Perform some modification of the messages + for (let i = 0, len = messages.length; i < len; i++) { + const message = messages[i]; + + // COERCION: thread_id's to strings + message.thread_id = `${message.thread_id}`; + thread_ids.push(message.thread_id); + + // TODO: Remove bogus `insert-address-token` entries + let a = message.addresses.length; + + while (a--) { + if (message.addresses[a].address === undefined || + message.addresses[a].address === 'insert-address-token') + message.addresses.splice(a, 1); + } + } + + // If there's multiple thread_id's it's a summary of threads + if (thread_ids.some(id => id !== thread_ids[0])) + this._handleDigest(messages, thread_ids); + + // Otherwise this is single thread or new message + else + this._handleThread(messages); + } catch (e) { + debug(e, this.device.name); + } + } + + /** + * Request a list of messages from a single thread. + * + * @param {number} thread_id - The id of the thread to request + */ + _requestConversation(thread_id) { + this.device.sendPacket({ + type: 'kdeconnect.sms.request_conversation', + body: { + threadID: thread_id, + }, + }); + } + + /** + * Request a list of the last message in each unarchived thread. + */ + _requestConversations() { + this.device.sendPacket({ + type: 'kdeconnect.sms.request_conversations', + }); + } + + /** + * A notification action for replying to SMS messages (or missed calls). + * + * @param {string} hint - Could be either a contact name or phone number + */ + replySms(hint) { + this.window.present(); + // FIXME: causes problems now that non-numeric addresses are allowed + // this.window.address = hint.toPhoneNumber(); + } + + /** + * Send an SMS message + * + * @param {string} phoneNumber - The phone number to send the message to + * @param {string} messageBody - The message to send + */ + sendSms(phoneNumber, messageBody) { + this.device.sendPacket({ + type: 'kdeconnect.sms.request', + body: { + sendSms: true, + phoneNumber: phoneNumber, + messageBody: messageBody, + }, + }); + } + + /** + * Send a message + * + * @param {Object[]} addresses - A list of address objects + * @param {string} messageBody - The message text + * @param {number} [event] - An event bitmask + * @param {boolean} [forceSms] - Whether to force SMS + * @param {number} [subId] - The SIM card to use + */ + sendMessage(addresses, messageBody, event = 1, forceSms = false, subId = undefined) { + // TODO: waiting on support in kdeconnect-android + // if (this._version === 1) { + this.device.sendPacket({ + type: 'kdeconnect.sms.request', + body: { + sendSms: true, + phoneNumber: addresses[0].address, + messageBody: messageBody, + }, + }); + // } else if (this._version === 2) { + // this.device.sendPacket({ + // type: 'kdeconnect.sms.request', + // body: { + // version: 2, + // addresses: addresses, + // messageBody: messageBody, + // forceSms: forceSms, + // sub_id: subId + // } + // }); + // } + } + + /** + * Share a text content by SMS message. This is used by the WebExtension to + * share URLs from the browser, but could be used to initiate sharing of any + * text content. + * + * @param {string} url - The link to be shared + */ + shareSms(url) { + // Legacy Mode + if (this.settings.get_boolean('legacy-sms')) { + const window = this.window; + window.present(); + window.setMessage(url); + + // If there are active threads, show the chooser dialog + } else if (Object.values(this.threads).length > 0) { + const window = new Messaging.ConversationChooser({ + application: Gio.Application.get_default(), + device: this.device, + message: url, + plugin: this, + }); + + window.present(); + + // Otherwise show the window and wait for a contact to be chosen + } else { + this.window.present(); + this.window.setMessage(url, true); + } + } + + /** + * Open and present the messaging window + */ + sms() { + this.window.present(); + } + + /** + * This is the sms: URI scheme handler + * + * @param {string} uri - The URI the handle (sms:|sms://|sms:///) + */ + uriSms(uri) { + try { + uri = new URI.SmsURI(uri); + + // Lookup contacts + const addresses = uri.recipients.map(number => { + return {address: number.toPhoneNumber()}; + }); + const contacts = this.device.contacts.lookupAddresses(addresses); + + // Present the window and show the conversation + const window = this.window; + window.present(); + window.setContacts(contacts); + + // Set the outgoing message if the uri has a body variable + if (uri.body) + window.setMessage(uri.body); + } catch (e) { + debug(e, `${this.device.name}: "${uri}"`); + } + } + + _threadHasAddress(thread, addressObj) { + const number = addressObj.address.toPhoneNumber(); + + for (const taddressObj of thread[0].addresses) { + const tnumber = taddressObj.address.toPhoneNumber(); + + if (number.endsWith(tnumber) || tnumber.endsWith(number)) + return true; + } + + return false; + } + + /** + * Try to find a thread_id in @smsPlugin for @addresses. + * + * @param {Object[]} addresses - a list of address objects + * @return {string|null} a thread ID + */ + getThreadIdForAddresses(addresses = []) { + const threads = Object.values(this.threads); + + for (const thread of threads) { + if (addresses.length !== thread[0].addresses.length) + continue; + + if (addresses.every(addressObj => this._threadHasAddress(thread, addressObj))) + return thread[0].thread_id; + } + + return null; + } + + destroy() { + if (this._window !== undefined) + this._window.destroy(); + + super.destroy(); + } +}); diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/systemvolume.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/systemvolume.js new file mode 100644 index 0000000..758fcc3 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/systemvolume.js @@ -0,0 +1,200 @@ +'use strict'; + +const GObject = imports.gi.GObject; + +const Components = imports.service.components; +const Config = imports.config; +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('System Volume'), + description: _('Enable the paired device to control the system volume'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.SystemVolume', + incomingCapabilities: ['kdeconnect.systemvolume.request'], + outgoingCapabilities: ['kdeconnect.systemvolume'], + actions: {}, +}; + + +/** + * SystemVolume Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/systemvolume + * https://github.com/KDE/kdeconnect-android/tree/master/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/ + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectSystemVolumePlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'systemvolume'); + + // Cache stream properties + this._cache = new WeakMap(); + + // Connect to the mixer + try { + this._mixer = Components.acquire('pulseaudio'); + + this._streamChangedId = this._mixer.connect( + 'stream-changed', + this._sendSink.bind(this) + ); + + this._outputAddedId = this._mixer.connect( + 'output-added', + this._sendSinkList.bind(this) + ); + + this._outputRemovedId = this._mixer.connect( + 'output-removed', + this._sendSinkList.bind(this) + ); + + // Modify the error to redirect to the wiki + } catch (e) { + e.name = _('PulseAudio not found'); + e.url = `${Config.PACKAGE_URL}/wiki/Error#pulseaudio-not-found`; + throw e; + } + } + + handlePacket(packet) { + switch (true) { + case packet.body.hasOwnProperty('requestSinks'): + this._sendSinkList(); + break; + + case packet.body.hasOwnProperty('name'): + this._changeSink(packet); + break; + } + } + + connected() { + super.connected(); + + this._sendSinkList(); + } + + /** + * Handle a request to change an output + * + * @param {Core.Packet} packet - a `kdeconnect.systemvolume.request` + */ + _changeSink(packet) { + let stream; + + for (const sink of this._mixer.get_sinks()) { + if (sink.name === packet.body.name) { + stream = sink; + break; + } + } + + // No sink with the given name + if (stream === undefined) { + this._sendSinkList(); + return; + } + + // Get a cache and store volume and mute states if changed + const cache = this._cache.get(stream) || {}; + + if (packet.body.hasOwnProperty('muted')) { + cache.muted = packet.body.muted; + this._cache.set(stream, cache); + stream.change_is_muted(packet.body.muted); + } + + if (packet.body.hasOwnProperty('volume')) { + cache.volume = packet.body.volume; + this._cache.set(stream, cache); + stream.volume = packet.body.volume; + stream.push_volume(); + } + } + + /** + * Update the cache for @stream + * + * @param {Gvc.MixerStream} stream - The stream to cache + * @return {Object} The updated cache object + */ + _updateCache(stream) { + const state = { + name: stream.name, + description: stream.display_name, + muted: stream.is_muted, + volume: stream.volume, + maxVolume: this._mixer.get_vol_max_norm(), + }; + + this._cache.set(stream, state); + + return state; + } + + /** + * Send the state of a local sink + * + * @param {Gvc.MixerControl} mixer - The mixer that owns the stream + * @param {number} id - The Id of the stream that changed + */ + _sendSink(mixer, id) { + // Avoid starving the packet channel when fading + if (this._mixer.fading) + return; + + // Check the cache + const stream = this._mixer.lookup_stream_id(id); + const cache = this._cache.get(stream) || {}; + + // If the port has changed we have to send the whole list to update the + // display name + if (!cache.display_name || cache.display_name !== stream.display_name) { + this._sendSinkList(); + return; + } + + // If only volume and/or mute are set, send a single update + if (cache.volume !== stream.volume || cache.muted !== stream.is_muted) { + // Update the cache + const state = this._updateCache(stream); + + // Send the stream update + this.device.sendPacket({ + type: 'kdeconnect.systemvolume', + body: state, + }); + } + } + + /** + * Send a list of local sinks + */ + _sendSinkList() { + const sinkList = this._mixer.get_sinks().map(sink => { + return this._updateCache(sink); + }); + + // Send the sinkList + this.device.sendPacket({ + type: 'kdeconnect.systemvolume', + body: { + sinkList: sinkList, + }, + }); + } + + destroy() { + if (this._mixer !== undefined) { + this._mixer.disconnect(this._streamChangedId); + this._mixer.disconnect(this._outputAddedId); + this._mixer.disconnect(this._outputRemovedId); + this._mixer = Components.release('pulseaudio'); + } + + super.destroy(); + } +}); diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/telephony.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/telephony.js new file mode 100644 index 0000000..7d1b44d --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/telephony.js @@ -0,0 +1,241 @@ +'use strict'; + +const GdkPixbuf = imports.gi.GdkPixbuf; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Components = imports.service.components; +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('Telephony'), + description: _('Be notified about calls and adjust system volume during ringing/ongoing calls'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Telephony', + incomingCapabilities: [ + 'kdeconnect.telephony', + ], + outgoingCapabilities: [ + 'kdeconnect.telephony.request', + 'kdeconnect.telephony.request_mute', + ], + actions: { + muteCall: { + // TRANSLATORS: Silence the actively ringing call + label: _('Mute Call'), + icon_name: 'audio-volume-muted-symbolic', + + parameter_type: null, + incoming: ['kdeconnect.telephony'], + outgoing: ['kdeconnect.telephony.request_mute'], + }, + }, +}; + + +/** + * Telephony Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/telephony + * https://github.com/KDE/kdeconnect-android/tree/master/src/org/kde/kdeconnect/Plugins/TelephonyPlugin + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectTelephonyPlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'telephony'); + + // Neither of these are crucial for the plugin to work + this._mpris = Components.acquire('mpris'); + this._mixer = Components.acquire('pulseaudio'); + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.telephony': + this._handleEvent(packet); + break; + } + } + + /** + * Change volume, microphone and media player state in response to an + * incoming or answered call. + * + * @param {string} eventType - 'ringing' or 'talking' + */ + _setMediaState(eventType) { + // Mixer Volume + if (this._mixer !== undefined) { + switch (this.settings.get_string(`${eventType}-volume`)) { + case 'restore': + this._mixer.restore(); + break; + + case 'lower': + this._mixer.lowerVolume(); + break; + + case 'mute': + this._mixer.muteVolume(); + break; + } + + if (eventType === 'talking' && this.settings.get_boolean('talking-microphone')) + this._mixer.muteMicrophone(); + } + + // Media Playback + if (this._mpris && this.settings.get_boolean(`${eventType}-pause`)) + this._mpris.pauseAll(); + } + + /** + * Restore volume, microphone and media player state (if changed), making + * sure to unpause before raising volume. + * + * TODO: there's a possibility we might revert a media/mixer state set for + * another device. + */ + _restoreMediaState() { + // Media Playback + if (this._mpris) + this._mpris.unpauseAll(); + + // Mixer Volume + if (this._mixer) + this._mixer.restore(); + } + + /** + * Load a Gdk.Pixbuf from base64 encoded data + * + * @param {string} data - Base64 encoded JPEG data + * @return {Gdk.Pixbuf|null} A contact photo + */ + _getThumbnailPixbuf(data) { + const loader = new GdkPixbuf.PixbufLoader(); + + try { + data = GLib.base64_decode(data); + loader.write(data); + loader.close(); + } catch (e) { + debug(e, this.device.name); + } + + return loader.get_pixbuf(); + } + + /** + * Handle a telephony event (ringing, talking), showing or hiding a + * notification and possibly adjusting the media/mixer state. + * + * @param {Core.Packet} packet - A `kdeconnect.telephony` + */ + _handleEvent(packet) { + // Only handle 'ringing' or 'talking' events; leave the notification + // plugin to handle 'missedCall' since they're often repliable + if (!['ringing', 'talking'].includes(packet.body.event)) + return; + + // This is the end of a telephony event + if (packet.body.isCancel) + this._cancelEvent(packet); + else + this._notifyEvent(packet); + } + + _cancelEvent(packet) { + // Ensure we have a sender + // TRANSLATORS: No name or phone number + let sender = _('Unknown Contact'); + + if (packet.body.contactName) + sender = packet.body.contactName; + else if (packet.body.phoneNumber) + sender = packet.body.phoneNumber; + + this.device.hideNotification(`${packet.body.event}|${sender}`); + this._restoreMediaState(); + } + + _notifyEvent(packet) { + let body; + let buttons = []; + let icon = null; + let priority = Gio.NotificationPriority.NORMAL; + + // Ensure we have a sender + // TRANSLATORS: No name or phone number + let sender = _('Unknown Contact'); + + if (packet.body.contactName) + sender = packet.body.contactName; + else if (packet.body.phoneNumber) + sender = packet.body.phoneNumber; + + // If there's a photo, use it as the notification icon + if (packet.body.phoneThumbnail) + icon = this._getThumbnailPixbuf(packet.body.phoneThumbnail); + + if (icon === null) + icon = new Gio.ThemedIcon({name: 'call-start-symbolic'}); + + // Notify based based on the event type + if (packet.body.event === 'ringing') { + this._setMediaState('ringing'); + + // TRANSLATORS: The phone is ringing + body = _('Incoming call'); + buttons = [{ + action: 'muteCall', + // TRANSLATORS: Silence the actively ringing call + label: _('Mute'), + parameter: null, + }]; + priority = Gio.NotificationPriority.URGENT; + } + + if (packet.body.event === 'talking') { + this.device.hideNotification(`ringing|${sender}`); + this._setMediaState('talking'); + + // TRANSLATORS: A phone call is active + body = _('Ongoing call'); + } + + this.device.showNotification({ + id: `${packet.body.event}|${sender}`, + title: sender, + body: body, + icon: icon, + priority: priority, + buttons: buttons, + }); + } + + /** + * Silence an incoming call and restore the previous mixer/media state, if + * applicable. + */ + muteCall() { + this.device.sendPacket({ + type: 'kdeconnect.telephony.request_mute', + body: {}, + }); + + this._restoreMediaState(); + } + + destroy() { + if (this._mixer !== undefined) + this._mixer = Components.release('pulseaudio'); + + if (this._mpris !== undefined) + this._mpris = Components.release('mpris'); + + super.destroy(); + } +}); diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/__init__.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/__init__.js new file mode 100644 index 0000000..c85aea9 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/__init__.js @@ -0,0 +1,49 @@ +'use strict'; + +const Gdk = imports.gi.Gdk; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Gtk = imports.gi.Gtk; + +const Config = imports.config; + + +/* + * Window State + */ +Gtk.Window.prototype.restoreGeometry = function (context = 'default') { + this._windowState = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup( + 'org.gnome.Shell.Extensions.GSConnect.WindowState', + true + ), + path: `/org/gnome/shell/extensions/gsconnect/${context}/`, + }); + + // Size + const [width, height] = this._windowState.get_value('window-size').deepUnpack(); + + if (width && height) + this.set_default_size(width, height); + + // Maximized State + if (this._windowState.get_boolean('window-maximized')) + this.maximize(); +}; + +Gtk.Window.prototype.saveGeometry = function () { + const state = this.get_window().get_state(); + + // Maximized State + const maximized = (state & Gdk.WindowState.MAXIMIZED); + this._windowState.set_boolean('window-maximized', maximized); + + // Leave the size at the value before maximizing + if (maximized || (state & Gdk.WindowState.FULLSCREEN)) + return; + + // Size + const size = this.get_size(); + this._windowState.set_value('window-size', new GLib.Variant('(ii)', size)); +}; + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/contacts.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/contacts.js new file mode 100644 index 0000000..93467c8 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/contacts.js @@ -0,0 +1,638 @@ +'use strict'; + +const Gdk = imports.gi.Gdk; +const GdkPixbuf = imports.gi.GdkPixbuf; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + + +/** + * Return a random color + * + * @param {*} [salt] - If not %null, will be used as salt for generating a color + * @param {number} alpha - A value in the [0...1] range for the alpha channel + * @return {Gdk.RGBA} A new Gdk.RGBA object generated from the input + */ +function randomRGBA(salt = null, alpha = 1.0) { + let red, green, blue; + + if (salt !== null) { + const hash = new GLib.Variant('s', `${salt}`).hash(); + red = ((hash & 0xFF0000) >> 16) / 255; + green = ((hash & 0x00FF00) >> 8) / 255; + blue = (hash & 0x0000FF) / 255; + } else { + red = Math.random(); + green = Math.random(); + blue = Math.random(); + } + + return new Gdk.RGBA({red: red, green: green, blue: blue, alpha: alpha}); +} + + +/** + * Get the relative luminance of a RGB set + * See: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef + * + * @param {Gdk.RGBA} rgba - A GdkRGBA object + * @return {number} The relative luminance of the color + */ +function relativeLuminance(rgba) { + const {red, green, blue} = rgba; + + const R = (red > 0.03928) ? red / 12.92 : Math.pow(((red + 0.055) / 1.055), 2.4); + const G = (green > 0.03928) ? green / 12.92 : Math.pow(((green + 0.055) / 1.055), 2.4); + const B = (blue > 0.03928) ? blue / 12.92 : Math.pow(((blue + 0.055) / 1.055), 2.4); + + return 0.2126 * R + 0.7152 * G + 0.0722 * B; +} + + +/** + * Get a GdkRGBA contrasted for the input + * See: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef + * + * @param {Gdk.RGBA} rgba - A GdkRGBA object for the background color + * @return {Gdk.RGBA} A GdkRGBA object for the foreground color + */ +function getFgRGBA(rgba) { + const bgLuminance = relativeLuminance(rgba); + const lightContrast = (0.07275541795665634 + 0.05) / (bgLuminance + 0.05); + const darkContrast = (bgLuminance + 0.05) / (0.0046439628482972135 + 0.05); + + const value = (darkContrast > lightContrast) ? 0.06 : 0.94; + return new Gdk.RGBA({red: value, green: value, blue: value, alpha: 0.5}); +} + + +/** + * Get a GdkPixbuf for @path, allowing the corrupt JPEG's KDE Connect sometimes + * sends. This function is synchronous. + * + * @param {string} path - A local file path + * @param {number} size - Size in pixels + * @param {scale} [scale] - Scale factor for the size + * @return {Gdk.Pixbuf} A pixbuf + */ +function getPixbufForPath(path, size, scale = 1.0) { + let data, loader; + + // Catch missing avatar files + try { + data = GLib.file_get_contents(path)[1]; + } catch (e) { + debug(e, path); + return undefined; + } + + // Consider errors from partially corrupt JPEGs to be warnings + try { + loader = new GdkPixbuf.PixbufLoader(); + loader.write(data); + loader.close(); + } catch (e) { + debug(e, path); + } + + const pixbuf = loader.get_pixbuf(); + + // Scale to monitor + size = Math.floor(size * scale); + return pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.HYPER); +} + +function getPixbufForIcon(name, size, scale, bgColor) { + const color = getFgRGBA(bgColor); + const theme = Gtk.IconTheme.get_default(); + const info = theme.lookup_icon_for_scale( + name, + size, + scale, + Gtk.IconLookupFlags.FORCE_SYMBOLIC + ); + + return info.load_symbolic(color, null, null, null)[0]; +} + + +/** + * Return a localized string for a phone number type + * See: http://www.ietf.org/rfc/rfc2426.txt + * + * @param {string} type - An RFC2426 phone number type + * @return {string} A localized string like 'Mobile' + */ +function getNumberTypeLabel(type) { + if (type.includes('fax')) + // TRANSLATORS: A fax number + return _('Fax'); + + if (type.includes('work')) + // TRANSLATORS: A work or office phone number + return _('Work'); + + if (type.includes('cell')) + // TRANSLATORS: A mobile or cellular phone number + return _('Mobile'); + + if (type.includes('home')) + // TRANSLATORS: A home phone number + return _('Home'); + + // TRANSLATORS: All other phone number types + return _('Other'); +} + +/** + * Get a display number from @contact for @address. + * + * @param {Object} contact - A contact object + * @param {string} address - A phone number + * @return {string} A (possibly) better display number for the address + */ +function getDisplayNumber(contact, address) { + const number = address.toPhoneNumber(); + + for (const contactNumber of contact.numbers) { + const cnumber = contactNumber.value.toPhoneNumber(); + + if (number.endsWith(cnumber) || cnumber.endsWith(number)) + return GLib.markup_escape_text(contactNumber.value, -1); + } + + return GLib.markup_escape_text(address, -1); +} + + +/** + * Contact Avatar + */ +const AvatarCache = new WeakMap(); + +var Avatar = GObject.registerClass({ + GTypeName: 'GSConnectContactAvatar', +}, class ContactAvatar extends Gtk.DrawingArea { + + _init(contact = null) { + super._init({ + height_request: 32, + width_request: 32, + valign: Gtk.Align.CENTER, + visible: true, + }); + + this.contact = contact; + } + + get rgba() { + if (this._rgba === undefined) { + if (this.contact) + this._rgba = randomRGBA(this.contact.name); + else + this._rgba = randomRGBA(GLib.uuid_string_random()); + } + + return this._rgba; + } + + get contact() { + if (this._contact === undefined) + this._contact = null; + + return this._contact; + } + + set contact(contact) { + if (this.contact === contact) + return; + + this._contact = contact; + this._surface = undefined; + this._rgba = undefined; + this._offset = 0; + } + + _loadSurface() { + // Get the monitor scale + const display = Gdk.Display.get_default(); + const monitor = display.get_monitor_at_window(this.get_window()); + const scale = monitor.get_scale_factor(); + + // If there's a contact with an avatar, try to load it + if (this.contact && this.contact.avatar) { + // Check the cache + this._surface = AvatarCache.get(this.contact); + + // Try loading the pixbuf + if (!this._surface) { + const pixbuf = getPixbufForPath( + this.contact.avatar, + this.width_request, + scale + ); + + if (pixbuf) { + this._surface = Gdk.cairo_surface_create_from_pixbuf( + pixbuf, + 0, + this.get_window() + ); + AvatarCache.set(this.contact, this._surface); + } + } + } + + // If we still don't have a surface, load a fallback + if (!this._surface) { + let iconName; + + // If we were given a contact, it's direct message otherwise group + if (this.contact) + iconName = 'avatar-default-symbolic'; + else + iconName = 'group-avatar-symbolic'; + + // Center the icon + this._offset = (this.width_request - 24) / 2; + + // Load the fallback + const pixbuf = getPixbufForIcon(iconName, 24, scale, this.rgba); + + this._surface = Gdk.cairo_surface_create_from_pixbuf( + pixbuf, + 0, + this.get_window() + ); + } + } + + vfunc_draw(cr) { + if (!this._surface) + this._loadSurface(); + + // Clip to a circle + const rad = this.width_request / 2; + cr.arc(rad, rad, rad, 0, 2 * Math.PI); + cr.clipPreserve(); + + // Fill the background if the the surface is offset + if (this._offset > 0) { + Gdk.cairo_set_source_rgba(cr, this.rgba); + cr.fill(); + } + + // Draw the avatar/icon + cr.setSourceSurface(this._surface, this._offset, this._offset); + cr.paint(); + + cr.$dispose(); + return Gdk.EVENT_PROPAGATE; + } +}); + + +/** + * A row for a contact address (usually a phone number). + */ +const AddressRow = GObject.registerClass({ + GTypeName: 'GSConnectContactsAddressRow', + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/contacts-address-row.ui', + Children: ['avatar', 'name-label', 'address-label', 'type-label'], +}, class AddressRow extends Gtk.ListBoxRow { + + _init(contact, index = 0) { + super._init(); + + this._index = index; + this._number = contact.numbers[index]; + this.contact = contact; + } + + get contact() { + if (this._contact === undefined) + this._contact = null; + + return this._contact; + } + + set contact(contact) { + if (this.contact === contact) + return; + + this._contact = contact; + + if (this._index === 0) { + this.avatar.contact = contact; + this.avatar.visible = true; + + this.name_label.label = GLib.markup_escape_text(contact.name, -1); + this.name_label.visible = true; + + this.address_label.margin_start = 0; + this.address_label.margin_end = 0; + } else { + this.avatar.visible = false; + this.name_label.visible = false; + + // TODO: rtl inverts margin-start so the number don't align + this.address_label.margin_start = 38; + this.address_label.margin_end = 38; + } + + this.address_label.label = GLib.markup_escape_text(this.number.value, -1); + + if (this.number.type !== undefined) + this.type_label.label = getNumberTypeLabel(this.number.type); + } + + get number() { + if (this._number === undefined) + return {value: 'unknown', type: 'unknown'}; + + return this._number; + } +}); + + +/** + * A widget for selecting contact addresses (usually phone numbers) + */ +var ContactChooser = GObject.registerClass({ + GTypeName: 'GSConnectContactChooser', + Properties: { + 'device': GObject.ParamSpec.object( + 'device', + 'Device', + 'The device associated with this window', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'store': GObject.ParamSpec.object( + 'store', + 'Store', + 'The contacts store', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT, + GObject.Object + ), + }, + Signals: { + 'number-selected': { + flags: GObject.SignalFlags.RUN_FIRST, + param_types: [GObject.TYPE_STRING], + }, + }, + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/contact-chooser.ui', + Children: ['entry', 'list', 'scrolled'], +}, class ContactChooser extends Gtk.Grid { + + _init(params) { + super._init(params); + + // Setup the contact list + this.list._entry = this.entry.text; + this.list.set_filter_func(this._filter); + this.list.set_sort_func(this._sort); + + // Make sure we're using the correct contacts store + this.device.bind_property( + 'contacts', + this, + 'store', + GObject.BindingFlags.SYNC_CREATE + ); + + // Cleanup on ::destroy + this.connect('destroy', this._onDestroy); + } + + get store() { + if (this._store === undefined) + this._store = null; + + return this._store; + } + + set store(store) { + if (this.store === store) + return; + + // Unbind the old store + if (this._store) { + // Disconnect from the store + this._store.disconnect(this._contactAddedId); + this._store.disconnect(this._contactRemovedId); + this._store.disconnect(this._contactChangedId); + + // Clear the contact list + const rows = this.list.get_children(); + + for (let i = 0, len = rows.length; i < len; i++) { + rows[i].destroy(); + // HACK: temporary mitigator for mysterious GtkListBox leak + imports.system.gc(); + } + } + + // Set the store + this._store = store; + + // Bind the new store + if (this._store) { + // Connect to the new store + this._contactAddedId = store.connect( + 'contact-added', + this._onContactAdded.bind(this) + ); + + this._contactRemovedId = store.connect( + 'contact-removed', + this._onContactRemoved.bind(this) + ); + + this._contactChangedId = store.connect( + 'contact-changed', + this._onContactChanged.bind(this) + ); + + // Populate the list + this._populate(); + } + } + + /* + * ContactStore Callbacks + */ + _onContactAdded(store, id) { + const contact = this.store.get_contact(id); + this._addContact(contact); + } + + _onContactRemoved(store, id) { + const rows = this.list.get_children(); + + for (let i = 0, len = rows.length; i < len; i++) { + const row = rows[i]; + + if (row.contact.id === id) { + row.destroy(); + // HACK: temporary mitigator for mysterious GtkListBox leak + imports.system.gc(); + } + } + } + + _onContactChanged(store, id) { + this._onContactRemoved(store, id); + this._onContactAdded(store, id); + } + + _onDestroy(chooser) { + chooser.store = null; + } + + _onSearchChanged(entry) { + this.list._entry = entry.text; + let dynamic = this.list.get_row_at_index(0); + + // If the entry contains string with 2 or more digits... + if (entry.text.replace(/\D/g, '').length >= 2) { + // ...ensure we have a dynamic contact for it + if (!dynamic || !dynamic.__tmp) { + dynamic = new AddressRow({ + // TRANSLATORS: A phone number (eg. "Send to 555-5555") + name: _('Send to %s').format(entry.text), + numbers: [{type: 'unknown', value: entry.text}], + }); + dynamic.__tmp = true; + this.list.add(dynamic); + + // ...or if we already do, then update it + } else { + const address = entry.text; + + // Update contact object + dynamic.contact.name = address; + dynamic.contact.numbers[0].value = address; + + // Update UI + dynamic.name_label.label = _('Send to %s').format(address); + dynamic.address_label.label = address; + } + + // ...otherwise remove any dynamic contact that's been created + } else if (dynamic && dynamic.__tmp) { + dynamic.destroy(); + } + + this.list.invalidate_filter(); + this.list.invalidate_sort(); + } + + // GtkListBox::row-activated + _onNumberSelected(box, row) { + if (row === null) + return; + + // Emit the number + const address = row.number.value; + this.emit('number-selected', address); + + // Reset the contact list + this.entry.text = ''; + this.list.select_row(null); + this.scrolled.vadjustment.value = 0; + } + + _filter(row) { + // Dynamic contact always shown + if (row.__tmp) + return true; + + const query = row.get_parent()._entry; + + // Show contact if text is substring of name + const queryName = query.toLocaleLowerCase(); + + if (row.contact.name.toLocaleLowerCase().includes(queryName)) + return true; + + // Show contact if text is substring of number + const queryNumber = query.toPhoneNumber(); + + if (queryNumber.length) { + for (const number of row.contact.numbers) { + if (number.value.toPhoneNumber().includes(queryNumber)) + return true; + } + + // Query is effectively empty + } else if (/^0+/.test(query)) { + return true; + } + + return false; + } + + _sort(row1, row2) { + if (row1.__tmp) + return -1; + + if (row2.__tmp) + return 1; + + return row1.contact.name.localeCompare(row2.contact.name); + } + + _populate() { + // Add each contact + const contacts = this.store.contacts; + + for (let i = 0, len = contacts.length; i < len; i++) + this._addContact(contacts[i]); + } + + _addContactNumber(contact, index) { + const row = new AddressRow(contact, index); + this.list.add(row); + + return row; + } + + _addContact(contact) { + try { + // HACK: fix missing contact names + if (contact.name === undefined) + contact.name = _('Unknown Contact'); + + if (contact.numbers.length === 1) + return this._addContactNumber(contact, 0); + + for (let i = 0, len = contact.numbers.length; i < len; i++) + this._addContactNumber(contact, i); + } catch (e) { + logError(e); + } + } + + /** + * Get a dictionary of number-contact pairs for each selected phone number. + * + * @return {Object[]} A dictionary of contacts + */ + getSelected() { + try { + const selected = {}; + + for (const row of this.list.get_selected_rows()) + selected[row.number.value] = row.contact; + + return selected; + } catch (e) { + logError(e); + return {}; + } + } +}); + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/legacyMessaging.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/legacyMessaging.js new file mode 100644 index 0000000..91cfb24 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/legacyMessaging.js @@ -0,0 +1,223 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + +const Contacts = imports.service.ui.contacts; +const Messaging = imports.service.ui.messaging; +const URI = imports.service.utils.uri; + + +var Dialog = GObject.registerClass({ + GTypeName: 'GSConnectLegacyMessagingDialog', + Properties: { + 'device': GObject.ParamSpec.object( + 'device', + 'Device', + 'The device associated with this window', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'plugin': GObject.ParamSpec.object( + 'plugin', + 'Plugin', + 'The plugin providing messages', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + }, + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/legacy-messaging-dialog.ui', + Children: [ + 'infobar', 'stack', + 'message-box', 'message-avatar', 'message-label', 'entry', + ], +}, class Dialog extends Gtk.Dialog { + + _init(params) { + super._init({ + application: Gio.Application.get_default(), + device: params.device, + plugin: params.plugin, + use_header_bar: true, + }); + + this.set_response_sensitive(Gtk.ResponseType.OK, false); + + // Dup some functions + this.headerbar = this.get_titlebar(); + this._setHeaderBar = Messaging.Window.prototype._setHeaderBar; + + // Info bar + this.device.bind_property( + 'connected', + this.infobar, + 'reveal-child', + GObject.BindingFlags.INVERT_BOOLEAN + ); + + // Message Entry/Send Button + this.device.bind_property( + 'connected', + this.entry, + 'sensitive', + GObject.BindingFlags.DEFAULT + ); + + this._connectedId = this.device.connect( + 'notify::connected', + this._onStateChanged.bind(this) + ); + + this._entryChangedId = this.entry.buffer.connect( + 'changed', + this._onStateChanged.bind(this) + ); + + // Set the message if given + if (params.message) { + this.message = params.message; + this.addresses = params.message.addresses; + + this.message_avatar.contact = this.device.contacts.query({ + number: this.addresses[0].address, + }); + this.message_label.label = URI.linkify(this.message.body); + this.message_box.visible = true; + + // Otherwise set the address(es) if we were passed those + } else if (params.addresses) { + this.addresses = params.addresses; + } + + // Load the contact list if we weren't supplied with an address + if (this.addresses.length === 0) { + this.contact_chooser = new Contacts.ContactChooser({ + device: this.device, + }); + this.stack.add_named(this.contact_chooser, 'contact-chooser'); + this.stack.child_set_property(this.contact_chooser, 'position', 0); + + this._numberSelectedId = this.contact_chooser.connect( + 'number-selected', + this._onNumberSelected.bind(this) + ); + + this.stack.visible_child_name = 'contact-chooser'; + } + + this.restoreGeometry('legacy-messaging-dialog'); + + this.connect('destroy', this._onDestroy); + } + + _onDestroy(dialog) { + if (dialog._numberSelectedId !== undefined) { + dialog.contact_chooser.disconnect(dialog._numberSelectedId); + dialog.contact_chooser.destroy(); + } + + dialog.entry.buffer.disconnect(dialog._entryChangedId); + dialog.device.disconnect(dialog._connectedId); + } + + vfunc_delete_event() { + this.saveGeometry(); + + return false; + } + + vfunc_response(response_id) { + if (response_id === Gtk.ResponseType.OK) { + // Refuse to send empty or whitespace only texts + if (!this.entry.buffer.text.trim()) + return; + + this.plugin.sendMessage( + this.addresses, + this.entry.buffer.text, + 1, + true + ); + } + + this.destroy(); + } + + get addresses() { + if (this._addresses === undefined) + this._addresses = []; + + return this._addresses; + } + + set addresses(addresses = []) { + this._addresses = addresses; + + // Set the headerbar + this._setHeaderBar(this._addresses); + + // Show the message editor + this.stack.visible_child_name = 'message-editor'; + this._onStateChanged(); + } + + get device() { + if (this._device === undefined) + this._device = null; + + return this._device; + } + + set device(device) { + this._device = device; + } + + get plugin() { + if (this._plugin === undefined) + this._plugin = null; + + return this._plugin; + } + + set plugin(plugin) { + this._plugin = plugin; + } + + _onActivateLink(label, uri) { + Gtk.show_uri_on_window( + this.get_toplevel(), + uri.includes('://') ? uri : `https://${uri}`, + Gtk.get_current_event_time() + ); + + return true; + } + + _onNumberSelected(chooser, number) { + const contacts = chooser.getSelected(); + + this.addresses = Object.keys(contacts).map(address => { + return {address: address}; + }); + } + + _onStateChanged() { + if (this.device.connected && + this.entry.buffer.text.trim() && + this.stack.visible_child_name === 'message-editor') + this.set_response_sensitive(Gtk.ResponseType.OK, true); + else + this.set_response_sensitive(Gtk.ResponseType.OK, false); + } + + /** + * Set the contents of the message entry + * + * @param {string} text - The message to place in the entry + */ + setMessage(text) { + this.entry.buffer.text = text; + } +}); + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/messaging.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/messaging.js new file mode 100644 index 0000000..40a91ae --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/messaging.js @@ -0,0 +1,1312 @@ +'use strict'; + +const Tweener = imports.tweener.tweener; + +const Gdk = imports.gi.Gdk; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; +const Pango = imports.gi.Pango; + +const Contacts = imports.service.ui.contacts; +const Sms = imports.service.plugins.sms; +const URI = imports.service.utils.uri; + + +/* + * Useful time constants + */ +const TIME_SPAN_MINUTE = 60000; +const TIME_SPAN_HOUR = 3600000; +const TIME_SPAN_DAY = 86400000; +const TIME_SPAN_WEEK = 604800000; + + +// Less than an hour (eg. 42 minutes ago) +const _lthLong = new Intl.RelativeTimeFormat('default', { + numeric: 'auto', + style: 'long', +}); + +// Less than a day ago (eg. 11:42 PM) +const _ltdFormat = new Intl.DateTimeFormat('default', { + hour: 'numeric', + minute: 'numeric', +}); + +// Less than a week ago (eg. Monday) +const _ltwLong = new Intl.DateTimeFormat('default', { + weekday: 'long', +}); + +// Less than a week ago (eg. Mon) +const _ltwShort = new Intl.DateTimeFormat('default', { + weekday: 'short', +}); + +// Less than a year (eg. Oct 31) +const _ltyShort = new Intl.DateTimeFormat('default', { + day: 'numeric', + month: 'short', +}); + +// Less than a year (eg. October 31) +const _ltyLong = new Intl.DateTimeFormat('default', { + day: 'numeric', + month: 'long', +}); + +// Greater than a year (eg. October 31, 2019) +const _gtyLong = new Intl.DateTimeFormat('default', { + day: 'numeric', + month: 'long', + year: 'numeric', +}); + +// Greater than a year (eg. 10/31/2019) +const _gtyShort = new Intl.DateTimeFormat('default', { + day: 'numeric', + month: 'numeric', + year: 'numeric', +}); + +// Pretty close to strftime's %c +const _cFormat = new Intl.DateTimeFormat('default', { + year: 'numeric', + month: 'short', + day: 'numeric', + weekday: 'short', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', +}); + + +/** + * Return a human-readable timestamp, formatted for longer contexts. + * + * @param {number} time - Milliseconds since the epoch (local time) + * @return {string} A localized timestamp similar to what Android Messages uses + */ +function getTime(time) { + const date = new Date(time); + const now = new Date(); + const diff = now - time; + + // Super recent + if (diff < TIME_SPAN_MINUTE) + // TRANSLATORS: Less than a minute ago + return _('Just now'); + + // Under an hour (TODO: these labels aren't updated) + if (diff < TIME_SPAN_HOUR) + return _lthLong.format(-Math.floor(diff / TIME_SPAN_MINUTE), 'minute'); + + // Yesterday, but less than 24 hours ago + if (diff < TIME_SPAN_DAY && now.getDay() !== date.getDay()) + // TRANSLATORS: Yesterday, but less than 24 hours (eg. Yesterday · 11:29 PM) + return _('Yesterday・%s').format(_ltdFormat.format(time)); + + // Less than a day ago + if (diff < TIME_SPAN_DAY) + return _ltdFormat.format(time); + + // Less than a week ago + if (diff < TIME_SPAN_WEEK) + return _ltwLong.format(time); + + // Sometime this year + if (date.getFullYear() === now.getFullYear()) + return _ltyLong.format(time); + + // Earlier than that + return _gtyLong.format(time); +} + + +/** + * Return a human-readable timestamp, formatted for shorter contexts. + * + * @param {number} time - Milliseconds since the epoch (local time) + * @return {string} A localized timestamp similar to what Android Messages uses + */ +function getShortTime(time) { + const date = new Date(time); + const now = new Date(); + const diff = now - time; + + if (diff < TIME_SPAN_MINUTE) + // TRANSLATORS: Less than a minute ago + return _('Just now'); + + if (diff < TIME_SPAN_HOUR) { + // TRANSLATORS: Time duration in minutes (eg. 15 minutes) + return ngettext( + '%d minute', + '%d minutes', + (diff / TIME_SPAN_MINUTE) + ).format(diff / TIME_SPAN_MINUTE); + } + + // Less than a day ago + if (diff < TIME_SPAN_DAY) + return _ltdFormat.format(time); + + // Less than a week ago + if (diff < TIME_SPAN_WEEK) + return _ltwShort.format(time); + + // Sometime this year + if (date.getFullYear() === now.getFullYear()) + return _ltyShort.format(time); + + // Earlier than that + return _gtyShort.format(time); +} + + +/** + * Return a human-readable timestamp, similar to `strftime()` with `%c`. + * + * @param {number} time - Milliseconds since the epoch (local time) + * @return {string} A localized timestamp + */ +function getDetailedTime(time) { + return _cFormat.format(time); +} + + +function setAvatarVisible(row, visible) { + const incoming = row.message.type === Sms.MessageBox.INBOX; + + // Adjust the margins + if (visible) { + row.grid.margin_start = incoming ? 6 : 56; + row.grid.margin_bottom = 6; + } else { + row.grid.margin_start = incoming ? 44 : 56; + row.grid.margin_bottom = 0; + } + + // Show hide the avatar + if (incoming) + row.avatar.visible = visible; +} + + +/** + * A ListBoxRow for a preview of a conversation + */ +const ConversationMessage = GObject.registerClass({ + GTypeName: 'GSConnectMessagingConversationMessage', + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/messaging-conversation-message.ui', + Children: ['grid', 'avatar', 'sender-label', 'message-label'], +}, class ConversationMessage extends Gtk.ListBoxRow { + _init(contact, message) { + super._init(); + + this.contact = contact; + this.message = message; + + // Sort properties + this.sender = message.addresses[0].address || 'unknown'; + this.message_label.label = URI.linkify(message.body); + this.message_label.tooltip_text = getDetailedTime(message.date); + + // Add avatar for incoming messages + if (message.type === Sms.MessageBox.INBOX) { + this.grid.margin_end = 18; + this.grid.halign = Gtk.Align.START; + + this.avatar.contact = this.contact; + this.avatar.visible = true; + + this.sender_label.label = contact.name; + this.sender_label.visible = true; + + this.message_label.get_style_context().add_class('message-in'); + this.message_label.halign = Gtk.Align.START; + } else { + this.message_label.get_style_context().add_class('message-out'); + } + } + + _onActivateLink(label, uri) { + Gtk.show_uri_on_window( + this.get_toplevel(), + uri.includes('://') ? uri : `https://${uri}`, + Gtk.get_current_event_time() + ); + + return true; + } + + get date() { + return this._message.date; + } + + get thread_id() { + return this._message.thread_id; + } + + get message() { + if (this._message === undefined) + this._message = null; + + return this._message; + } + + set message(message) { + this._message = message; + } +}); + + +/** + * A widget for displaying a conversation thread, with an entry for responding. + */ +const Conversation = GObject.registerClass({ + GTypeName: 'GSConnectMessagingConversation', + Properties: { + 'device': GObject.ParamSpec.object( + 'device', + 'Device', + 'The device associated with this conversation', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'plugin': GObject.ParamSpec.object( + 'plugin', + 'Plugin', + 'The plugin providing this conversation', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'has-pending': GObject.ParamSpec.boolean( + 'has-pending', + 'Has Pending', + 'Whether there are sent messages pending confirmation', + GObject.ParamFlags.READABLE, + false + ), + 'thread-id': GObject.ParamSpec.string( + 'thread-id', + 'Thread ID', + 'The current thread', + GObject.ParamFlags.READWRITE, + '' + ), + }, + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/messaging-conversation.ui', + Children: [ + 'entry', 'list', 'scrolled', + 'pending', 'pending-box', + ], +}, class MessagingConversation extends Gtk.Grid { + + _init(params) { + super._init({ + device: params.device, + plugin: params.plugin, + }); + Object.assign(this, params); + + this.device.bind_property( + 'connected', + this.entry, + 'sensitive', + GObject.BindingFlags.SYNC_CREATE + ); + + // If we're disconnected pending messages might not succeed, but we'll + // leave them until reconnect when we'll ask for an update + this._connectedId = this.device.connect( + 'notify::connected', + this._onConnected.bind(this) + ); + + // Pending messages + this.pending.message = { + date: Number.MAX_SAFE_INTEGER, + type: Sms.MessageBox.OUTBOX, + }; + + // Auto-scrolling + this._vadj = this.scrolled.get_vadjustment(); + this._scrolledId = this._vadj.connect( + 'value-changed', + this._holdPosition.bind(this) + ); + + // Message List + this.list.set_header_func(this._headerMessages); + this.list.set_sort_func(this._sortMessages); + this._populateMessages(); + + // Cleanup on ::destroy + this.connect('destroy', this._onDestroy); + } + + get addresses() { + if (this._addresses === undefined) + this._addresses = []; + + return this._addresses; + } + + set addresses(addresses) { + if (!addresses || addresses.length === 0) { + this._addresses = []; + this._contacts = {}; + return; + } + + // Lookup a contact for each address object, then loop back to correct + // each address carried by the message. + this._addresses = addresses; + + for (let i = 0, len = this.addresses.length; i < len; i++) { + // Lookup the contact + const address = this.addresses[i].address; + const contact = this.device.contacts.query({number: address}); + + // Get corrected address + let number = address.toPhoneNumber(); + + for (const contactNumber of contact.numbers) { + const cnumber = contactNumber.value.toPhoneNumber(); + + if (number.endsWith(cnumber) || cnumber.endsWith(number)) { + number = contactNumber.value; + break; + } + } + + // Store the final result + this.addresses[i].address = number; + this.contacts[address] = contact; + } + + // TODO: Mark the entry as insensitive for group messages + if (this.addresses.length > 1) { + this.entry.placeholder_text = _('Not available'); + this.entry.secondary_icon_name = null; + this.entry.secondary_icon_tooltip_text = null; + this.entry.sensitive = false; + this.entry.tooltip_text = null; + } + } + + get contacts() { + if (this._contacts === undefined) + this._contacts = {}; + + return this._contacts; + } + + get has_pending() { + if (this.pending_box === undefined) + return false; + + return (this.pending_box.get_children().length > 0); + } + + get plugin() { + if (this._plugin === undefined) + this._plugin = null; + + return this._plugin; + } + + set plugin(plugin) { + this._plugin = plugin; + } + + get thread_id() { + if (this._thread_id === undefined) + this._thread_id = null; + + return this._thread_id; + } + + set thread_id(thread_id) { + const thread = this.plugin.threads[thread_id]; + const message = (thread) ? thread[0] : null; + + if (message && this.addresses.length === 0) { + this.addresses = message.addresses; + this._thread_id = thread_id; + } + } + + _onConnected(device) { + if (device.connected) + this.pending_box.foreach(msg => msg.destroy()); + } + + _onDestroy(conversation) { + conversation.device.disconnect(conversation._connectedId); + conversation._vadj.disconnect(conversation._scrolledId); + + conversation.list.foreach(message => { + // HACK: temporary mitigator for mysterious GtkListBox leak + message.destroy(); + imports.system.gc(); + }); + } + + _onEdgeReached(scrolled_window, pos) { + // Try to load more messages + if (pos === Gtk.PositionType.TOP) + this.logPrevious(); + + // Release any hold to resume auto-scrolling + else if (pos === Gtk.PositionType.BOTTOM) + this._releasePosition(); + } + + _onEntryChanged(entry) { + entry.secondary_icon_sensitive = (entry.text.length); + } + + _onKeyPressEvent(entry, event) { + const keyval = event.get_keyval()[1]; + const state = event.get_state()[1]; + const mask = state & Gtk.accelerator_get_default_mod_mask(); + + if (keyval === Gdk.KEY_Return && (mask & Gdk.ModifierType.SHIFT_MASK)) { + entry.emit('insert-at-cursor', '\n'); + return true; + } + + return false; + } + + _onSendMessage(entry, signal_id, event) { + // Don't send empty texts + if (!this.entry.text.trim()) + return; + + // Send the message + this.plugin.sendMessage(this.addresses, this.entry.text); + + // Add a phony message in the pending box + const message = new Gtk.Label({ + label: URI.linkify(this.entry.text), + halign: Gtk.Align.END, + selectable: true, + use_markup: true, + visible: true, + wrap: true, + wrap_mode: Pango.WrapMode.WORD_CHAR, + xalign: 0, + }); + message.get_style_context().add_class('message-out'); + message.date = Date.now(); + message.type = Sms.MessageBox.SENT; + + // Notify to reveal the pending box + this.pending_box.add(message); + this.notify('has-pending'); + + // Clear the entry + this.entry.text = ''; + } + + _onSizeAllocate(listbox, allocation) { + const upper = this._vadj.get_upper(); + const pageSize = this._vadj.get_page_size(); + + // If the scrolled window hasn't been filled yet, load another message + if (upper <= pageSize) { + this.logPrevious(); + this.scrolled.get_child().check_resize(); + + // We've been asked to hold the position, so we'll reset the adjustment + // value and update the hold position + } else if (this.__pos) { + this._vadj.set_value(upper - this.__pos); + + // Otherwise we probably appended a message and should scroll to it + } else { + this._scrollPosition(Gtk.PositionType.BOTTOM); + } + } + + /** + * Create a message row, ensuring a contact object has been retrieved or + * generated for the message. + * + * @param {Object} message - A dictionary of message data + * @return {ConversationMessage} A message row + */ + _createMessageRow(message) { + // Ensure we have a contact + const sender = message.addresses[0].address || 'unknown'; + + if (this.contacts[sender] === undefined) { + this.contacts[sender] = this.device.contacts.query({ + number: sender, + }); + } + + return new ConversationMessage(this.contacts[sender], message); + } + + _populateMessages() { + this.__first = null; + this.__last = null; + this.__pos = 0; + this.__messages = []; + + // Try and find a thread_id for this number + if (this.thread_id === null && this.addresses.length) + this._thread_id = this.plugin.getThreadIdForAddresses(this.addresses); + + // Make a copy of the thread and fill the window with messages + if (this.plugin.threads[this.thread_id]) { + this.__messages = this.plugin.threads[this.thread_id].slice(0); + this.logPrevious(); + } + } + + _headerMessages(row, before) { + // Skip pending + if (row.get_name() === 'pending') + return; + + if (before === null) + return setAvatarVisible(row, true); + + // Add date header if the last message was more than an hour ago + let header = row.get_header(); + + if ((row.message.date - before.message.date) > TIME_SPAN_HOUR) { + if (!header) { + header = new Gtk.Label({visible: true, selectable: true}); + header.get_style_context().add_class('dim-label'); + row.set_header(header); + } + + header.label = getTime(row.message.date); + + // Also show the avatar + setAvatarVisible(row, true); + + row.sender_label.visible = row.message.addresses.length > 1; + + // Or if the previous sender was the same, hide its avatar + } else if (row.message.type === before.message.type && + row.sender.equalsPhoneNumber(before.sender)) { + setAvatarVisible(before, false); + setAvatarVisible(row, true); + + row.sender_label.visible = false; + + // otherwise show the avatar + } else { + setAvatarVisible(row, true); + } + } + + _holdPosition() { + this.__pos = this._vadj.get_upper() - this._vadj.get_value(); + } + + _releasePosition() { + this.__pos = 0; + } + + _scrollPosition(pos = Gtk.PositionType.BOTTOM, animate = true) { + let vpos = pos; + this._vadj.freeze_notify(); + + if (pos === Gtk.PositionType.BOTTOM) + vpos = this._vadj.get_upper() - this._vadj.get_page_size(); + + if (animate) { + Tweener.addTween(this._vadj, { + value: vpos, + time: 0.5, + transition: 'easeInOutCubic', + onComplete: () => this._vadj.thaw_notify(), + }); + } else { + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + this._vadj.set_value(vpos); + this._vadj.thaw_notify(); + }); + } + } + + _sortMessages(row1, row2) { + return (row1.message.date > row2.message.date) ? 1 : -1; + } + + /** + * Log the next message in the conversation. + * + * @param {Object} message - A message object + */ + logNext(message) { + try { + // TODO: Unsupported MessageBox + if (message.type !== Sms.MessageBox.INBOX && + message.type !== Sms.MessageBox.SENT) + throw TypeError(`invalid message box ${message.type}`); + + // Append the message + const row = this._createMessageRow(message); + this.list.add(row); + this.list.invalidate_headers(); + + // Remove the first pending message + if (this.has_pending && message.type === Sms.MessageBox.SENT) { + this.pending_box.get_children()[0].destroy(); + this.notify('has-pending'); + } + } catch (e) { + debug(e); + } + } + + /** + * Log the previous message in the thread + */ + logPrevious() { + try { + const message = this.__messages.pop(); + + if (!message) + return; + + // TODO: Unsupported MessageBox + if (message.type !== Sms.MessageBox.INBOX && + message.type !== Sms.MessageBox.SENT) + throw TypeError(`invalid message box ${message.type}`); + + // Prepend the message + const row = this._createMessageRow(message); + this.list.prepend(row); + this.list.invalidate_headers(); + } catch (e) { + debug(e); + } + } + + /** + * Set the contents of the message entry + * + * @param {string} text - The message to place in the entry + */ + setMessage(text) { + this.entry.text = text; + this.entry.emit('move-cursor', 0, text.length, false); + } +}); + + +/** + * A ListBoxRow for a preview of a conversation + */ +const ConversationSummary = GObject.registerClass({ + GTypeName: 'GSConnectMessagingConversationSummary', + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/messaging-conversation-summary.ui', + Children: ['avatar', 'name-label', 'time-label', 'body-label'], +}, class ConversationSummary extends Gtk.ListBoxRow { + _init(contacts, message) { + super._init(); + + this.contacts = contacts; + this.message = message; + } + + get date() { + return this._message.date; + } + + get thread_id() { + return this._message.thread_id; + } + + get message() { + return this._message; + } + + set message(message) { + this._message = message; + this._sender = message.addresses[0].address || 'unknown'; + + // Contact Name + let nameLabel = _('Unknown Contact'); + + // Update avatar for single-recipient messages + if (message.addresses.length === 1) { + this.avatar.contact = this.contacts[this._sender]; + nameLabel = GLib.markup_escape_text(this.avatar.contact.name, -1); + } else { + this.avatar.contact = null; + nameLabel = _('Group Message'); + const participants = []; + message.addresses.forEach((address) => { + participants.push(this.contacts[address.address].name); + }); + this.name_label.tooltip_text = participants.join(', '); + } + + // Contact Name & Message body + let bodyLabel = message.body.split(/\r|\n/)[0]; + bodyLabel = GLib.markup_escape_text(bodyLabel, -1); + + // Ignore the 'read' flag if it's an outgoing message + if (message.type === Sms.MessageBox.SENT) { + // TRANSLATORS: An outgoing message body in a conversation summary + bodyLabel = _('You: %s').format(bodyLabel); + + // Otherwise make it bold if it's unread + } else if (message.read === Sms.MessageStatus.UNREAD) { + nameLabel = `<b>${nameLabel}</b>`; + bodyLabel = `<b>${bodyLabel}</b>`; + } + + // Set the labels, body always smaller + this.name_label.label = nameLabel; + this.body_label.label = `<small>${bodyLabel}</small>`; + + // Time + const timeLabel = `<small>${getShortTime(message.date)}</small>`; + this.time_label.label = timeLabel; + } + + /** + * Update the relative time label. + */ + update() { + const timeLabel = `<small>${getShortTime(this.message.date)}</small>`; + this.time_label.label = timeLabel; + } +}); + + +/** + * A Gtk.ApplicationWindow for SMS conversations + */ +var Window = GObject.registerClass({ + GTypeName: 'GSConnectMessagingWindow', + Properties: { + 'device': GObject.ParamSpec.object( + 'device', + 'Device', + 'The device associated with this window', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'plugin': GObject.ParamSpec.object( + 'plugin', + 'Plugin', + 'The plugin providing messages', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'thread-id': GObject.ParamSpec.string( + 'thread-id', + 'Thread ID', + 'The current thread', + GObject.ParamFlags.READWRITE, + '' + ), + }, + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/messaging-window.ui', + Children: [ + 'headerbar', 'infobar', + 'thread-list', 'stack', + ], +}, class MessagingWindow extends Gtk.ApplicationWindow { + + _init(params) { + super._init(params); + this.headerbar.subtitle = this.device.name; + + this.insert_action_group('device', this.device); + + // Device Status + this.device.bind_property( + 'connected', + this.infobar, + 'reveal-child', + GObject.BindingFlags.INVERT_BOOLEAN + ); + + // Contacts + this.contact_chooser = new Contacts.ContactChooser({ + device: this.device, + }); + this.stack.add_named(this.contact_chooser, 'contact-chooser'); + + this._numberSelectedId = this.contact_chooser.connect( + 'number-selected', + this._onNumberSelected.bind(this) + ); + + // Threads + this.thread_list.set_sort_func(this._sortThreads); + + this._threadsChangedId = this.plugin.connect( + 'notify::threads', + this._onThreadsChanged.bind(this) + ); + + this._timestampThreadsId = GLib.timeout_add_seconds( + GLib.PRIORITY_DEFAULT_IDLE, + 60, + this._timestampThreads.bind(this) + ); + + this._sync(); + this._onThreadsChanged(); + this.restoreGeometry('messaging'); + } + + vfunc_delete_event(event) { + this.saveGeometry(); + + GLib.source_remove(this._timestampThreadsId); + this.contact_chooser.disconnect(this._numberSelectedId); + this.plugin.disconnect(this._threadsChangedId); + + return false; + } + + get plugin() { + return this._plugin || null; + } + + set plugin(plugin) { + this._plugin = plugin; + } + + get thread_id() { + return this.stack.visible_child_name; + } + + set thread_id(thread_id) { + thread_id = `${thread_id}`; // FIXME + + // Reset to the empty placeholder + if (!thread_id) { + this.thread_list.select_row(null); + this.stack.set_visible_child_name('placeholder'); + return; + } + + // Create a conversation widget if there isn't one + let conversation = this.stack.get_child_by_name(thread_id); + const thread = this.plugin.threads[thread_id]; + + if (conversation === null) { + if (!thread) { + debug(`Thread ID ${thread_id} not found`); + return; + } + + conversation = new Conversation({ + device: this.device, + plugin: this.plugin, + thread_id: thread_id, + }); + + this.stack.add_named(conversation, thread_id); + } + + // Figure out whether this is a multi-recipient thread + this._setHeaderBar(thread[0].addresses); + + // Select the conversation and entry active + this.stack.visible_child = conversation; + this.stack.visible_child.entry.has_focus = true; + + // There was a pending message waiting for a conversation to be chosen + if (this._pendingShare) { + conversation.setMessage(this._pendingShare); + this._pendingShare = null; + } + + this._thread_id = thread_id; + this.notify('thread_id'); + } + + _setHeaderBar(addresses = []) { + const address = addresses[0].address; + const contact = this.device.contacts.query({number: address}); + + if (addresses.length === 1) { + this.headerbar.title = contact.name; + this.headerbar.subtitle = Contacts.getDisplayNumber(contact, address); + } else { + const otherLength = addresses.length - 1; + + this.headerbar.title = contact.name; + this.headerbar.subtitle = ngettext( + 'And %d other contact', + 'And %d others', + otherLength + ).format(otherLength); + } + } + + _sync() { + this.device.contacts.fetch(); + this.plugin.connected(); + } + + _onNewConversation() { + this._sync(); + this.stack.set_visible_child_name('contact-chooser'); + this.thread_list.select_row(null); + this.contact_chooser.entry.has_focus = true; + } + + _onNumberSelected(chooser, number) { + const contacts = chooser.getSelected(); + const row = this._getRowForContacts(contacts); + + if (row) + row.emit('activate'); + else + this.setContacts(contacts); + } + + /** + * Threads + */ + _onThreadsChanged() { + // Get the last message in each thread + const messages = {}; + + for (const [thread_id, thread] of Object.entries(this.plugin.threads)) { + const message = thread[thread.length - 1]; + + // Skip messages without a body (eg. MMS messages without text) + if (message.body) + messages[thread_id] = thread[thread.length - 1]; + } + + // Update existing summaries and destroy old ones + for (const row of this.thread_list.get_children()) { + const message = messages[row.thread_id]; + + // If it's an existing conversation, update it + if (message) { + // Ensure there's a contact mapping + const sender = message.addresses[0].address || 'unknown'; + + if (row.contacts[sender] === undefined) { + row.contacts[sender] = this.device.contacts.query({ + number: sender, + }); + } + + row.message = message; + delete messages[row.thread_id]; + + // Otherwise destroy it + } else { + // Destroy the conversation widget + const conversation = this.stack.get_child_by_name(`${row.thread_id}`); + + if (conversation) { + conversation.destroy(); + imports.system.gc(); + } + + // Then the summary widget + row.destroy(); + // HACK: temporary mitigator for mysterious GtkListBox leak + imports.system.gc(); + } + } + + // What's left in the dictionary is new summaries + for (const message of Object.values(messages)) { + const contacts = this.device.contacts.lookupAddresses(message.addresses); + const conversation = new ConversationSummary(contacts, message); + this.thread_list.add(conversation); + } + + // Re-sort the summaries + this.thread_list.invalidate_sort(); + } + + // GtkListBox::row-activated + _onThreadSelected(box, row) { + // Show the conversation for this number (if applicable) + if (row) { + this.thread_id = row.thread_id; + + // Show the placeholder + } else { + this.headerbar.title = _('Messaging'); + this.headerbar.subtitle = this.device.name; + } + } + + _sortThreads(row1, row2) { + return (row1.date > row2.date) ? -1 : 1; + } + + _timestampThreads() { + if (this.visible) + this.thread_list.foreach(row => row.update()); + + return GLib.SOURCE_CONTINUE; + } + + /** + * Find the thread row for @contacts + * + * @param {Object[]} contacts - A contact group + * @return {ConversationSummary|null} The thread row or %null + */ + _getRowForContacts(contacts) { + const addresses = Object.keys(contacts).map(address => { + return {address: address}; + }); + + // Try to find a thread_id + const thread_id = this.plugin.getThreadIdForAddresses(addresses); + + for (const row of this.thread_list.get_children()) { + if (row.message.thread_id === thread_id) + return row; + } + + return null; + } + + setContacts(contacts) { + // Group the addresses + const addresses = []; + + for (const address of Object.keys(contacts)) + addresses.push({address: address}); + + // Try to find a thread ID for this address group + let thread_id = this.plugin.getThreadIdForAddresses(addresses); + + if (thread_id === null) + thread_id = GLib.uuid_string_random(); + else + thread_id = thread_id.toString(); + + // Try to find a thread row for the ID + const row = this._getRowForContacts(contacts); + + if (row !== null) { + this.thread_list.select_row(row); + return; + } + + // We're creating a new conversation + const conversation = new Conversation({ + device: this.device, + plugin: this.plugin, + addresses: addresses, + }); + + // Set the headerbar + this._setHeaderBar(addresses); + + // Select the conversation and entry active + this.stack.add_named(conversation, thread_id); + this.stack.visible_child = conversation; + this.stack.visible_child.entry.has_focus = true; + + // There was a pending message waiting for a conversation to be chosen + if (this._pendingShare) { + conversation.setMessage(this._pendingShare); + this._pendingShare = null; + } + + this._thread_id = thread_id; + this.notify('thread-id'); + } + + _includesAddress(addresses, addressObj) { + const number = addressObj.address.toPhoneNumber(); + + for (const haystackObj of addresses) { + const tnumber = haystackObj.address.toPhoneNumber(); + + if (number.endsWith(tnumber) || tnumber.endsWith(number)) + return true; + } + + return false; + } + + /** + * Try and find an existing conversation widget for @message. + * + * @param {Object} message - A message object + * @return {Conversation|null} A conversation widget or %null + */ + getConversationForMessage(message) { + // TODO: This shouldn't happen? + if (message === null) + return null; + + // First try to find a conversation by thread_id + const thread_id = `${message.thread_id}`; + const conversation = this.stack.get_child_by_name(thread_id); + + if (conversation !== null) + return conversation; + + // Try and find one by matching addresses, which is necessary if we've + // started a thread locally and haven't set the thread_id + const addresses = message.addresses; + + for (const conversation of this.stack.get_children()) { + if (conversation.addresses === undefined || + conversation.addresses.length !== addresses.length) + continue; + + const caddrs = conversation.addresses; + + // If we find a match, set `thread-id` on the conversation and the + // child property `name`. + if (addresses.every(addr => this._includesAddress(caddrs, addr))) { + conversation._thread_id = thread_id; + this.stack.child_set_property(conversation, 'name', thread_id); + + return conversation; + } + } + + return null; + } + + /** + * Set the contents of the message entry. If @pending is %false set the + * message of the currently selected conversation, otherwise mark the + * message to be set for the next selected conversation. + * + * @param {string} message - The message to place in the entry + * @param {boolean} pending - Wait for a conversation to be selected + */ + setMessage(message, pending = false) { + try { + if (pending) + this._pendingShare = message; + else + this.stack.visible_child.setMessage(message); + } catch (e) { + debug(e); + } + } +}); + + +/** + * A Gtk.ApplicationWindow for selecting from open conversations + */ +var ConversationChooser = GObject.registerClass({ + GTypeName: 'GSConnectConversationChooser', + Properties: { + 'device': GObject.ParamSpec.object( + 'device', + 'Device', + 'The device associated with this window', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'message': GObject.ParamSpec.string( + 'message', + 'Message', + 'The message to share', + GObject.ParamFlags.READWRITE, + '' + ), + 'plugin': GObject.ParamSpec.object( + 'plugin', + 'Plugin', + 'The plugin providing messages', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + }, +}, class ConversationChooser extends Gtk.ApplicationWindow { + + _init(params) { + super._init(Object.assign({ + title: _('Share Link'), + default_width: 300, + default_height: 200, + }, params)); + this.set_keep_above(true); + + // HeaderBar + this.headerbar = new Gtk.HeaderBar({ + title: _('Share Link'), + subtitle: this.message, + show_close_button: true, + tooltip_text: this.message, + }); + this.set_titlebar(this.headerbar); + + const newButton = new Gtk.Button({ + image: new Gtk.Image({icon_name: 'list-add-symbolic'}), + tooltip_text: _('New Conversation'), + always_show_image: true, + }); + newButton.connect('clicked', this._new.bind(this)); + this.headerbar.pack_start(newButton); + + // Threads + const scrolledWindow = new Gtk.ScrolledWindow({ + can_focus: false, + hexpand: true, + vexpand: true, + hscrollbar_policy: Gtk.PolicyType.NEVER, + }); + this.add(scrolledWindow); + + this.thread_list = new Gtk.ListBox({ + activate_on_single_click: false, + }); + this.thread_list.set_sort_func(Window.prototype._sortThreads); + this.thread_list.connect('row-activated', this._select.bind(this)); + scrolledWindow.add(this.thread_list); + + // Filter Setup + Window.prototype._onThreadsChanged.call(this); + this.show_all(); + } + + get plugin() { + return this._plugin || null; + } + + set plugin(plugin) { + this._plugin = plugin; + } + + _new(button) { + const message = this.message; + this.destroy(); + + this.plugin.sms(); + this.plugin.window._onNewConversation(); + this.plugin.window._pendingShare = message; + } + + _select(box, row) { + this.plugin.sms(); + this.plugin.window.thread_id = row.message.thread_id.toString(); + this.plugin.window.setMessage(this.message); + + this.destroy(); + } +}); + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/mousepad.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/mousepad.js new file mode 100644 index 0000000..3d2e3e7 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/mousepad.js @@ -0,0 +1,299 @@ +'use strict'; + +const Gdk = imports.gi.Gdk; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + + +/** + * A map of Gdk to "KDE Connect" keyvals + */ +const ReverseKeyMap = new Map([ + [Gdk.KEY_BackSpace, 1], + [Gdk.KEY_Tab, 2], + [Gdk.KEY_Linefeed, 3], + [Gdk.KEY_Left, 4], + [Gdk.KEY_Up, 5], + [Gdk.KEY_Right, 6], + [Gdk.KEY_Down, 7], + [Gdk.KEY_Page_Up, 8], + [Gdk.KEY_Page_Down, 9], + [Gdk.KEY_Home, 10], + [Gdk.KEY_End, 11], + [Gdk.KEY_Return, 12], + [Gdk.KEY_Delete, 13], + [Gdk.KEY_Escape, 14], + [Gdk.KEY_Sys_Req, 15], + [Gdk.KEY_Scroll_Lock, 16], + [Gdk.KEY_F1, 21], + [Gdk.KEY_F2, 22], + [Gdk.KEY_F3, 23], + [Gdk.KEY_F4, 24], + [Gdk.KEY_F5, 25], + [Gdk.KEY_F6, 26], + [Gdk.KEY_F7, 27], + [Gdk.KEY_F8, 28], + [Gdk.KEY_F9, 29], + [Gdk.KEY_F10, 30], + [Gdk.KEY_F11, 31], + [Gdk.KEY_F12, 32], +]); + + +/* + * A list of keyvals we consider modifiers + */ +const MOD_KEYS = [ + Gdk.KEY_Alt_L, + Gdk.KEY_Alt_R, + Gdk.KEY_Caps_Lock, + Gdk.KEY_Control_L, + Gdk.KEY_Control_R, + Gdk.KEY_Meta_L, + Gdk.KEY_Meta_R, + Gdk.KEY_Num_Lock, + Gdk.KEY_Shift_L, + Gdk.KEY_Shift_R, + Gdk.KEY_Super_L, + Gdk.KEY_Super_R, +]; + + +/* + * Some convenience functions for checking keyvals for modifiers + */ +const isAlt = (key) => [Gdk.KEY_Alt_L, Gdk.KEY_Alt_R].includes(key); +const isCtrl = (key) => [Gdk.KEY_Control_L, Gdk.KEY_Control_R].includes(key); +const isShift = (key) => [Gdk.KEY_Shift_L, Gdk.KEY_Shift_R].includes(key); +const isSuper = (key) => [Gdk.KEY_Super_L, Gdk.KEY_Super_R].includes(key); + + +var InputDialog = GObject.registerClass({ + GTypeName: 'GSConnectMousepadInputDialog', + Properties: { + 'device': GObject.ParamSpec.object( + 'device', + 'Device', + 'The device associated with this window', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'plugin': GObject.ParamSpec.object( + 'plugin', + 'Plugin', + 'The mousepad plugin associated with this window', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + }, + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/mousepad-input-dialog.ui', + Children: [ + 'infobar', 'infobar-label', + 'shift-label', 'ctrl-label', 'alt-label', 'super-label', 'entry', + ], +}, class InputDialog extends Gtk.Dialog { + + _init(params) { + super._init(Object.assign({ + use_header_bar: true, + }, params)); + + const headerbar = this.get_titlebar(); + headerbar.title = _('Keyboard'); + headerbar.subtitle = this.device.name; + + // Main Box + const content = this.get_content_area(); + content.border_width = 0; + + // TRANSLATORS: Displayed when the remote keyboard is not ready to accept input + this.infobar_label.label = _('Remote keyboard on %s is not active').format(this.device.name); + + // Text Input + this.entry.buffer.connect( + 'insert-text', + this._onInsertText.bind(this) + ); + + this.infobar.connect('notify::reveal-child', this._onState.bind(this)); + this.plugin.bind_property('state', this.infobar, 'reveal-child', 6); + + this.show_all(); + } + + vfunc_delete_event(event) { + this._ungrab(); + return this.hide_on_delete(); + } + + vfunc_grab_broken_event(event) { + if (event.keyboard) + this._ungrab(); + + return false; + } + + vfunc_key_release_event(event) { + if (!this.plugin.state) + debug('ignoring remote keyboard state'); + + const keyvalLower = Gdk.keyval_to_lower(event.keyval); + const realMask = event.state & Gtk.accelerator_get_default_mod_mask(); + + this.alt_label.sensitive = !isAlt(keyvalLower) && (realMask & Gdk.ModifierType.MOD1_MASK); + this.ctrl_label.sensitive = !isCtrl(keyvalLower) && (realMask & Gdk.ModifierType.CONTROL_MASK); + this.shift_label.sensitive = !isShift(keyvalLower) && (realMask & Gdk.ModifierType.SHIFT_MASK); + this.super_label.sensitive = !isSuper(keyvalLower) && (realMask & Gdk.ModifierType.SUPER_MASK); + + return super.vfunc_key_release_event(event); + } + + vfunc_key_press_event(event) { + if (!this.plugin.state) + debug('ignoring remote keyboard state'); + + let keyvalLower = Gdk.keyval_to_lower(event.keyval); + let realMask = event.state & Gtk.accelerator_get_default_mod_mask(); + + this.alt_label.sensitive = isAlt(keyvalLower) || (realMask & Gdk.ModifierType.MOD1_MASK); + this.ctrl_label.sensitive = isCtrl(keyvalLower) || (realMask & Gdk.ModifierType.CONTROL_MASK); + this.shift_label.sensitive = isShift(keyvalLower) || (realMask & Gdk.ModifierType.SHIFT_MASK); + this.super_label.sensitive = isSuper(keyvalLower) || (realMask & Gdk.ModifierType.SUPER_MASK); + + // Wait for a real key before sending + if (MOD_KEYS.includes(keyvalLower)) + return false; + + // Normalize Tab + if (keyvalLower === Gdk.KEY_ISO_Left_Tab) + keyvalLower = Gdk.KEY_Tab; + + // Put shift back if it changed the case of the key, not otherwise. + if (keyvalLower !== event.keyval) + realMask |= Gdk.ModifierType.SHIFT_MASK; + + // HACK: we don't want to use SysRq as a keybinding (but we do want + // Alt+Print), so we avoid translation from Alt+Print to SysRq + if (keyvalLower === Gdk.KEY_Sys_Req && (realMask & Gdk.ModifierType.MOD1_MASK) !== 0) + keyvalLower = Gdk.KEY_Print; + + // CapsLock isn't supported as a keybinding modifier, so keep it from + // confusing us + realMask &= ~Gdk.ModifierType.LOCK_MASK; + + if (keyvalLower === 0) + return false; + + debug(`keyval: ${event.keyval}, mask: ${realMask}`); + + const request = { + alt: !!(realMask & Gdk.ModifierType.MOD1_MASK), + ctrl: !!(realMask & Gdk.ModifierType.CONTROL_MASK), + shift: !!(realMask & Gdk.ModifierType.SHIFT_MASK), + super: !!(realMask & Gdk.ModifierType.SUPER_MASK), + sendAck: true, + }; + + // specialKey + if (ReverseKeyMap.has(event.keyval)) { + request.specialKey = ReverseKeyMap.get(event.keyval); + + // key + } else { + const codePoint = Gdk.keyval_to_unicode(event.keyval); + request.key = String.fromCodePoint(codePoint); + } + + this.device.sendPacket({ + type: 'kdeconnect.mousepad.request', + body: request, + }); + + // Pass these key combinations rather than using the echo reply + if (request.alt || request.ctrl || request.super) + return super.vfunc_key_press_event(event); + + return false; + } + + vfunc_window_state_event(event) { + if (!this.plugin.state) + debug('ignoring remote keyboard state'); + + if (event.new_window_state & Gdk.WindowState.FOCUSED) + this._grab(); + else + this._ungrab(); + + return super.vfunc_window_state_event(event); + } + + _onInsertText(buffer, location, text, len) { + if (this._isAck) + return; + + debug(`insert-text: ${text} (chars ${[...text].length})`); + + for (const char of [...text]) { + if (!char) + continue; + + // TODO: modifiers? + this.device.sendPacket({ + type: 'kdeconnect.mousepad.request', + body: { + alt: false, + ctrl: false, + shift: false, + super: false, + sendAck: false, + key: char, + }, + }); + } + } + + _onState(widget) { + if (!this.plugin.state) + debug('ignoring remote keyboard state'); + + if (this.is_active) + this._grab(); + else + this._ungrab(); + } + + _grab() { + if (!this.visible || this._keyboard) + return; + + const seat = Gdk.Display.get_default().get_default_seat(); + const status = seat.grab( + this.get_window(), + Gdk.SeatCapabilities.KEYBOARD, + false, + null, + null, + null + ); + + if (status !== Gdk.GrabStatus.SUCCESS) { + logError(new Error('Grabbing keyboard failed')); + return; + } + + this._keyboard = seat.get_keyboard(); + this.grab_add(); + this.entry.has_focus = true; + } + + _ungrab() { + if (this._keyboard) { + this._keyboard.get_seat().ungrab(); + this._keyboard = null; + this.grab_remove(); + } + + this.entry.buffer.text = ''; + } +}); diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/notification.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/notification.js new file mode 100644 index 0000000..c1d1f8a --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/notification.js @@ -0,0 +1,174 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + +const URI = imports.service.utils.uri; + + +/** + * A dialog for repliable notifications. + */ +var ReplyDialog = GObject.registerClass({ + GTypeName: 'GSConnectNotificationReplyDialog', + Properties: { + 'device': GObject.ParamSpec.object( + 'device', + 'Device', + 'The device associated with this window', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'plugin': GObject.ParamSpec.object( + 'plugin', + 'Plugin', + 'The plugin that owns this notification', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'uuid': GObject.ParamSpec.string( + 'uuid', + 'UUID', + 'The notification reply UUID', + GObject.ParamFlags.READWRITE, + null + ), + }, + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/notification-reply-dialog.ui', + Children: ['infobar', 'notification-title', 'notification-body', 'entry'], +}, class ReplyDialog extends Gtk.Dialog { + + _init(params) { + super._init({ + application: Gio.Application.get_default(), + device: params.device, + plugin: params.plugin, + uuid: params.uuid, + use_header_bar: true, + }); + + this.set_response_sensitive(Gtk.ResponseType.OK, false); + + // Info bar + this.device.bind_property( + 'connected', + this.infobar, + 'reveal-child', + GObject.BindingFlags.INVERT_BOOLEAN + ); + + // Notification Data + const headerbar = this.get_titlebar(); + headerbar.title = params.notification.appName; + headerbar.subtitle = this.device.name; + + this.notification_title.label = params.notification.title; + this.notification_body.label = URI.linkify(params.notification.text); + + // Message Entry/Send Button + this.device.bind_property( + 'connected', + this.entry, + 'sensitive', + GObject.BindingFlags.DEFAULT + ); + + this._connectedId = this.device.connect( + 'notify::connected', + this._onStateChanged.bind(this) + ); + + this._entryChangedId = this.entry.buffer.connect( + 'changed', + this._onStateChanged.bind(this) + ); + + this.restoreGeometry('notification-reply-dialog'); + + this.connect('destroy', this._onDestroy); + } + + _onDestroy(dialog) { + dialog.entry.buffer.disconnect(dialog._entryChangedId); + dialog.device.disconnect(dialog._connectedId); + } + + vfunc_delete_event() { + this.saveGeometry(); + + return false; + } + + vfunc_response(response_id) { + if (response_id === Gtk.ResponseType.OK) { + // Refuse to send empty or whitespace only messages + if (!this.entry.buffer.text.trim()) + return; + + this.plugin.replyNotification( + this.uuid, + this.entry.buffer.text + ); + } + + this.destroy(); + } + + get device() { + if (this._device === undefined) + this._device = null; + + return this._device; + } + + set device(device) { + this._device = device; + } + + get plugin() { + if (this._plugin === undefined) + this._plugin = null; + + return this._plugin; + } + + set plugin(plugin) { + this._plugin = plugin; + } + + get uuid() { + if (this._uuid === undefined) + this._uuid = null; + + return this._uuid; + } + + set uuid(uuid) { + this._uuid = uuid; + + // We must have a UUID + if (!uuid) { + this.destroy(); + debug('no uuid for repliable notification'); + } + } + + _onActivateLink(label, uri) { + Gtk.show_uri_on_window( + this.get_toplevel(), + uri.includes('://') ? uri : `https://${uri}`, + Gtk.get_current_event_time() + ); + + return true; + } + + _onStateChanged() { + if (this.device.connected && this.entry.buffer.text.trim()) + this.set_response_sensitive(Gtk.ResponseType.OK, true); + else + this.set_response_sensitive(Gtk.ResponseType.OK, false); + } +}); + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/service.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/service.js new file mode 100644 index 0000000..fa02710 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/ui/service.js @@ -0,0 +1,248 @@ +'use strict'; + +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + +const Config = imports.config; + + +/* + * Issue Header + */ +const ISSUE_HEADER = ` +GSConnect: ${Config.PACKAGE_VERSION} (${Config.IS_USER ? 'user' : 'system'}) +GJS: ${imports.system.version} +Session: ${GLib.getenv('XDG_SESSION_TYPE')} +OS: ${GLib.get_os_info('PRETTY_NAME')} +`; + + +/** + * A dialog for selecting a device + */ +var DeviceChooser = GObject.registerClass({ + GTypeName: 'GSConnectServiceDeviceChooser', + Properties: { + 'action-name': GObject.ParamSpec.string( + 'action-name', + 'Action Name', + 'The name of the associated action, like "sendFile"', + GObject.ParamFlags.READWRITE, + null + ), + 'action-target': GObject.param_spec_variant( + 'action-target', + 'Action Target', + 'The parameter for action invocations', + new GLib.VariantType('*'), + null, + GObject.ParamFlags.READWRITE + ), + }, + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/service-device-chooser.ui', + Children: ['device-list', 'cancel-button', 'select-button'], +}, class DeviceChooser extends Gtk.Dialog { + + _init(params = {}) { + super._init({ + use_header_bar: true, + application: Gio.Application.get_default(), + }); + this.set_keep_above(true); + + // HeaderBar + this.get_header_bar().subtitle = params.title; + + // Dialog Action + this.action_name = params.action_name; + this.action_target = params.action_target; + + // Device List + this.device_list.set_sort_func(this._sortDevices); + + this._devicesChangedId = this.application.settings.connect( + 'changed::devices', + this._onDevicesChanged.bind(this) + ); + this._onDevicesChanged(); + } + + vfunc_response(response_id) { + if (response_id === Gtk.ResponseType.OK) { + try { + const device = this.device_list.get_selected_row().device; + device.activate_action(this.action_name, this.action_target); + } catch (e) { + logError(e); + } + } + + this.destroy(); + } + + get action_name() { + if (this._action_name === undefined) + this._action_name = null; + + return this._action_name; + } + + set action_name(name) { + this._action_name = name; + } + + get action_target() { + if (this._action_target === undefined) + this._action_target = null; + + return this._action_target; + } + + set action_target(variant) { + this._action_target = variant; + } + + _onDeviceActivated(box, row) { + this.response(Gtk.ResponseType.OK); + } + + _onDeviceSelected(box) { + this.set_response_sensitive( + Gtk.ResponseType.OK, + (box.get_selected_row()) + ); + } + + _onDevicesChanged() { + // Collect known devices + const devices = {}; + + for (const [id, device] of this.application.manager.devices.entries()) + devices[id] = device; + + // Prune device rows + this.device_list.foreach(row => { + if (!devices.hasOwnProperty(row.name)) + row.destroy(); + else + delete devices[row.name]; + }); + + // Add new devices + for (const device of Object.values(devices)) { + const action = device.lookup_action(this.action_name); + + if (action === null) + continue; + + const row = new Gtk.ListBoxRow({ + visible: action.enabled, + }); + row.set_name(device.id); + row.device = device; + + action.bind_property( + 'enabled', + row, + 'visible', + Gio.SettingsBindFlags.DEFAULT + ); + + const grid = new Gtk.Grid({ + column_spacing: 12, + margin: 6, + visible: true, + }); + row.add(grid); + + const icon = new Gtk.Image({ + icon_name: device.icon_name, + pixel_size: 32, + visible: true, + }); + grid.attach(icon, 0, 0, 1, 1); + + const name = new Gtk.Label({ + label: device.name, + halign: Gtk.Align.START, + hexpand: true, + visible: true, + }); + grid.attach(name, 1, 0, 1, 1); + + this.device_list.add(row); + } + + if (this.device_list.get_selected_row() === null) + this.device_list.select_row(this.device_list.get_row_at_index(0)); + } + + _sortDevices(row1, row2) { + return row1.device.name.localeCompare(row2.device.name); + } +}); + + +/** + * A dialog for reporting an error. + */ +var ErrorDialog = GObject.registerClass({ + GTypeName: 'GSConnectServiceErrorDialog', + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/service-error-dialog.ui', + Children: [ + 'error-stack', + 'expander-arrow', + 'gesture', + 'report-button', + 'revealer', + ], +}, class ErrorDialog extends Gtk.Window { + + _init(error) { + super._init({ + application: Gio.Application.get_default(), + title: `GSConnect: ${error.name}`, + }); + this.set_keep_above(true); + + this.error = error; + this.error_stack.buffer.text = `${error.message}\n\n${error.stack}`; + this.gesture.connect('released', this._onReleased.bind(this)); + } + + _onClicked(button) { + if (this.report_button === button) { + const uri = this._buildUri(this.error.message, this.error.stack); + Gio.AppInfo.launch_default_for_uri_async(uri, null, null, null); + } + + this.destroy(); + } + + _onReleased(gesture, n_press) { + if (n_press === 1) + this.revealer.reveal_child = !this.revealer.reveal_child; + } + + _onRevealChild(revealer, pspec) { + this.expander_arrow.icon_name = this.revealer.reveal_child + ? 'pan-down-symbolic' + : 'pan-end-symbolic'; + } + + _buildUri(message, stack) { + const body = `\`\`\`${ISSUE_HEADER}\n${stack}\n\`\`\``; + const titleQuery = encodeURIComponent(message).replace('%20', '+'); + const bodyQuery = encodeURIComponent(body).replace('%20', '+'); + const uri = `${Config.PACKAGE_BUGREPORT}?title=${titleQuery}&body=${bodyQuery}`; + + // Reasonable URI length limit + if (uri.length > 2000) + return uri.substr(0, 2000); + + return uri; + } +}); + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/utils/dbus.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/utils/dbus.js new file mode 100644 index 0000000..ab8f6fd --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/utils/dbus.js @@ -0,0 +1,279 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GjsPrivate = imports.gi.GjsPrivate; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + + +/* + * Some utility methods + */ +function toDBusCase(string) { + return string.replace(/(?:^\w|[A-Z]|\b\w)/g, (ltr, offset) => { + return ltr.toUpperCase(); + }).replace(/[\s_-]+/g, ''); +} + +function toHyphenCase(string) { + return string.replace(/(?:[A-Z])/g, (ltr, offset) => { + return (offset > 0) ? `-${ltr.toLowerCase()}` : ltr.toLowerCase(); + }).replace(/[\s_]+/g, ''); +} + +function toUnderscoreCase(string) { + return string.replace(/(?:^\w|[A-Z]|_|\b\w)/g, (ltr, offset) => { + if (ltr === '_') + return ''; + + return (offset > 0) ? `_${ltr.toLowerCase()}` : ltr.toLowerCase(); + }).replace(/[\s-]+/g, ''); +} + + +/** + * DBus.Interface represents a DBus interface bound to an object instance, meant + * to be exported over DBus. + */ +var Interface = GObject.registerClass({ + GTypeName: 'GSConnectDBusInterface', + Implements: [Gio.DBusInterface], + Properties: { + 'g-instance': GObject.ParamSpec.object( + 'g-instance', + 'Instance', + 'The delegate GObject', + GObject.ParamFlags.READWRITE, + GObject.Object.$gtype + ), + }, +}, class Interface extends GjsPrivate.DBusImplementation { + + _init(params) { + super._init({ + g_instance: params.g_instance, + g_interface_info: params.g_interface_info, + }); + + // Cache member lookups + this._instanceHandlers = []; + this._instanceMethods = {}; + this._instanceProperties = {}; + + const info = this.get_info(); + this.connect('handle-method-call', this._call.bind(this._instance, info)); + this.connect('handle-property-get', this._get.bind(this._instance, info)); + this.connect('handle-property-set', this._set.bind(this._instance, info)); + + // Automatically forward known signals + const id = this._instance.connect('notify', this._notify.bind(this)); + this._instanceHandlers.push(id); + + for (const signal of info.signals) { + const type = `(${signal.args.map(arg => arg.signature).join('')})`; + const id = this._instance.connect( + signal.name, + this._emit.bind(this, signal.name, type) + ); + + this._instanceHandlers.push(id); + } + + // Export if connection and object path were given + if (params.g_connection && params.g_object_path) + this.export(params.g_connection, params.g_object_path); + } + + get g_instance() { + if (this._instance === undefined) + this._instance = null; + + return this._instance; + } + + set g_instance(instance) { + this._instance = instance; + } + + /** + * Invoke an instance's method for a DBus method call. + * + * @param {Gio.DBusInterfaceInfo} info - The DBus interface + * @param {DBus.Interface} iface - The DBus interface + * @param {string} name - The DBus method name + * @param {GLib.Variant} parameters - The method parameters + * @param {Gio.DBusMethodInvocation} invocation - The method invocation info + */ + async _call(info, iface, name, parameters, invocation) { + let retval; + + // Invoke the instance method + try { + const args = parameters.unpack().map(parameter => { + if (parameter.get_type_string() === 'h') { + const message = invocation.get_message(); + const fds = message.get_unix_fd_list(); + const idx = parameter.deepUnpack(); + return fds.get(idx); + } else { + return parameter.recursiveUnpack(); + } + }); + + retval = await this[name](...args); + } catch (e) { + if (e instanceof GLib.Error) { + invocation.return_gerror(e); + } else { + // likely to be a normal JS error + if (!e.name.includes('.')) + e.name = `org.gnome.gjs.JSError.${e.name}`; + + invocation.return_dbus_error(e.name, e.message); + } + + logError(e, `${this}: ${name}`); + return; + } + + // `undefined` is an empty tuple on DBus + if (retval === undefined) + retval = new GLib.Variant('()', []); + + // Return the instance result or error + try { + if (!(retval instanceof GLib.Variant)) { + const args = info.lookup_method(name).out_args; + retval = new GLib.Variant( + `(${args.map(arg => arg.signature).join('')})`, + (args.length === 1) ? [retval] : retval + ); + } + + invocation.return_value(retval); + } catch (e) { + invocation.return_dbus_error( + 'org.gnome.gjs.JSError.ValueError', + 'Service implementation returned an incorrect value type' + ); + + logError(e, `${this}: ${name}`); + } + } + + _nativeProp(obj, name) { + if (this._instanceProperties[name] === undefined) { + let propName = name; + + if (propName in obj) + this._instanceProperties[name] = propName; + + if (this._instanceProperties[name] === undefined) { + propName = toUnderscoreCase(name); + + if (propName in obj) + this._instanceProperties[name] = propName; + } + } + + return this._instanceProperties[name]; + } + + _emit(name, type, obj, ...args) { + this.emit_signal(name, new GLib.Variant(type, args)); + } + + _get(info, iface, name) { + const nativeValue = this[iface._nativeProp(this, name)]; + const propertyInfo = info.lookup_property(name); + + if (nativeValue === undefined || propertyInfo === null) + return null; + + return new GLib.Variant(propertyInfo.signature, nativeValue); + } + + _set(info, iface, name, value) { + const nativeValue = value.recursiveUnpack(); + + this[iface._nativeProp(this, name)] = nativeValue; + } + + _notify(obj, pspec) { + const name = toDBusCase(pspec.name); + const propertyInfo = this.get_info().lookup_property(name); + + if (propertyInfo === null) + return; + + this.emit_property_changed( + name, + new GLib.Variant( + propertyInfo.signature, + // Adjust for GJS's '-'/'_' conversion + this._instance[pspec.name.replace(/-/gi, '_')] + ) + ); + } + + destroy() { + try { + for (const id of this._instanceHandlers) + this._instance.disconnect(id); + this._instanceHandlers = []; + + this.flush(); + this.unexport(); + } catch (e) { + logError(e); + } + } +}); + + +/** + * Get the DBus connection on @busType + * + * @param {Gio.BusType} [busType] - a Gio.BusType constant + * @param {Gio.Cancellable} [cancellable] - an optional Gio.Cancellable + * @return {Promise<Gio.DBusConnection>} A DBus connection + */ +function getConnection(busType = Gio.BusType.SESSION, cancellable = null) { + return new Promise((resolve, reject) => { + Gio.bus_get(busType, cancellable, (connection, res) => { + try { + resolve(Gio.bus_get_finish(res)); + } catch (e) { + reject(e); + } + }); + }); +} + +/** + * Get a new, dedicated DBus connection on @busType + * + * @param {Gio.BusType} [busType] - a Gio.BusType constant + * @param {Gio.Cancellable} [cancellable] - an optional Gio.Cancellable + * @return {Promise<Gio.DBusConnection>} A new DBus connection + */ +function newConnection(busType = Gio.BusType.SESSION, cancellable = null) { + return new Promise((resolve, reject) => { + Gio.DBusConnection.new_for_address( + Gio.dbus_address_get_for_bus_sync(busType, cancellable), + Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT | + Gio.DBusConnectionFlags.MESSAGE_BUS_CONNECTION, + null, + cancellable, + (connection, res) => { + try { + resolve(Gio.DBusConnection.new_for_address_finish(res)); + } catch (e) { + reject(e); + } + } + ); + + }); +} + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/utils/uri.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/utils/uri.js new file mode 100644 index 0000000..4741f64 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/utils/uri.js @@ -0,0 +1,167 @@ +'use strict'; + +const GLib = imports.gi.GLib; + + +/** + * The same regular expression used in GNOME Shell + * + * http://daringfireball.net/2010/07/improved_regex_for_matching_urls + */ +const _balancedParens = '\\((?:[^\\s()<>]+|(?:\\(?:[^\\s()<>]+\\)))*\\)'; +const _leadingJunk = '[\\s`(\\[{\'\\"<\u00AB\u201C\u2018]'; +const _notTrailingJunk = '[^\\s`!()\\[\\]{};:\'\\".,<>?\u00AB\u00BB\u201C\u201D\u2018\u2019]'; + +const _urlRegexp = new RegExp( + '(^|' + _leadingJunk + ')' + + '(' + + '(?:' + + '(?:http|https)://' + // scheme:// + '|' + + 'www\\d{0,3}[.]' + // www. + '|' + + '[a-z0-9.\\-]+[.][a-z]{2,4}/' + // foo.xx/ + ')' + + '(?:' + // one or more: + '[^\\s()<>]+' + // run of non-space non-() + '|' + // or + _balancedParens + // balanced parens + ')+' + + '(?:' + // end with: + _balancedParens + // balanced parens + '|' + // or + _notTrailingJunk + // last non-junk char + ')' + + ')', 'gi'); + + +/** + * sms/tel URI RegExp (https://tools.ietf.org/html/rfc5724) + * + * A fairly lenient regexp for sms: URIs that allows tel: numbers with chars + * from global-number, local-number (without phone-context) and single spaces. + * This allows passing numbers directly from libfolks or GData without + * pre-processing. It also makes an allowance for URIs passed from Gio.File + * that always come in the form "sms:///". + */ +const _smsParam = "[\\w.!~*'()-]+=(?:[\\w.!~*'()-]|%[0-9A-F]{2})*"; +const _telParam = ";[a-zA-Z0-9-]+=(?:[\\w\\[\\]/:&+$.!~*'()-]|%[0-9A-F]{2})+"; +const _lenientDigits = '[+]?(?:[0-9A-F*#().-]| (?! )|%20(?!%20))+'; +const _lenientNumber = `${_lenientDigits}(?:${_telParam})*`; + +const _smsRegex = new RegExp( + '^' + + 'sms:' + // scheme + '(?:[/]{2,3})?' + // Gio.File returns ":///" + '(' + // one or more... + _lenientNumber + // phone numbers + '(?:,' + _lenientNumber + ')*' + // separated by commas + ')' + + '(?:\\?(' + // followed by optional... + _smsParam + // parameters... + '(?:&' + _smsParam + ')*' + // separated by "&" (unescaped) + '))?' + + '$', 'g'); // fragments (#foo) not allowed + + +const _numberRegex = new RegExp( + '^' + + '(' + _lenientDigits + ')' + // phone number digits + '((?:' + _telParam + ')*)' + // followed by optional parameters + '$', 'g'); + + +/** + * Searches @str for URLs and returns an array of objects with %url + * properties showing the matched URL string, and %pos properties indicating + * the position within @str where the URL was found. + * + * @param {string} str - the string to search + * @return {Object[]} the list of match objects, as described above + */ +function findUrls(str) { + _urlRegexp.lastIndex = 0; + + const res = []; + let match; + + while ((match = _urlRegexp.exec(str))) { + const name = match[2]; + const url = GLib.uri_parse_scheme(name) ? name : `http://${name}`; + res.push({name, url, pos: match.index + match[1].length}); + } + + return res; +} + + +/** + * Return a string with URLs couched in <a> tags, parseable by Pango and + * using the same RegExp as GNOME Shell. + * + * @param {string} str - The string to be modified + * @param {string} [title] - An optional title (eg. alt text, tooltip) + * @return {string} the modified text + */ +function linkify(str, title = null) { + const text = GLib.markup_escape_text(str, -1); + + _urlRegexp.lastIndex = 0; + + if (title) { + return text.replace( + _urlRegexp, + `$1<a href="$2" title="${title}">$2</a>` + ); + } else { + return text.replace(_urlRegexp, '$1<a href="$2">$2</a>'); + } +} + + +/** + * A simple parsing class for sms: URI's (https://tools.ietf.org/html/rfc5724) + */ +var SmsURI = class URI { + constructor(uri) { + _smsRegex.lastIndex = 0; + const [, recipients, query] = _smsRegex.exec(uri); + + this.recipients = recipients.split(',').map(recipient => { + _numberRegex.lastIndex = 0; + const [, number, params] = _numberRegex.exec(recipient); + + if (params) { + for (const param of params.substr(1).split(';')) { + const [key, value] = param.split('='); + + // add phone-context to beginning of + if (key === 'phone-context' && value.startsWith('+')) + return value + unescape(number); + } + } + + return unescape(number); + }); + + if (query) { + for (const field of query.split('&')) { + const [key, value] = field.split('='); + + if (key === 'body') { + if (this.body) + throw URIError('duplicate "body" field'); + + this.body = value ? decodeURIComponent(value) : undefined; + } + } + } + } + + toString() { + const uri = `sms:${this.recipients.join(',')}`; + + return this.body ? `${uri}?body=${escape(this.body)}` : uri; + } +}; + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/__init__.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/__init__.js new file mode 100644 index 0000000..9d4c0c2 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/__init__.js @@ -0,0 +1,43 @@ +'use strict'; + +const Gettext = imports.gettext; + +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; +const Gtk = imports.gi.Gtk; + +const Extension = imports.misc.extensionUtils.getCurrentExtension(); +const Config = Extension.imports.config; +Config.PACKAGE_DATADIR = Extension.path; + + +// Ensure config.js is setup properly +const userDir = GLib.build_filenamev([GLib.get_user_data_dir(), 'gnome-shell']); + +if (Config.PACKAGE_DATADIR.startsWith(userDir)) { + Config.IS_USER = true; + + Config.GSETTINGS_SCHEMA_DIR = `${Extension.path}/schemas`; + Config.PACKAGE_LOCALEDIR = `${Extension.path}/locale`; +} + + +// Init Gettext +Gettext.bindtextdomain(Config.APP_ID, Config.PACKAGE_LOCALEDIR); +Extension._ = GLib.dgettext.bind(null, Config.APP_ID); +Extension.ngettext = GLib.dngettext.bind(null, Config.APP_ID); + + +// Init GResources +Gio.Resource.load( + GLib.build_filenamev([Config.PACKAGE_DATADIR, `${Config.APP_ID}.gresource`]) +)._register(); + + +// Init GSchema +Config.GSCHEMA = Gio.SettingsSchemaSource.new_from_directory( + Config.GSETTINGS_SCHEMA_DIR, + Gio.SettingsSchemaSource.get_default(), + false +); + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/clipboard.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/clipboard.js new file mode 100644 index 0000000..2bc0b70 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/clipboard.js @@ -0,0 +1,380 @@ +'use strict'; + +const ByteArray = imports.byteArray; + +const Gio = imports.gi.Gio; +const GjsPrivate = imports.gi.GjsPrivate; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Meta = imports.gi.Meta; + + +/* + * DBus Interface Info + */ +const DBUS_NAME = 'org.gnome.Shell.Extensions.GSConnect.Clipboard'; +const DBUS_PATH = '/org/gnome/Shell/Extensions/GSConnect/Clipboard'; +const DBUS_NODE = Gio.DBusNodeInfo.new_for_xml(` +<node> + <interface name="org.gnome.Shell.Extensions.GSConnect.Clipboard"> + <!-- Methods --> + <method name="GetMimetypes"> + <arg direction="out" type="as" name="mimetypes"/> + </method> + <method name="GetText"> + <arg direction="out" type="s" name="text"/> + </method> + <method name="SetText"> + <arg direction="in" type="s" name="text"/> + </method> + <method name="GetValue"> + <arg direction="in" type="s" name="mimetype"/> + <arg direction="out" type="ay" name="value"/> + </method> + <method name="SetValue"> + <arg direction="in" type="ay" name="value"/> + <arg direction="in" type="s" name="mimetype"/> + </method> + + <!-- Signals --> + <signal name="OwnerChange"/> + </interface> +</node> +`); +const DBUS_INFO = DBUS_NODE.lookup_interface(DBUS_NAME); + + +/* + * Text Mimetypes + */ +const TEXT_MIMETYPES = [ + 'text/plain;charset=utf-8', + 'UTF8_STRING', + 'text/plain', + 'STRING', +]; + + +/* GSConnectClipboardPortal: + * + * A simple clipboard portal, especially useful on Wayland where GtkClipboard + * doesn't work in the background. + */ +var Clipboard = GObject.registerClass({ + GTypeName: 'GSConnectShellClipboard', +}, class GSConnectShellClipboard extends GjsPrivate.DBusImplementation { + + _init(params = {}) { + super._init({ + g_interface_info: DBUS_INFO, + }); + + this._transferring = false; + + // Watch global selection + this._selection = global.display.get_selection(); + this._ownerChangedId = this._selection.connect( + 'owner-changed', + this._onOwnerChanged.bind(this) + ); + + // Prepare DBus interface + this._handleMethodCallId = this.connect( + 'handle-method-call', + this._onHandleMethodCall.bind(this) + ); + + this._nameId = Gio.DBus.own_name( + Gio.BusType.SESSION, + DBUS_NAME, + Gio.BusNameOwnerFlags.NONE, + this._onBusAcquired.bind(this), + null, + this._onNameLost.bind(this) + ); + } + + _onOwnerChanged(selection, type, source) { + /* We're only interested in the standard clipboard */ + if (type !== Meta.SelectionType.SELECTION_CLIPBOARD) + return; + + /* In Wayland an intermediate GMemoryOutputStream is used which triggers + * a second ::owner-changed emission, so we need to ensure we ignore + * that while the transfer is resolving. + */ + if (this._transferring) + return; + + this._transferring = true; + + /* We need to put our signal emission in an idle callback to ensure that + * Mutter's internal calls have finished resolving in the loop, or else + * we'll end up with the previous selection's content. + */ + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + this.emit_signal('OwnerChange', null); + this._transferring = false; + + return GLib.SOURCE_REMOVE; + }); + } + + _onBusAcquired(connection, name) { + try { + this.export(connection, DBUS_PATH); + } catch (e) { + logError(e); + } + } + + _onNameLost(connection, name) { + try { + this.unexport(); + } catch (e) { + logError(e); + } + } + + async _onHandleMethodCall(iface, name, parameters, invocation) { + let retval; + + try { + const args = parameters.recursiveUnpack(); + + retval = await this[name](...args); + } catch (e) { + if (e instanceof GLib.Error) { + invocation.return_gerror(e); + } else { + if (!e.name.includes('.')) + e.name = `org.gnome.gjs.JSError.${e.name}`; + + invocation.return_dbus_error(e.name, e.message); + } + + return; + } + + if (retval === undefined) + retval = new GLib.Variant('()', []); + + try { + if (!(retval instanceof GLib.Variant)) { + const args = DBUS_INFO.lookup_method(name).out_args; + retval = new GLib.Variant( + `(${args.map(arg => arg.signature).join('')})`, + (args.length === 1) ? [retval] : retval + ); + } + + invocation.return_value(retval); + + // Without a response, the client will wait for timeout + } catch (e) { + invocation.return_dbus_error( + 'org.gnome.gjs.JSError.ValueError', + 'Service implementation returned an incorrect value type' + ); + } + } + + /** + * Get the available mimetypes of the current clipboard content + * + * @return {Promise<string[]>} A list of mime-types + */ + GetMimetypes() { + return new Promise((resolve, reject) => { + try { + const mimetypes = this._selection.get_mimetypes( + Meta.SelectionType.SELECTION_CLIPBOARD + ); + + resolve(mimetypes); + } catch (e) { + reject(e); + } + }); + } + + /** + * Get the text content of the clipboard + * + * @return {Promise<string>} Text content of the clipboard + */ + GetText() { + return new Promise((resolve, reject) => { + const mimetypes = this._selection.get_mimetypes( + Meta.SelectionType.SELECTION_CLIPBOARD); + + const mimetype = mimetypes.find(type => TEXT_MIMETYPES.includes(type)); + + if (mimetype !== undefined) { + const stream = Gio.MemoryOutputStream.new_resizable(); + + this._selection.transfer_async( + Meta.SelectionType.SELECTION_CLIPBOARD, + mimetype, -1, + stream, null, + (selection, res) => { + try { + selection.transfer_finish(res); + + const bytes = stream.steal_as_bytes(); + const bytearray = bytes.get_data(); + + resolve(ByteArray.toString(bytearray)); + } catch (e) { + reject(e); + } + } + ); + } else { + reject(new Error('text not available')); + } + }); + } + + /** + * Set the text content of the clipboard + * + * @param {string} text - text content to set + * @return {Promise} A promise for the operation + */ + SetText(text) { + return new Promise((resolve, reject) => { + try { + if (typeof text !== 'string') { + throw new Gio.DBusError({ + code: Gio.DBusError.INVALID_ARGS, + message: 'expected string', + }); + } + + const source = Meta.SelectionSourceMemory.new( + 'text/plain;charset=utf-8', GLib.Bytes.new(text)); + + this._selection.set_owner( + Meta.SelectionType.SELECTION_CLIPBOARD, source); + + resolve(); + } catch (e) { + reject(e); + } + }); + } + + /** + * Get the content of the clipboard with the type @mimetype. + * + * @param {string} mimetype - the mimetype to request + * @return {Promise<Uint8Array>} The content of the clipboard + */ + GetValue(mimetype) { + return new Promise((resolve, reject) => { + const stream = Gio.MemoryOutputStream.new_resizable(); + + this._selection.transfer_async( + Meta.SelectionType.SELECTION_CLIPBOARD, + mimetype, -1, + stream, null, + (selection, res) => { + try { + selection.transfer_finish(res); + + const bytes = stream.steal_as_bytes(); + + resolve(bytes.get_data()); + } catch (e) { + reject(e); + } + } + ); + }); + } + + /** + * Set the content of the clipboard to @value with the type @mimetype. + * + * @param {Uint8Array} value - the value to set + * @param {string} mimetype - the mimetype of the value + * @return {Promise} - A promise for the operation + */ + SetValue(value, mimetype) { + return new Promise((resolve, reject) => { + try { + const source = Meta.SelectionSourceMemory.new(mimetype, + GLib.Bytes.new(value)); + + this._selection.set_owner( + Meta.SelectionType.SELECTION_CLIPBOARD, source); + + resolve(); + } catch (e) { + reject(e); + } + }); + } + + destroy() { + if (this._selection && this._ownerChangedId > 0) { + this._selection.disconnect(this._ownerChangedId); + this._ownerChangedId = 0; + } + + if (this._nameId > 0) { + Gio.bus_unown_name(this._nameId); + this._nameId = 0; + } + + if (this._handleMethodCallId > 0) { + this.disconnect(this._handleMethodCallId); + this._handleMethodCallId = 0; + this.unexport(); + } + } +}); + + +var _portal = null; +var _portalId = 0; + +/** + * Watch for the service to start and export the clipboard portal when it does. + */ +function watchService() { + if (GLib.getenv('XDG_SESSION_TYPE') !== 'wayland') + return; + + if (_portalId > 0) + return; + + _portalId = Gio.bus_watch_name( + Gio.BusType.SESSION, + 'org.gnome.Shell.Extensions.GSConnect', + Gio.BusNameWatcherFlags.NONE, + () => { + if (_portal === null) + _portal = new Clipboard(); + }, + () => { + if (_portal !== null) { + _portal.destroy(); + _portal = null; + } + } + ); +} + +/** + * Stop watching the service and export the portal if currently running. + */ +function unwatchService() { + if (_portalId > 0) { + Gio.bus_unwatch_name(_portalId); + _portalId = 0; + } +} + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/device.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/device.js new file mode 100644 index 0000000..2bacd51 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/device.js @@ -0,0 +1,379 @@ +'use strict'; + +const Clutter = imports.gi.Clutter; +const GObject = imports.gi.GObject; +const St = imports.gi.St; + +const PanelMenu = imports.ui.panelMenu; +const PopupMenu = imports.ui.popupMenu; + +const Extension = imports.misc.extensionUtils.getCurrentExtension(); + +// eslint-disable-next-line no-redeclare +const _ = Extension._; +const GMenu = Extension.imports.shell.gmenu; +const Tooltip = Extension.imports.shell.tooltip; + + +/** + * A battery widget with an icon, text percentage and time estimate tooltip + */ +var Battery = GObject.registerClass({ + GTypeName: 'GSConnectShellDeviceBattery', +}, class Battery extends St.BoxLayout { + + _init(params) { + super._init({ + reactive: true, + style_class: 'gsconnect-device-battery', + track_hover: true, + }); + Object.assign(this, params); + + // Percent Label + this.label = new St.Label({ + y_align: Clutter.ActorAlign.CENTER, + }); + this.label.clutter_text.ellipsize = 0; + this.add_child(this.label); + + // Battery Icon + this.icon = new St.Icon({ + fallback_icon_name: 'battery-missing-symbolic', + icon_size: 16, + }); + this.add_child(this.icon); + + // Battery Estimate + this.tooltip = new Tooltip.Tooltip({ + parent: this, + text: null, + }); + + // Battery GAction + this._actionAddedId = this.device.action_group.connect( + 'action-added', + this._onActionChanged.bind(this) + ); + this._actionRemovedId = this.device.action_group.connect( + 'action-removed', + this._onActionChanged.bind(this) + ); + this._actionStateChangedId = this.device.action_group.connect( + 'action-state-changed', + this._onStateChanged.bind(this) + ); + + this._onActionChanged(this.device.action_group, 'battery'); + + // Cleanup on destroy + this.connect('destroy', this._onDestroy); + } + + _onActionChanged(action_group, action_name) { + if (action_name !== 'battery') + return; + + if (action_group.has_action('battery')) { + const value = action_group.get_action_state('battery'); + const [charging, icon_name, level, time] = value.deepUnpack(); + + this._state = { + charging: charging, + icon_name: icon_name, + level: level, + time: time, + }; + } else { + this._state = null; + } + + this._sync(); + } + + _onStateChanged(action_group, action_name, value) { + if (action_name !== 'battery') + return; + + const [charging, icon_name, level, time] = value.deepUnpack(); + + this._state = { + charging: charging, + icon_name: icon_name, + level: level, + time: time, + }; + + this._sync(); + } + + _getBatteryLabel() { + if (!this._state) + return null; + + const {charging, level, time} = this._state; + + if (level === 100) + // TRANSLATORS: When the battery level is 100% + return _('Fully Charged'); + + if (time === 0) + // TRANSLATORS: When no time estimate for the battery is available + // EXAMPLE: 42% (Estimating…) + return _('%d%% (Estimating…)').format(level); + + const total = time / 60; + const minutes = Math.floor(total % 60); + const hours = Math.floor(total / 60); + + if (charging) { + // TRANSLATORS: Estimated time until battery is charged + // EXAMPLE: 42% (1:15 Until Full) + return _('%d%% (%d\u2236%02d Until Full)').format( + level, + hours, + minutes + ); + } else { + // TRANSLATORS: Estimated time until battery is empty + // EXAMPLE: 42% (12:15 Remaining) + return _('%d%% (%d\u2236%02d Remaining)').format( + level, + hours, + minutes + ); + } + } + + _onDestroy(actor) { + actor.device.action_group.disconnect(actor._actionAddedId); + actor.device.action_group.disconnect(actor._actionRemovedId); + actor.device.action_group.disconnect(actor._actionStateChangedId); + } + + _sync() { + this.visible = !!this._state; + + if (!this.visible) + return; + + this.icon.icon_name = this._state.icon_name; + this.label.text = (this._state.level > -1) ? `${this._state.level}%` : ''; + this.tooltip.text = this._getBatteryLabel(); + } +}); + + +/** + * A cell signal strength widget with two icons + */ +var SignalStrength = GObject.registerClass({ + GTypeName: 'GSConnectShellDeviceSignalStrength', +}, class SignalStrength extends St.BoxLayout { + + _init(params) { + super._init({ + reactive: true, + style_class: 'gsconnect-device-signal-strength', + track_hover: true, + }); + Object.assign(this, params); + + // Network Type Icon + this.networkTypeIcon = new St.Icon({ + fallback_icon_name: 'network-cellular-symbolic', + icon_size: 16, + }); + this.add_child(this.networkTypeIcon); + + // Signal Strength Icon + this.signalStrengthIcon = new St.Icon({ + fallback_icon_name: 'network-cellular-offline-symbolic', + icon_size: 16, + }); + this.add_child(this.signalStrengthIcon); + + // Network Type Text + this.tooltip = new Tooltip.Tooltip({ + parent: this, + text: null, + }); + + // ConnectivityReport GAction + this._actionAddedId = this.device.action_group.connect( + 'action-added', + this._onActionChanged.bind(this) + ); + this._actionRemovedId = this.device.action_group.connect( + 'action-removed', + this._onActionChanged.bind(this) + ); + this._actionStateChangedId = this.device.action_group.connect( + 'action-state-changed', + this._onStateChanged.bind(this) + ); + + this._onActionChanged(this.device.action_group, 'connectivityReport'); + + // Cleanup on destroy + this.connect('destroy', this._onDestroy); + } + + _onActionChanged(action_group, action_name) { + if (action_name !== 'connectivityReport') + return; + + if (action_group.has_action('connectivityReport')) { + const value = action_group.get_action_state('connectivityReport'); + const [ + cellular_network_type, + cellular_network_type_icon, + cellular_network_strength, + cellular_network_strength_icon, + hotspot_name, + hotspot_bssid, + ] = value.deepUnpack(); + + this._state = { + cellular_network_type: cellular_network_type, + cellular_network_type_icon: cellular_network_type_icon, + cellular_network_strength: cellular_network_strength, + cellular_network_strength_icon: cellular_network_strength_icon, + hotspot_name: hotspot_name, + hotspot_bssid: hotspot_bssid, + }; + } else { + this._state = null; + } + + this._sync(); + } + + _onStateChanged(action_group, action_name, value) { + if (action_name !== 'connectivityReport') + return; + + const [ + cellular_network_type, + cellular_network_type_icon, + cellular_network_strength, + cellular_network_strength_icon, + hotspot_name, + hotspot_bssid, + ] = value.deepUnpack(); + + this._state = { + cellular_network_type: cellular_network_type, + cellular_network_type_icon: cellular_network_type_icon, + cellular_network_strength: cellular_network_strength, + cellular_network_strength_icon: cellular_network_strength_icon, + hotspot_name: hotspot_name, + hotspot_bssid: hotspot_bssid, + }; + + this._sync(); + } + + _onDestroy(actor) { + actor.device.action_group.disconnect(actor._actionAddedId); + actor.device.action_group.disconnect(actor._actionRemovedId); + actor.device.action_group.disconnect(actor._actionStateChangedId); + } + + _sync() { + this.visible = !!this._state; + + if (!this.visible) + return; + + this.networkTypeIcon.icon_name = this._state.cellular_network_type_icon; + this.signalStrengthIcon.icon_name = this._state.cellular_network_strength_icon; + this.tooltip.text = this._state.cellular_network_type; + } +}); + + +/** + * A PopupMenu used as an information and control center for a device + */ +var Menu = class Menu extends PopupMenu.PopupMenuSection { + + constructor(params) { + super(); + Object.assign(this, params); + + this.actor.add_style_class_name('gsconnect-device-menu'); + + // Title + this._title = new PopupMenu.PopupSeparatorMenuItem(this.device.name); + this.addMenuItem(this._title); + + // Title -> Name + this._title.label.style_class = 'gsconnect-device-name'; + this._title.label.clutter_text.ellipsize = 0; + this.device.bind_property( + 'name', + this._title.label, + 'text', + GObject.BindingFlags.SYNC_CREATE + ); + + // Title -> Cellular Signal Strength + this._signalStrength = new SignalStrength({device: this.device}); + this._title.actor.add_child(this._signalStrength); + + // Title -> Battery + this._battery = new Battery({device: this.device}); + this._title.actor.add_child(this._battery); + + // Actions + let actions; + + if (this.menu_type === 'icon') { + actions = new GMenu.IconBox({ + action_group: this.device.action_group, + model: this.device.menu, + }); + } else if (this.menu_type === 'list') { + actions = new GMenu.ListBox({ + action_group: this.device.action_group, + model: this.device.menu, + }); + } + + this.addMenuItem(actions); + } + + isEmpty() { + return false; + } +}; + + +/** + * An indicator representing a Device in the Status Area + */ +var Indicator = GObject.registerClass({ + GTypeName: 'GSConnectDeviceIndicator', +}, class Indicator extends PanelMenu.Button { + + _init(params) { + super._init(0.0, `${params.device.name} Indicator`, false); + Object.assign(this, params); + + // Device Icon + this._icon = new St.Icon({ + gicon: Extension.getIcon(this.device.icon_name), + style_class: 'system-status-icon gsconnect-device-indicator', + }); + this.add_child(this._icon); + + // Menu + const menu = new Menu({ + device: this.device, + menu_type: 'icon', + }); + this.menu.addMenuItem(menu); + } +}); + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/gmenu.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/gmenu.js new file mode 100644 index 0000000..e3f1248 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/gmenu.js @@ -0,0 +1,649 @@ +'use strict'; + +const Atk = imports.gi.Atk; +const Clutter = imports.gi.Clutter; +const Gio = imports.gi.Gio; +const GObject = imports.gi.GObject; +const St = imports.gi.St; + +const PopupMenu = imports.ui.popupMenu; + +const Extension = imports.misc.extensionUtils.getCurrentExtension(); + +const Tooltip = Extension.imports.shell.tooltip; + + +/** + * Get a dictionary of a GMenuItem's attributes + * + * @param {Gio.MenuModel} model - The menu model containing the item + * @param {number} index - The index of the item in @model + * @return {Object} A dictionary of the item's attributes + */ +function getItemInfo(model, index) { + const info = { + target: null, + links: [], + }; + + // + let iter = model.iterate_item_attributes(index); + + while (iter.next()) { + const name = iter.get_name(); + let value = iter.get_value(); + + switch (name) { + case 'icon': + value = Gio.Icon.deserialize(value); + + if (value instanceof Gio.ThemedIcon) + value = Extension.getIcon(value.names[0]); + + info[name] = value; + break; + + case 'target': + info[name] = value; + break; + + default: + info[name] = value.unpack(); + } + } + + // Submenus & Sections + iter = model.iterate_item_links(index); + + while (iter.next()) { + info.links.push({ + name: iter.get_name(), + value: iter.get_value(), + }); + } + + return info; +} + + +/** + * + */ +var ListBox = class ListBox extends PopupMenu.PopupMenuSection { + + constructor(params) { + super(); + Object.assign(this, params); + + // Main Actor + this.actor = new St.BoxLayout({ + x_expand: true, + clip_to_allocation: true, + }); + this.actor._delegate = this; + + // Item Box + this.box.clip_to_allocation = true; + this.box.x_expand = true; + this.box.add_style_class_name('gsconnect-list-box'); + this.box.set_pivot_point(1, 1); + this.actor.add_child(this.box); + + // Submenu Container + this.sub = new St.BoxLayout({ + clip_to_allocation: true, + vertical: false, + visible: false, + x_expand: true, + }); + this.sub.set_pivot_point(1, 1); + this.sub._delegate = this; + this.actor.add_child(this.sub); + + // Handle transitions + this._boxTransitionsCompletedId = this.box.connect( + 'transitions-completed', + this._onTransitionsCompleted.bind(this) + ); + + this._subTransitionsCompletedId = this.sub.connect( + 'transitions-completed', + this._onTransitionsCompleted.bind(this) + ); + + // Handle keyboard navigation + this._submenuCloseKeyId = this.sub.connect( + 'key-press-event', + this._onSubmenuCloseKey.bind(this) + ); + + // Refresh the menu when mapped + this._mappedId = this.actor.connect( + 'notify::mapped', + this._onMapped.bind(this) + ); + + // Watch the model for changes + this._itemsChangedId = this.model.connect( + 'items-changed', + this._onItemsChanged.bind(this) + ); + this._onItemsChanged(); + } + + _onMapped(actor) { + if (actor.mapped) { + this._onItemsChanged(); + + // We use this instead of close() to avoid touching finalized objects + } else { + this.box.set_opacity(255); + this.box.set_width(-1); + this.box.set_height(-1); + this.box.visible = true; + + this._submenu = null; + this.sub.set_opacity(0); + this.sub.set_width(0); + this.sub.set_height(0); + this.sub.visible = false; + this.sub.get_children().map(menu => menu.hide()); + } + } + + _onSubmenuCloseKey(actor, event) { + if (this.submenu && event.get_key_symbol() === Clutter.KEY_Left) { + this.submenu.submenu_for.setActive(true); + this.submenu = null; + return Clutter.EVENT_STOP; + } + + return Clutter.EVENT_PROPAGATE; + } + + _onSubmenuOpenKey(actor, event) { + const item = actor._delegate; + + if (item.submenu && event.get_key_symbol() === Clutter.KEY_Right) { + this.submenu = item.submenu; + item.submenu.firstMenuItem.setActive(true); + } + + return Clutter.EVENT_PROPAGATE; + } + + _onGMenuItemActivate(item, event) { + this.emit('activate', item); + + if (item.submenu) { + this.submenu = item.submenu; + } else if (item.action_name) { + this.action_group.activate_action( + item.action_name, + item.action_target + ); + this.itemActivated(); + } + } + + _addGMenuItem(info) { + const item = new PopupMenu.PopupMenuItem(info.label); + this.addMenuItem(item); + + if (info.action !== undefined) { + item.action_name = info.action.split('.')[1]; + item.action_target = info.target; + + item.actor.visible = this.action_group.get_action_enabled( + item.action_name + ); + } + + // Modify the ::activate callback to invoke the GAction or submenu + item.disconnect(item._activateId); + item._activateId = item.connect( + 'activate', + this._onGMenuItemActivate.bind(this) + ); + + return item; + } + + _addGMenuSection(model) { + const section = new ListBox({ + model: model, + action_group: this.action_group, + }); + this.addMenuItem(section); + } + + _addGMenuSubmenu(model, item) { + // Add an expander arrow to the item + const arrow = PopupMenu.arrowIcon(St.Side.RIGHT); + arrow.x_align = Clutter.ActorAlign.END; + arrow.x_expand = true; + item.actor.add_child(arrow); + + // Mark it as an expandable and open on right-arrow + item.actor.add_accessible_state(Atk.StateType.EXPANDABLE); + + item.actor.connect( + 'key-press-event', + this._onSubmenuOpenKey.bind(this) + ); + + // Create the submenu + item.submenu = new ListBox({ + model: model, + action_group: this.action_group, + submenu_for: item, + _parent: this, + }); + item.submenu.actor.hide(); + + // Add to the submenu container + this.sub.add_child(item.submenu.actor); + } + + _onItemsChanged(model, position, removed, added) { + // Clear the menu + this.removeAll(); + this.sub.get_children().map(child => child.destroy()); + + for (let i = 0, len = this.model.get_n_items(); i < len; i++) { + const info = getItemInfo(this.model, i); + let item; + + // A regular item + if (info.hasOwnProperty('label')) + item = this._addGMenuItem(info); + + for (const link of info.links) { + // Submenu + if (link.name === 'submenu') { + this._addGMenuSubmenu(link.value, item); + + // Section + } else if (link.name === 'section') { + this._addGMenuSection(link.value); + + // len is length starting at 1 + if (i + 1 < len) + this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + } + } + } + + // If this is a submenu of another item... + if (this.submenu_for) { + // Prepend an "<= Go Back" item, bold with a unicode arrow + const prev = new PopupMenu.PopupMenuItem(this.submenu_for.label.text); + prev.label.style = 'font-weight: bold;'; + const prevArrow = PopupMenu.arrowIcon(St.Side.LEFT); + prev.replace_child(prev._ornamentLabel, prevArrow); + this.addMenuItem(prev, 0); + + // Modify the ::activate callback to close the submenu + prev.disconnect(prev._activateId); + + prev._activateId = prev.connect('activate', (item, event) => { + this.emit('activate', item); + this._parent.submenu = null; + }); + } + } + + _onTransitionsCompleted(actor) { + if (this.submenu) { + this.box.visible = false; + } else { + this.sub.visible = false; + this.sub.get_children().map(menu => menu.hide()); + } + } + + get submenu() { + return this._submenu || null; + } + + set submenu(submenu) { + // Get the current allocation to hold the menu width + const allocation = this.actor.allocation; + const width = Math.max(0, allocation.x2 - allocation.x1); + + // Prepare the appropriate child for tweening + if (submenu) { + this.sub.set_opacity(0); + this.sub.set_width(0); + this.sub.set_height(0); + this.sub.visible = true; + } else { + this.box.set_opacity(0); + this.box.set_width(0); + this.sub.set_height(0); + this.box.visible = true; + } + + // Setup the animation + this.box.save_easing_state(); + this.box.set_easing_mode(Clutter.AnimationMode.EASE_IN_OUT_CUBIC); + this.box.set_easing_duration(250); + + this.sub.save_easing_state(); + this.sub.set_easing_mode(Clutter.AnimationMode.EASE_IN_OUT_CUBIC); + this.sub.set_easing_duration(250); + + if (submenu) { + submenu.actor.show(); + + this.sub.set_opacity(255); + this.sub.set_width(width); + this.sub.set_height(-1); + + this.box.set_opacity(0); + this.box.set_width(0); + this.box.set_height(0); + } else { + this.box.set_opacity(255); + this.box.set_width(width); + this.box.set_height(-1); + + this.sub.set_opacity(0); + this.sub.set_width(0); + this.sub.set_height(0); + } + + // Reset the animation + this.box.restore_easing_state(); + this.sub.restore_easing_state(); + + // + this._submenu = submenu; + } + + destroy() { + this.actor.disconnect(this._mappedId); + this.box.disconnect(this._boxTransitionsCompletedId); + this.sub.disconnect(this._subTransitionsCompletedId); + this.sub.disconnect(this._submenuCloseKeyId); + this.model.disconnect(this._itemsChangedId); + + super.destroy(); + } +}; + + +/** + * A St.Button subclass for iconic GMenu items + */ +var IconButton = GObject.registerClass({ + GTypeName: 'GSConnectShellIconButton', +}, class Button extends St.Button { + + _init(params) { + super._init({ + style_class: 'gsconnect-icon-button', + can_focus: true, + }); + Object.assign(this, params); + + // Item attributes + if (params.info.hasOwnProperty('action')) + this.action_name = params.info.action.split('.')[1]; + + if (params.info.hasOwnProperty('target')) + this.action_target = params.info.target; + + if (params.info.hasOwnProperty('label')) { + this.tooltip = new Tooltip.Tooltip({ + parent: this, + markup: params.info.label, + }); + + this.accessible_name = params.info.label; + } + + if (params.info.hasOwnProperty('icon')) + this.child = new St.Icon({gicon: params.info.icon}); + + // Submenu + for (const link of params.info.links) { + if (link.name === 'submenu') { + this.add_accessible_state(Atk.StateType.EXPANDABLE); + this.toggle_mode = true; + this.connect('notify::checked', this._onChecked); + + this.submenu = new ListBox({ + model: link.value, + action_group: this.action_group, + _parent: this._parent, + }); + + this.submenu.actor.style_class = 'popup-sub-menu'; + this.submenu.actor.visible = false; + } + } + } + + // This is (reliably?) emitted before ::clicked + _onChecked(button) { + if (button.checked) { + button.add_accessible_state(Atk.StateType.EXPANDED); + button.add_style_pseudo_class('active'); + } else { + button.remove_accessible_state(Atk.StateType.EXPANDED); + button.remove_style_pseudo_class('active'); + } + } + + // This is (reliably?) emitted after notify::checked + vfunc_clicked(clicked_button) { + // Unless this has a submenu, activate the action and close the menu + if (!this.toggle_mode) { + this._parent._getTopMenu().close(); + + this.action_group.activate_action( + this.action_name, + this.action_target + ); + + // StButton.checked has already been toggled so we're opening + } else if (this.checked) { + this._parent.submenu = this.submenu; + + // If this is the active submenu being closed, animate-close it + } else if (this._parent.submenu === this.submenu) { + this._parent.submenu = null; + } + } +}); + + +var IconBox = class IconBox extends PopupMenu.PopupMenuSection { + + constructor(params) { + super(); + Object.assign(this, params); + + // Main Actor + this.actor = new St.BoxLayout({ + vertical: true, + x_expand: true, + }); + this.actor._delegate = this; + + // Button Box + this.box._delegate = this; + this.box.style_class = 'gsconnect-icon-box'; + this.box.vertical = false; + this.actor.add_child(this.box); + + // Submenu Container + this.sub = new St.BoxLayout({ + clip_to_allocation: true, + vertical: true, + x_expand: true, + }); + this.sub.connect('transitions-completed', this._onTransitionsCompleted); + this.sub._delegate = this; + this.actor.add_child(this.sub); + + // Track menu items so we can use ::items-changed + this._menu_items = new Map(); + + // PopupMenu + this._mappedId = this.actor.connect( + 'notify::mapped', + this._onMapped.bind(this) + ); + + // GMenu + this._itemsChangedId = this.model.connect( + 'items-changed', + this._onItemsChanged.bind(this) + ); + + // GActions + this._actionAddedId = this.action_group.connect( + 'action-added', + this._onActionChanged.bind(this) + ); + this._actionEnabledChangedId = this.action_group.connect( + 'action-enabled-changed', + this._onActionChanged.bind(this) + ); + this._actionRemovedId = this.action_group.connect( + 'action-removed', + this._onActionChanged.bind(this) + ); + } + + destroy() { + this.actor.disconnect(this._mappedId); + this.model.disconnect(this._itemsChangedId); + this.action_group.disconnect(this._actionAddedId); + this.action_group.disconnect(this._actionEnabledChangedId); + this.action_group.disconnect(this._actionRemovedId); + + super.destroy(); + } + + get submenu() { + return this._submenu || null; + } + + set submenu(submenu) { + if (submenu) { + for (const button of this.box.get_children()) { + if (button.submenu && this._submenu && button.submenu !== submenu) { + button.checked = false; + button.submenu.actor.hide(); + } + } + + this.sub.set_height(0); + submenu.actor.show(); + } + + this.sub.save_easing_state(); + this.sub.set_easing_duration(250); + this.sub.set_easing_mode(Clutter.AnimationMode.EASE_IN_OUT_CUBIC); + + this.sub.set_height(submenu ? submenu.actor.get_preferred_size()[1] : 0); + this.sub.restore_easing_state(); + + this._submenu = submenu; + } + + _onMapped(actor) { + if (!actor.mapped) { + this._submenu = null; + + for (const button of this.box.get_children()) + button.checked = false; + + for (const submenu of this.sub.get_children()) + submenu.hide(); + } + } + + _onActionChanged(group, name, enabled) { + const menuItem = this._menu_items.get(name); + + if (menuItem !== undefined) + menuItem.visible = group.get_action_enabled(name); + } + + _onItemsChanged(model, position, removed, added) { + // Remove items + while (removed > 0) { + const button = this.box.get_child_at_index(position); + const action_name = button.action_name; + + if (button.submenu) + button.submenu.destroy(); + + button.destroy(); + + this._menu_items.delete(action_name); + removed--; + } + + // Add items + for (let i = 0; i < added; i++) { + const index = position + i; + + // Create an iconic button + const button = new IconButton({ + action_group: this.action_group, + info: getItemInfo(model, index), + // NOTE: Because this doesn't derive from a PopupMenu class + // it lacks some things its parent will expect from it + _parent: this, + _delegate: null, + }); + + // Set the visibility based on the enabled state + if (button.action_name !== undefined) { + button.visible = this.action_group.get_action_enabled( + button.action_name + ); + } + + // If it has a submenu, add it as a sibling + if (button.submenu) + this.sub.add_child(button.submenu.actor); + + // Track the item if it has an action + if (button.action_name !== undefined) + this._menu_items.set(button.action_name, button); + + // Insert it in the box at the defined position + this.box.insert_child_at_index(button, index); + } + } + + _onTransitionsCompleted(actor) { + const menu = actor._delegate; + + for (const button of menu.box.get_children()) { + if (button.submenu && button.submenu !== menu.submenu) { + button.checked = false; + button.submenu.actor.hide(); + } + } + + menu.sub.set_height(-1); + } + + // PopupMenu.PopupMenuBase overrides + isEmpty() { + return (this.box.get_children().length === 0); + } + + _setParent(parent) { + super._setParent(parent); + this._onItemsChanged(this.model, 0, 0, this.model.get_n_items()); + } +}; + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/keybindings.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/keybindings.js new file mode 100644 index 0000000..48cba6c --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/keybindings.js @@ -0,0 +1,102 @@ +'use strict'; + +const Config = imports.misc.config; +const Main = imports.ui.main; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; + + +/** + * Keybindings.Manager is a simple convenience class for managing keyboard + * shortcuts in GNOME Shell. You bind a shortcut using add(), which on success + * will return a non-zero action id that can later be used with remove() to + * unbind the shortcut. + * + * Accelerators are accepted in the form returned by Gtk.accelerator_name() and + * callbacks are invoked directly, so should be complete closures. + * + * References: + * https://developer.gnome.org/gtk3/stable/gtk3-Keyboard-Accelerators.html + * https://developer.gnome.org/meta/stable/MetaDisplay.html + * https://developer.gnome.org/meta/stable/meta-MetaKeybinding.html + * https://gitlab.gnome.org/GNOME/gnome-shell/blob/master/js/ui/windowManager.js#L1093-1112 + */ +var Manager = class Manager { + + constructor() { + this._keybindings = new Map(); + + this._acceleratorActivatedId = global.display.connect( + 'accelerator-activated', + this._onAcceleratorActivated.bind(this) + ); + } + + _onAcceleratorActivated(display, action, inputDevice, timestamp) { + try { + const binding = this._keybindings.get(action); + + if (binding !== undefined) + binding.callback(); + } catch (e) { + logError(e); + } + } + + /** + * Add a keybinding with callback + * + * @param {string} accelerator - An accelerator in the form '<Control>q' + * @param {Function} callback - A callback for the accelerator + * @return {number} A non-zero action id on success, or 0 on failure + */ + add(accelerator, callback) { + try { + const action = global.display.grab_accelerator(accelerator, 0); + + if (action === Meta.KeyBindingAction.NONE) + throw new Error(`Failed to add keybinding: '${accelerator}'`); + + const name = Meta.external_binding_name_for_action(action); + Main.wm.allowKeybinding(name, Shell.ActionMode.ALL); + this._keybindings.set(action, {name: name, callback: callback}); + + return action; + } catch (e) { + logError(e); + } + } + + /** + * Remove a keybinding + * + * @param {number} action - A non-zero action id returned by add() + */ + remove(action) { + try { + const binding = this._keybindings.get(action); + global.display.ungrab_accelerator(action); + Main.wm.allowKeybinding(binding.name, Shell.ActionMode.NONE); + this._keybindings.delete(action); + } catch (e) { + logError(new Error(`Failed to remove keybinding: ${e.message}`)); + } + } + + /** + * Remove all keybindings + */ + removeAll() { + for (const action of this._keybindings.keys()) + this.remove(action); + } + + /** + * Destroy the keybinding manager and remove all keybindings + */ + destroy() { + global.display.disconnect(this._acceleratorActivatedId); + this.removeAll(); + } +}; + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/notification.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/notification.js new file mode 100644 index 0000000..efacc62 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/notification.js @@ -0,0 +1,439 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const St = imports.gi.St; + +const Main = imports.ui.main; +const MessageTray = imports.ui.messageTray; +const NotificationDaemon = imports.ui.notificationDaemon; + +const Extension = imports.misc.extensionUtils.getCurrentExtension(); + +// eslint-disable-next-line no-redeclare +const _ = Extension._; +const APP_ID = 'org.gnome.Shell.Extensions.GSConnect'; +const APP_PATH = '/org/gnome/Shell/Extensions/GSConnect'; + + +// deviceId Pattern (<device-id>|<remote-id>) +const DEVICE_REGEX = new RegExp(/^([^|]+)\|([\s\S]+)$/); + +// requestReplyId Pattern (<device-id>|<remote-id>)|<reply-id>) +const REPLY_REGEX = new RegExp(/^([^|]+)\|([\s\S]+)\|([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/, 'i'); + + +/** + * A slightly modified Notification Banner with an entry field + */ +const NotificationBanner = GObject.registerClass({ + GTypeName: 'GSConnectNotificationBanner', +}, class NotificationBanner extends MessageTray.NotificationBanner { + + _init(notification) { + super._init(notification); + + if (notification.requestReplyId !== undefined) + this._addReplyAction(); + } + + _addReplyAction() { + if (!this._buttonBox) { + this._buttonBox = new St.BoxLayout({ + style_class: 'notification-actions', + x_expand: true, + }); + this.setActionArea(this._buttonBox); + global.focus_manager.add_group(this._buttonBox); + } + + // Reply Button + const button = new St.Button({ + style_class: 'notification-button', + label: _('Reply'), + x_expand: true, + can_focus: true, + }); + + button.connect( + 'clicked', + this._onEntryRequested.bind(this) + ); + + this._buttonBox.add_child(button); + + // Reply Entry + this._replyEntry = new St.Entry({ + can_focus: true, + hint_text: _('Type a message'), + style_class: 'chat-response', + x_expand: true, + visible: false, + }); + + this._buttonBox.add_child(this._replyEntry); + } + + _onEntryRequested(button) { + this.focused = true; + + for (const child of this._buttonBox.get_children()) + child.visible = (child === this._replyEntry); + + // Release the notification focus with the entry focus + this._replyEntry.connect( + 'key-focus-out', + this._onEntryDismissed.bind(this) + ); + + this._replyEntry.clutter_text.connect( + 'activate', + this._onEntryActivated.bind(this) + ); + + this._replyEntry.grab_key_focus(); + } + + _onEntryDismissed(entry) { + this.focused = false; + this.emit('unfocused'); + } + + _onEntryActivated(clutter_text) { + // Refuse to send empty replies + if (clutter_text.text === '') + return; + + // Copy the text, then clear the entry + const text = clutter_text.text; + clutter_text.text = ''; + + const {deviceId, requestReplyId} = this.notification; + + const target = new GLib.Variant('(ssbv)', [ + deviceId, + 'replyNotification', + true, + new GLib.Variant('(ssa{ss})', [requestReplyId, text, {}]), + ]); + const platformData = NotificationDaemon.getPlatformData(); + + Gio.DBus.session.call( + APP_ID, + APP_PATH, + 'org.freedesktop.Application', + 'ActivateAction', + GLib.Variant.new('(sava{sv})', ['device', [target], platformData]), + null, + Gio.DBusCallFlags.NO_AUTO_START, + -1, + null, + (connection, res) => { + try { + connection.call_finish(res); + } catch (e) { + // Silence errors + } + } + ); + + this.close(); + } +}); + + +/** + * A custom notification source for spawning notifications and closing device + * notifications. This source isn't actually used, but it's methods are patched + * into existing sources. + */ +const Source = GObject.registerClass({ + GTypeName: 'GSConnectNotificationSource', +}, class Source extends NotificationDaemon.GtkNotificationDaemonAppSource { + + _closeGSConnectNotification(notification, reason) { + if (reason !== MessageTray.NotificationDestroyedReason.DISMISSED) + return; + + // Avoid sending the request multiple times + if (notification._remoteClosed || notification.remoteId === undefined) + return; + + notification._remoteClosed = true; + + const target = new GLib.Variant('(ssbv)', [ + notification.deviceId, + 'closeNotification', + true, + new GLib.Variant('s', notification.remoteId), + ]); + const platformData = NotificationDaemon.getPlatformData(); + + Gio.DBus.session.call( + APP_ID, + APP_PATH, + 'org.freedesktop.Application', + 'ActivateAction', + GLib.Variant.new('(sava{sv})', ['device', [target], platformData]), + null, + Gio.DBusCallFlags.NO_AUTO_START, + -1, + null, + (connection, res) => { + try { + connection.call_finish(res); + } catch (e) { + // If we fail, reset in case we can try again + notification._remoteClosed = false; + } + } + ); + } + + /* + * Override to control notification spawning + */ + addNotification(notificationId, notificationParams, showBanner) { + this._notificationPending = true; + + // Parse the id to determine if it's a repliable notification, device + // notification or a regular local notification + let idMatch, deviceId, requestReplyId, remoteId, localId; + + if ((idMatch = REPLY_REGEX.exec(notificationId))) { + [, deviceId, remoteId, requestReplyId] = idMatch; + localId = `${deviceId}|${remoteId}`; + + } else if ((idMatch = DEVICE_REGEX.exec(notificationId))) { + [, deviceId, remoteId] = idMatch; + localId = `${deviceId}|${remoteId}`; + + } else { + localId = notificationId; + } + + // Fix themed icons + if (notificationParams.icon) { + let gicon = Gio.Icon.deserialize(notificationParams.icon); + + if (gicon instanceof Gio.ThemedIcon) { + gicon = Extension.getIcon(gicon.names[0]); + notificationParams.icon = gicon.serialize(); + } + } + + let notification = this._notifications[localId]; + + // Check if this is a repeat + if (notification) { + notification.requestReplyId = requestReplyId; + + // Bail early If @notificationParams represents an exact repeat + const title = notificationParams.title.unpack(); + const body = notificationParams.body + ? notificationParams.body.unpack() + : null; + + if (notification.title === title && + notification.bannerBodyText === body) { + this._notificationPending = false; + return; + } + + notification.title = title; + notification.bannerBodyText = body; + + // Device Notification + } else if (idMatch) { + notification = this._createNotification(notificationParams); + + notification.deviceId = deviceId; + notification.remoteId = remoteId; + notification.requestReplyId = requestReplyId; + + notification.connect('destroy', (notification, reason) => { + this._closeGSConnectNotification(notification, reason); + delete this._notifications[localId]; + }); + + this._notifications[localId] = notification; + + // Service Notification + } else { + notification = this._createNotification(notificationParams); + notification.connect('destroy', (notification, reason) => { + delete this._notifications[localId]; + }); + this._notifications[localId] = notification; + } + + if (showBanner) + this.showNotification(notification); + else + this.pushNotification(notification); + + this._notificationPending = false; + } + + /* + * Override to raise the usual notification limit (3) + */ + pushNotification(notification) { + if (this.notifications.includes(notification)) + return; + + while (this.notifications.length >= 10) + this.notifications.shift().destroy(MessageTray.NotificationDestroyedReason.EXPIRED); + + notification.connect('destroy', this._onNotificationDestroy.bind(this)); + notification.connect('notify::acknowledged', this.countUpdated.bind(this)); + this.notifications.push(notification); + this.emit('notification-added', notification); + + this.countUpdated(); + } + + createBanner(notification) { + return new NotificationBanner(notification); + } +}); + + +/** + * If there is an active GtkNotificationDaemonAppSource for GSConnect when the + * extension is loaded, it has to be patched in place. + */ +function patchGSConnectNotificationSource() { + const source = Main.notificationDaemon._gtkNotificationDaemon._sources[APP_ID]; + + if (source !== undefined) { + // Patch in the subclassed methods + source._closeGSConnectNotification = Source.prototype._closeGSConnectNotification; + source.addNotification = Source.prototype.addNotification; + source.pushNotification = Source.prototype.pushNotification; + source.createBanner = Source.prototype.createBanner; + + // Connect to existing notifications + for (const notification of Object.values(source._notifications)) { + + const _id = notification.connect('destroy', (notification, reason) => { + source._closeGSConnectNotification(notification, reason); + notification.disconnect(_id); + }); + } + } +} + + +/** + * Wrap GtkNotificationDaemon._ensureAppSource() to patch GSConnect's app source + * https://gitlab.gnome.org/GNOME/gnome-shell/blob/master/js/ui/notificationDaemon.js#L742-755 + */ +const __ensureAppSource = NotificationDaemon.GtkNotificationDaemon.prototype._ensureAppSource; + +// eslint-disable-next-line func-style +const _ensureAppSource = function (appId) { + const source = __ensureAppSource.call(this, appId); + + if (source._appId === APP_ID) { + source._closeGSConnectNotification = Source.prototype._closeGSConnectNotification; + source.addNotification = Source.prototype.addNotification; + source.pushNotification = Source.prototype.pushNotification; + source.createBanner = Source.prototype.createBanner; + } + + return source; +}; + + +function patchGtkNotificationDaemon() { + NotificationDaemon.GtkNotificationDaemon.prototype._ensureAppSource = _ensureAppSource; +} + + +function unpatchGtkNotificationDaemon() { + NotificationDaemon.GtkNotificationDaemon.prototype._ensureAppSource = __ensureAppSource; +} + +/** + * We patch other Gtk notification sources so we can notify remote devices when + * notifications have been closed locally. + */ +const _addNotification = NotificationDaemon.GtkNotificationDaemonAppSource.prototype.addNotification; + +function patchGtkNotificationSources() { + // This should diverge as little as possible from the original + // eslint-disable-next-line func-style + const addNotification = function (notificationId, notificationParams, showBanner) { + this._notificationPending = true; + + if (this._notifications[notificationId]) + this._notifications[notificationId].destroy(MessageTray.NotificationDestroyedReason.REPLACED); + + const notification = this._createNotification(notificationParams); + notification.connect('destroy', (notification, reason) => { + this._withdrawGSConnectNotification(notification, reason); + delete this._notifications[notificationId]; + }); + this._notifications[notificationId] = notification; + + if (showBanner) + this.showNotification(notification); + else + this.pushNotification(notification); + + this._notificationPending = false; + }; + + // eslint-disable-next-line func-style + const _withdrawGSConnectNotification = function (id, notification, reason) { + if (reason !== MessageTray.NotificationDestroyedReason.DISMISSED) + return; + + // Avoid sending the request multiple times + if (notification._remoteWithdrawn) + return; + + notification._remoteWithdrawn = true; + + // Recreate the notification id as it would've been sent + const target = new GLib.Variant('(ssbv)', [ + '*', + 'withdrawNotification', + true, + new GLib.Variant('s', `gtk|${this._appId}|${id}`), + ]); + const platformData = NotificationDaemon.getPlatformData(); + + Gio.DBus.session.call( + APP_ID, + APP_PATH, + 'org.freedesktop.Application', + 'ActivateAction', + GLib.Variant.new('(sava{sv})', ['device', [target], platformData]), + null, + Gio.DBusCallFlags.NO_AUTO_START, + -1, + null, + (connection, res) => { + try { + connection.call_finish(res); + } catch (e) { + // If we fail, reset in case we can try again + notification._remoteWithdrawn = false; + } + } + ); + }; + + NotificationDaemon.GtkNotificationDaemonAppSource.prototype.addNotification = addNotification; + NotificationDaemon.GtkNotificationDaemonAppSource.prototype._withdrawGSConnectNotification = _withdrawGSConnectNotification; +} + + +function unpatchGtkNotificationSources() { + NotificationDaemon.GtkNotificationDaemonAppSource.prototype.addNotification = _addNotification; + delete NotificationDaemon.GtkNotificationDaemonAppSource.prototype._withdrawGSConnectNotification; +} + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/tooltip.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/tooltip.js new file mode 100644 index 0000000..4e97988 --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/tooltip.js @@ -0,0 +1,302 @@ +'use strict'; + +const Clutter = imports.gi.Clutter; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Pango = imports.gi.Pango; +const St = imports.gi.St; + +const Main = imports.ui.main; + + +/** + * An StTooltip for ClutterActors + * + * Adapted from: https://github.com/RaphaelRochet/applications-overview-tooltip + * See also: https://github.com/GNOME/gtk/blob/master/gtk/gtktooltip.c + */ +var TOOLTIP_BROWSE_ID = 0; +var TOOLTIP_BROWSE_MODE = false; + +var Tooltip = class Tooltip { + + constructor(params) { + Object.assign(this, params); + + this._bin = null; + this._hoverTimeoutId = 0; + this._showing = false; + + this._destroyId = this.parent.connect( + 'destroy', + this.destroy.bind(this) + ); + + this._hoverId = this.parent.connect( + 'notify::hover', + this._onHover.bind(this) + ); + + this._buttonPressEventId = this.parent.connect( + 'button-press-event', + this._hide.bind(this) + ); + } + + get custom() { + if (this._custom === undefined) + this._custom = null; + + return this._custom; + } + + set custom(actor) { + this._custom = actor; + this._markup = null; + this._text = null; + + if (this._showing) + this._show(); + } + + get gicon() { + if (this._gicon === undefined) + this._gicon = null; + + return this._gicon; + } + + set gicon(gicon) { + this._gicon = gicon; + + if (this._showing) + this._show(); + } + + get icon() { + return (this.gicon) ? this.gicon.name : null; + } + + set icon(icon_name) { + if (!icon_name) + this.gicon = null; + else + this.gicon = new Gio.ThemedIcon({name: icon_name}); + } + + get markup() { + if (this._markup === undefined) + this._markup = null; + + return this._markup; + } + + set markup(text) { + this._markup = text; + this._text = null; + + if (this._showing) + this._show(); + } + + get text() { + if (this._text === undefined) + this._text = null; + + return this._text; + } + + set text(text) { + this._markup = null; + this._text = text; + + if (this._showing) + this._show(); + } + + get x_offset() { + if (this._x_offset === undefined) + this._x_offset = 0; + + return this._x_offset; + } + + set x_offset(offset) { + this._x_offset = (Number.isInteger(offset)) ? offset : 0; + } + + get y_offset() { + if (this._y_offset === undefined) + this._y_offset = 0; + + return this._y_offset; + } + + set y_offset(offset) { + this._y_offset = (Number.isInteger(offset)) ? offset : 0; + } + + _show() { + if (this.text === null && this.markup === null) + return this._hide(); + + if (this._bin === null) { + this._bin = new St.Bin({ + style_class: 'osd-window gsconnect-tooltip', + opacity: 232, + }); + + if (this.custom) { + this._bin.child = this.custom; + } else { + this._bin.child = new St.BoxLayout({vertical: false}); + + if (this.gicon) { + this._bin.child.icon = new St.Icon({ + gicon: this.gicon, + y_align: St.Align.START, + }); + this._bin.child.icon.set_y_align(Clutter.ActorAlign.START); + this._bin.child.add_child(this._bin.child.icon); + } + + this.label = new St.Label({text: this.markup || this.text}); + this.label.clutter_text.line_wrap = true; + this.label.clutter_text.line_wrap_mode = Pango.WrapMode.WORD; + this.label.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; + this.label.clutter_text.use_markup = (this.markup); + this._bin.child.add_child(this.label); + } + + Main.layoutManager.uiGroup.add_child(this._bin); + Main.layoutManager.uiGroup.set_child_above_sibling(this._bin, null); + } else if (this.custom) { + this._bin.child = this.custom; + } else { + if (this._bin.child.icon) + this._bin.child.icon.destroy(); + + if (this.gicon) { + this._bin.child.icon = new St.Icon({gicon: this.gicon}); + this._bin.child.insert_child_at_index(this._bin.child.icon, 0); + } + + this.label.clutter_text.text = this.markup || this.text; + this.label.clutter_text.use_markup = (this.markup); + } + + // Position tooltip + let [x, y] = this.parent.get_transformed_position(); + x = (x + (this.parent.width / 2)) - Math.round(this._bin.width / 2); + + x += this.x_offset; + y += this.y_offset; + + // Show tooltip + if (this._showing) { + this._bin.ease({ + x: x, + y: y, + time: 0.15, + transition: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } else { + this._bin.set_position(x, y); + this._bin.ease({ + opacity: 232, + time: 0.15, + transition: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + + this._showing = true; + } + + // Enable browse mode + TOOLTIP_BROWSE_MODE = true; + + if (TOOLTIP_BROWSE_ID) { + GLib.source_remove(TOOLTIP_BROWSE_ID); + TOOLTIP_BROWSE_ID = 0; + } + + if (this._hoverTimeoutId) { + GLib.source_remove(this._hoverTimeoutId); + this._hoverTimeoutId = 0; + } + } + + _hide() { + if (this._bin) { + this._bin.ease({ + opacity: 0, + time: 0.10, + transition: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + Main.layoutManager.uiGroup.remove_actor(this._bin); + + if (this.custom) + this._bin.remove_child(this.custom); + + this._bin.destroy(); + this._bin = null; + }, + }); + } + + GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => { + TOOLTIP_BROWSE_MODE = false; + TOOLTIP_BROWSE_ID = 0; + return false; + }); + + if (this._hoverTimeoutId) { + GLib.source_remove(this._hoverTimeoutId); + this._hoverTimeoutId = 0; + } + + this._showing = false; + this._hoverTimeoutId = 0; + } + + _onHover() { + if (this.parent.hover) { + if (!this._hoverTimeoutId) { + if (this._showing) { + this._show(); + } else { + this._hoverTimeoutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + (TOOLTIP_BROWSE_MODE) ? 60 : 500, + () => { + this._show(); + this._hoverTimeoutId = 0; + return false; + } + ); + } + } + } else { + this._hide(); + } + } + + destroy() { + this.parent.disconnect(this._destroyId); + this.parent.disconnect(this._hoverId); + this.parent.disconnect(this._buttonPressEventId); + + if (this.custom) + this.custom.destroy(); + + if (this._bin) { + Main.layoutManager.uiGroup.remove_actor(this._bin); + this._bin.destroy(); + } + + if (this._hoverTimeoutId) { + GLib.source_remove(this._hoverTimeoutId); + this._hoverTimeoutId = 0; + } + } +}; + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/utils.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/utils.js new file mode 100644 index 0000000..a16f73b --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/shell/utils.js @@ -0,0 +1,218 @@ +'use strict'; + +const ByteArray = imports.byteArray; + +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; + +const Extension = imports.misc.extensionUtils.getCurrentExtension(); +const Config = Extension.imports.config; + + +/** + * Get a themed icon, using fallbacks from GSConnect's GResource when necessary. + * + * @param {string} name - A themed icon name + * @return {Gio.Icon} A themed icon + */ +function getIcon(name) { + if (getIcon._resource === undefined) { + // Setup the desktop icons + const settings = imports.gi.St.Settings.get(); + getIcon._desktop = new imports.gi.Gtk.IconTheme(); + getIcon._desktop.set_custom_theme(settings.gtk_icon_theme); + settings.connect('notify::gtk-icon-theme', (settings_, key_) => { + getIcon._desktop.set_custom_theme(settings_.gtk_icon_theme); + }); + + // Preload our fallbacks + const iconPath = 'resource://org/gnome/Shell/Extensions/GSConnect/icons'; + const iconNames = [ + 'org.gnome.Shell.Extensions.GSConnect', + 'org.gnome.Shell.Extensions.GSConnect-symbolic', + 'computer-symbolic', + 'laptop-symbolic', + 'smartphone-symbolic', + 'tablet-symbolic', + 'tv-symbolic', + 'phonelink-ring-symbolic', + 'sms-symbolic', + ]; + + getIcon._resource = {}; + + for (const iconName of iconNames) { + getIcon._resource[iconName] = new Gio.FileIcon({ + file: Gio.File.new_for_uri(`${iconPath}/${iconName}.svg`), + }); + } + } + + // Check the desktop icon theme + if (getIcon._desktop.has_icon(name)) + return new Gio.ThemedIcon({name: name}); + + // Check our GResource + if (getIcon._resource[name] !== undefined) + return getIcon._resource[name]; + + // Fallback to hoping it's in the theme somewhere + return new Gio.ThemedIcon({name: name}); +} + + +/** + * Get the contents of a GResource file, replacing `@PACKAGE_DATADIR@` where + * necessary. + * + * @param {string} relativePath - A path relative to GSConnect's resource path + * @return {string} The file contents as a string + */ +function getResource(relativePath) { + try { + const bytes = Gio.resources_lookup_data( + GLib.build_filenamev([Config.APP_PATH, relativePath]), + Gio.ResourceLookupFlags.NONE + ); + + const source = ByteArray.toString(bytes.toArray()); + + return source.replace('@PACKAGE_DATADIR@', Config.PACKAGE_DATADIR); + } catch (e) { + logError(e, 'GSConnect'); + return null; + } +} + + +/** + * Install file contents, to an absolute directory path. + * + * @param {string} dirname - An absolute directory path + * @param {string} basename - The file name + * @param {string} contents - The file contents + * @return {boolean} A success boolean + */ +function _installFile(dirname, basename, contents) { + try { + const filename = GLib.build_filenamev([dirname, basename]); + GLib.mkdir_with_parents(dirname, 0o755); + + return GLib.file_set_contents(filename, contents); + } catch (e) { + logError(e, 'GSConnect'); + return false; + } +} + +/** + * Install file contents from a GResource, to an absolute directory path. + * + * @param {string} dirname - An absolute directory path + * @param {string} basename - The file name + * @param {string} relativePath - A path relative to GSConnect's resource path + * @return {boolean} A success boolean + */ +function _installResource(dirname, basename, relativePath) { + try { + const contents = getResource(relativePath); + + return _installFile(dirname, basename, contents); + } catch (e) { + logError(e, 'GSConnect'); + return false; + } +} + + +/** + * Install the files necessary for the GSConnect service to run. + */ +function installService() { + const confDir = GLib.get_user_config_dir(); + const dataDir = GLib.get_user_data_dir(); + const homeDir = GLib.get_home_dir(); + + // DBus Service + const dbusDir = GLib.build_filenamev([dataDir, 'dbus-1', 'services']); + const dbusFile = `${Config.APP_ID}.service`; + + // Desktop Entry + const appDir = GLib.build_filenamev([dataDir, 'applications']); + const appFile = `${Config.APP_ID}.desktop`; + const appPrefsFile = `${Config.APP_ID}.Preferences.desktop`; + + // Application Icon + const iconDir = GLib.build_filenamev([dataDir, 'icons', 'hicolor', 'scalable', 'apps']); + const iconFull = `${Config.APP_ID}.svg`; + const iconSym = `${Config.APP_ID}-symbolic.svg`; + + // File Manager Extensions + const fileManagers = [ + [`${dataDir}/nautilus-python/extensions`, 'nautilus-gsconnect.py'], + [`${dataDir}/nemo-python/extensions`, 'nemo-gsconnect.py'], + ]; + + // WebExtension Manifests + const manifestFile = 'org.gnome.shell.extensions.gsconnect.json'; + const google = getResource(`webextension/${manifestFile}.google.in`); + const mozilla = getResource(`webextension/${manifestFile}.mozilla.in`); + const manifests = [ + [`${confDir}/chromium/NativeMessagingHosts/`, google], + [`${confDir}/google-chrome/NativeMessagingHosts/`, google], + [`${confDir}/google-chrome-beta/NativeMessagingHosts/`, google], + [`${confDir}/google-chrome-unstable/NativeMessagingHosts/`, google], + [`${confDir}/BraveSoftware/Brave-Browser/NativeMessagingHosts/`, google], + [`${homeDir}/.mozilla/native-messaging-hosts/`, mozilla], + [`${homeDir}/.config/microsoft-edge-dev/NativeMessagingHosts`, google], + [`${homeDir}/.config/microsoft-edge-beta/NativeMessagingHosts`, google], + ]; + + // If running as a user extension, ensure the DBus service, desktop entry, + // file manager scripts, and WebExtension manifests are installed. + if (Config.IS_USER) { + // DBus Service + if (!_installResource(dbusDir, dbusFile, `${dbusFile}.in`)) + throw Error('GSConnect: Failed to install DBus Service'); + + // Desktop Entries + _installResource(appDir, appFile, appFile); + _installResource(appDir, appPrefsFile, appPrefsFile); + + // Application Icon + _installResource(iconDir, iconFull, `icons/${iconFull}`); + _installResource(iconDir, iconSym, `icons/${iconSym}`); + + // File Manager Extensions + const target = `${Config.PACKAGE_DATADIR}/nautilus-gsconnect.py`; + + for (const [dir, name] of fileManagers) { + const script = Gio.File.new_for_path(GLib.build_filenamev([dir, name])); + + if (!script.query_exists(null)) { + GLib.mkdir_with_parents(dir, 0o755); + script.make_symbolic_link(target, null); + } + } + + // WebExtension Manifests + for (const [dirname, contents] of manifests) + _installFile(dirname, manifestFile, contents); + + // Otherwise, if running as a system extension, ensure anything previously + // installed when running as a user extension is removed. + } else { + GLib.unlink(GLib.build_filenamev([dbusDir, dbusFile])); + GLib.unlink(GLib.build_filenamev([appDir, appFile])); + GLib.unlink(GLib.build_filenamev([appDir, appPrefsFile])); + GLib.unlink(GLib.build_filenamev([iconDir, iconFull])); + GLib.unlink(GLib.build_filenamev([iconDir, iconSym])); + + for (const [dir, name] of fileManagers) + GLib.unlink(GLib.build_filenamev([dir, name])); + + for (const manifest of manifests) + GLib.unlink(GLib.build_filenamev([manifest[0], manifestFile])); + } +} + diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/stylesheet.css b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/stylesheet.css new file mode 100644 index 0000000..77d1cac --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/stylesheet.css @@ -0,0 +1,122 @@ + +/* Device Menu + +PopupMenu.PopupMenuSection.gsconnect-device-section + PopupMenu.PopupMenuSection.gsconnect-device-menu + PopupMenu.PopupSeparatorMenuItem + StLabel.gsconnect-device-name + StBoxLayout.gsconnect-device-battery + PopupMenu.PopupMenuSection + StBoxLayout.gsconnect-list-box + StBoxLayout (Submenu Container) +*/ +.gsconnect-device-section { +} + +/* Title Bar */ +.gsconnect-device-name { + font-weight: bold; +} + +.gsconnect-device-menu .popup-separator-menu-item { + margin-left: 0; + margin-right: 0; +} + +/* Battery Widget */ +.gsconnect-device-battery { + spacing: 3px; +} + +.gsconnect-device-battery StLabel { + font-size: 0.75em; +} + +.gsconnect-device-battery StIcon { + icon-size: 16px; +} + +/* Signal Strength Widget */ +.gsconnect-device-signal-strength { + spacing: 3px; +} + +.gsconnect-device-signal-strength StLabel { + font-size: 0.75em; +} + +.gsconnect-device-signal-strength StIcon { + icon-size: 16px; +} + +/* List Box */ +.gsconnect-list-box { +} + + +/* Device Panel Indicator + +PanelMenu.Button.gsconnect-device-indicator + PopupMenu.PopupMenu + PopupMenu.PopupMenuSection.gsconnect-device-menu + PopupMenu.PopupSeparatorMenuItem + StLabel.gsconnect-device-name + StBoxLayout.gsconnect-device-battery + PopupMenu.PopupMenuSection + StBoxLayout.gsconnect-icon-box + StBoxLayout (Submenu Container) + */ +.gsconnect-device-indicator { + -st-icon-style: symbolic; +} + +/* Icon Box */ +.gsconnect-icon-box { + margin: 0em 2em 0.5em; + spacing: 6px; +} + +.gsconnect-icon-button { + border-radius: 1em; + padding: 0.5em; +} + +.gsconnect-icon-button:hover, .gsconnect-icon-button:focus { + background-color: rgba(255, 255, 255, 0.125); +} + +.gsconnect-icon-button StIcon { + icon-size: 1em; +} + + +/* Tooltip + +StBin.gsconnect-tooltip (inherits from .osd-window) + StBoxLayout || [ Custom ClutterActor ] + StIcon + StLabel +*/ +.gsconnect-tooltip { + border-radius: 3px; + min-width: 0; + min-height: 0; + padding: 6px; +} + +.gsconnect-tooltip > StBoxLayout { + spacing: 6px; +} + +.gsconnect-tooltip StIcon { + icon-size: 16px; +} + +.gsconnect-tooltip StLabel { + font-weight: normal; + text-align: left; +} + +.gsconnect-tooltip StLabel:rtl { + text-align: right; +} diff --git a/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/utils/remote.js b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/utils/remote.js new file mode 100644 index 0000000..7a1c27b --- /dev/null +++ b/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/utils/remote.js @@ -0,0 +1,516 @@ +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const SERVICE_NAME = 'org.gnome.Shell.Extensions.GSConnect'; +const SERVICE_PATH = '/org/gnome/Shell/Extensions/GSConnect'; +const DEVICE_NAME = 'org.gnome.Shell.Extensions.GSConnect.Device'; +const DEVICE_PATH = '/org/gnome/Shell/Extensions/GSConnect/Device'; + + +const _PROPERTIES = { + 'Connected': 'connected', + 'EncryptionInfo': 'encryption-info', + 'IconName': 'icon-name', + 'Id': 'id', + 'Name': 'name', + 'Paired': 'paired', + 'Type': 'type', +}; + + +function _proxyInit(proxy, cancellable = null) { + if (proxy.__initialized !== undefined) + return Promise.resolve(); + + return new Promise((resolve, reject) => { + proxy.init_async( + GLib.PRIORITY_DEFAULT, + cancellable, + (proxy, res) => { + try { + proxy.init_finish(res); + proxy.__initialized = true; + resolve(); + } catch (e) { + Gio.DBusError.strip_remote_error(e); + reject(e); + } + } + ); + }); +} + + +/** + * A simple proxy wrapper for devices exported over DBus. + */ +var Device = GObject.registerClass({ + GTypeName: 'GSConnectRemoteDevice', + Implements: [Gio.DBusInterface], + Properties: { + 'connected': GObject.ParamSpec.boolean( + 'connected', + 'Connected', + 'Whether the device is connected', + GObject.ParamFlags.READABLE, + null + ), + 'encryption-info': GObject.ParamSpec.string( + 'encryption-info', + 'Encryption Info', + 'A formatted string with the local and remote fingerprints', + GObject.ParamFlags.READABLE, + null + ), + 'icon-name': GObject.ParamSpec.string( + 'icon-name', + 'Icon Name', + 'Icon name representing the device', + GObject.ParamFlags.READABLE, + null + ), + 'id': GObject.ParamSpec.string( + 'id', + 'deviceId', + 'The device hostname or other unique id', + GObject.ParamFlags.READABLE, + '' + ), + 'name': GObject.ParamSpec.string( + 'name', + 'deviceName', + 'The device name', + GObject.ParamFlags.READABLE, + null + ), + 'paired': GObject.ParamSpec.boolean( + 'paired', + 'Paired', + 'Whether the device is paired', + GObject.ParamFlags.READABLE, + null + ), + 'type': GObject.ParamSpec.string( + 'type', + 'deviceType', + 'The device type', + GObject.ParamFlags.READABLE, + null + ), + }, +}, class Device extends Gio.DBusProxy { + + _init(service, object_path) { + this._service = service; + + super._init({ + g_connection: service.g_connection, + g_name: SERVICE_NAME, + g_object_path: object_path, + g_interface_name: DEVICE_NAME, + }); + } + + vfunc_g_properties_changed(changed, invalidated) { + try { + for (const name in changed.deepUnpack()) + this.notify(_PROPERTIES[name]); + } catch (e) { + logError(e); + } + } + + _get(name, fallback = null) { + try { + return this.get_cached_property(name).unpack(); + } catch (e) { + return fallback; + } + } + + get connected() { + return this._get('Connected', false); + } + + get encryption_info() { + return this._get('EncryptionInfo', ''); + } + + get icon_name() { + return this._get('IconName', 'computer'); + } + + get id() { + return this._get('Id', '0'); + } + + get name() { + return this._get('Name', 'Unknown'); + } + + get paired() { + return this._get('Paired', false); + } + + get service() { + return this._service; + } + + get type() { + return this._get('Type', 'desktop'); + } + + async start() { + try { + await _proxyInit(this); + + // For GActions & GMenu we pass the service's name owner to avoid + // any mixup with instances. + this.action_group = Gio.DBusActionGroup.get( + this.g_connection, + this.service.g_name_owner, + this.g_object_path + ); + + this.menu = Gio.DBusMenuModel.get( + this.g_connection, + this.service.g_name_owner, + this.g_object_path + ); + + // Poke the GMenu to ensure it's ready for us + await new Promise((resolve, reject) => { + this.g_connection.call( + SERVICE_NAME, + this.g_object_path, + 'org.gtk.Menus', + 'Start', + new GLib.Variant('(au)', [[0]]), + null, + Gio.DBusCallFlags.NONE, + -1, + null, + (proxy, res) => { + try { + resolve(proxy.call_finish(res)); + } catch (e) { + Gio.DBusError.strip_remote_error(e); + reject(e); + } + } + ); + }); + } catch (e) { + this.destroy(); + throw e; + } + } + + destroy() { + GObject.signal_handlers_destroy(this); + } +}); + + +/** + * A simple proxy wrapper for the GSConnect service. + */ +var Service = GObject.registerClass({ + GTypeName: 'GSConnectRemoteService', + Implements: [Gio.DBusInterface], + Properties: { + 'active': GObject.ParamSpec.boolean( + 'active', + 'Active', + 'Whether the service is active', + GObject.ParamFlags.READABLE, + false + ), + }, + Signals: { + 'device-added': { + flags: GObject.SignalFlags.RUN_FIRST, + param_types: [Device.$gtype], + }, + 'device-removed': { + flags: GObject.SignalFlags.RUN_FIRST, + param_types: [Device.$gtype], + }, + }, +}, class Service extends Gio.DBusProxy { + + _init() { + super._init({ + g_bus_type: Gio.BusType.SESSION, + g_name: SERVICE_NAME, + g_object_path: SERVICE_PATH, + g_interface_name: 'org.freedesktop.DBus.ObjectManager', + g_flags: Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION, + }); + + this._active = false; + this._devices = new Map(); + this._starting = false; + + // Watch the service + this._nameOwnerChangedId = this.connect( + 'notify::g-name-owner', + this._onNameOwnerChanged.bind(this) + ); + } + + get active() { + return this._active; + } + + get devices() { + return Array.from(this._devices.values()); + } + + vfunc_g_signal(sender_name, signal_name, parameters) { + try { + // Don't emit signals until the ObjectManager has started + if (!this.active) + return; + + parameters = parameters.deepUnpack(); + + switch (true) { + case (signal_name === 'InterfacesAdded'): + this._onInterfacesAdded(...parameters); + break; + + case (signal_name === 'InterfacesRemoved'): + this._onInterfacesRemoved(...parameters); + break; + } + } catch (e) { + logError(e); + } + } + + /** + * org.freedesktop.DBus.ObjectManager.InterfacesAdded + * + * @param {string} object_path - Path interfaces have been added to + * @param {Object} interfaces - A dictionary of interface objects + */ + async _onInterfacesAdded(object_path, interfaces) { + try { + // An empty list means only the object has been added + if (Object.values(interfaces).length === 0) + return; + + // Skip existing proxies + if (this._devices.has(object_path)) + return; + + // Create a proxy + const device = new Device(this, object_path); + await device.start(); + + // Hold the proxy and emit ::device-added + this._devices.set(object_path, device); + this.emit('device-added', device); + } catch (e) { + logError(e, object_path); + } + } + + /** + * org.freedesktop.DBus.ObjectManager.InterfacesRemoved + * + * @param {string} object_path - Path interfaces have been removed from + * @param {string[]} interfaces - List of interface names removed + */ + _onInterfacesRemoved(object_path, interfaces) { + try { + // An empty interface list means the object is being removed + if (interfaces.length === 0) + return; + + // Get the proxy + const device = this._devices.get(object_path); + + if (device === undefined) + return; + + // Release the proxy and emit ::device-removed + this._devices.delete(object_path); + this.emit('device-removed', device); + + // Destroy the device and force disposal + device.destroy(); + } catch (e) { + logError(e, object_path); + } + } + + async _addDevices() { + const objects = await new Promise((resolve, reject) => { + this.call( + 'GetManagedObjects', + null, + Gio.DBusCallFlags.NONE, + -1, + null, + (proxy, res) => { + try { + const variant = proxy.call_finish(res); + resolve(variant.deepUnpack()[0]); + } catch (e) { + Gio.DBusError.strip_remote_error(e); + reject(e); + } + } + ); + }); + + for (const [object_path, object] of Object.entries(objects)) + await this._onInterfacesAdded(object_path, object); + } + + _clearDevices() { + for (const [object_path, device] of this._devices) { + this._devices.delete(object_path); + this.emit('device-removed', device); + device.destroy(); + } + } + + async _onNameOwnerChanged() { + try { + // If the service stopped, remove each device and mark it inactive + if (this.g_name_owner === null) { + this._clearDevices(); + + this._active = false; + this.notify('active'); + + // If the service started, mark it active and add each device + } else { + this._active = true; + this.notify('active'); + + await this._addDevices(); + } + } catch (e) { + logError(e); + } + } + + /** + * Reload all devices without affecting the remote service. This amounts to + * removing and adding each device while emitting the appropriate signals. + */ + async reload() { + try { + if (this._starting === false) { + this._starting = true; + + this._clearDevices(); + await _proxyInit(this); + await this._onNameOwnerChanged(); + + this._starting = false; + } + } catch (e) { + this._starting = false; + throw e; + } + } + + /** + * Start the service + */ + async start() { + try { + if (this._starting === false && this.active === false) { + this._starting = true; + + await _proxyInit(this); + await this._onNameOwnerChanged(); + + // Activate the service if it's not already running + if (!this.active) { + await new Promise((resolve, reject) => { + this.g_connection.call( + SERVICE_NAME, + SERVICE_PATH, + 'org.freedesktop.Application', + 'Activate', + GLib.Variant.new('(a{sv})', [{}]), + null, + Gio.DBusCallFlags.NONE, + -1, + null, + (proxy, res) => { + try { + resolve(proxy.call_finish(res)); + } catch (e) { + Gio.DBusError.strip_remote_error(e); + reject(e); + } + } + ); + }); + } + + this._starting = false; + } + } catch (e) { + this._starting = false; + throw e; + } + } + + /** + * Stop the service + */ + stop() { + if (this.active) + this.activate_action('quit'); + } + + activate_action(name, parameter = null) { + try { + const paramArray = []; + + if (parameter instanceof GLib.Variant) + paramArray[0] = parameter; + + const connection = this.g_connection || Gio.DBus.session; + + connection.call( + SERVICE_NAME, + SERVICE_PATH, + 'org.freedesktop.Application', + 'ActivateAction', + GLib.Variant.new('(sava{sv})', [name, paramArray, {}]), + null, + Gio.DBusCallFlags.NONE, + -1, + null, + null + ); + } catch (e) { + logError(e); + } + } + + destroy() { + if (this._nameOwnerChangedId > 0) { + this.disconnect(this._nameOwnerChangedId); + this._nameOwnerChangedId = 0; + + this._clearDevices(); + this._active = false; + + GObject.signal_handlers_destroy(this); + } + } +}); + diff --git a/.local/share/gnome-shell/extensions/instantworkspaceswitcher@amalantony.net/extension.js b/.local/share/gnome-shell/extensions/instantworkspaceswitcher@amalantony.net/extension.js new file mode 100644 index 0000000..952abba --- /dev/null +++ b/.local/share/gnome-shell/extensions/instantworkspaceswitcher@amalantony.net/extension.js @@ -0,0 +1,47 @@ +/* extension.js + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +/* exported init */ + +const WorkspaceAnimation = imports.ui.workspaceAnimation; + +class Extension { + constructor() { + this.oldAnimateSwitch = + WorkspaceAnimation.WorkspaceAnimationController.prototype.animateSwitch; + } + + enable() { + WorkspaceAnimation.WorkspaceAnimationController.prototype.animateSwitch = function ( + _from, + _to, + _direction, + onComplete + ) { + onComplete(); + }; + } + + disable() { + WorkspaceAnimation.WorkspaceAnimationController.prototype.animateSwitch = this.oldAnimateSwitch; + } +} + +function init() { + return new Extension(); +} diff --git a/.local/share/gnome-shell/extensions/instantworkspaceswitcher@amalantony.net/metadata.json b/.local/share/gnome-shell/extensions/instantworkspaceswitcher@amalantony.net/metadata.json new file mode 100644 index 0000000..56094ae --- /dev/null +++ b/.local/share/gnome-shell/extensions/instantworkspaceswitcher@amalantony.net/metadata.json @@ -0,0 +1,12 @@ +{ + "_generated": "Generated by SweetTooth, do not edit", + "description": "Disables the workspace switch animation while preserving all other animations - instantly switch between workspaces with keyboard shortcuts.", + "name": "Disable Workspace Switch Animation for GNOME 40+", + "shell-version": [ + "40", + "41" + ], + "url": "https://github.com/amalantony/gnome-shell-extension-instant-workspace-switcher", + "uuid": "instantworkspaceswitcher@amalantony.net", + "version": 3 +} \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/move-workspaceSwitcherPopup@GithubUser699.github.com/convenience.js b/.local/share/gnome-shell/extensions/move-workspaceSwitcherPopup@GithubUser699.github.com/convenience.js new file mode 100644 index 0000000..bbc8608 --- /dev/null +++ b/.local/share/gnome-shell/extensions/move-workspaceSwitcherPopup@GithubUser699.github.com/convenience.js @@ -0,0 +1,93 @@ +/* -*- mode: js; js-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* + Copyright (c) 2011-2012, Giovanni Campagna <scampa.giovanni@gmail.com> + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the GNOME nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +const Gettext = imports.gettext; +const Gio = imports.gi.Gio; + +const Config = imports.misc.config; +const ExtensionUtils = imports.misc.extensionUtils; + +/** + * initTranslations: + * @domain: (optional): the gettext domain to use + * + * Initialize Gettext to load translations from extensionsdir/locale. + * If @domain is not provided, it will be taken from metadata['gettext-domain'] + */ +function initTranslations(domain) { + let extension = ExtensionUtils.getCurrentExtension(); + + domain = domain || extension.metadata['gettext-domain']; + + // check if this extension was built with "make zip-file", and thus + // has the locale files in a subfolder + // otherwise assume that extension has been installed in the + // same prefix as gnome-shell + let localeDir = extension.dir.get_child('locale'); + if (localeDir.query_exists(null)) + Gettext.bindtextdomain(domain, localeDir.get_path()); + else + Gettext.bindtextdomain(domain, Config.LOCALEDIR); +} + +/** + * getSettings: + * @schema: (optional): the GSettings schema id + * + * Builds and return a GSettings schema for @schema, using schema files + * in extensionsdir/schemas. If @schema is not provided, it is taken from + * metadata['settings-schema']. + */ +function getSettings(schema) { + let extension = ExtensionUtils.getCurrentExtension(); + + schema = schema || extension.metadata['settings-schema']; + + const GioSSS = Gio.SettingsSchemaSource; + + // check if this extension was built with "make zip-file", and thus + // has the schema files in a subfolder + // otherwise assume that extension has been installed in the + // same prefix as gnome-shell (and therefore schemas are available + // in the standard folders) + let schemaDir = extension.dir.get_child('schemas'); + let schemaSource; + if (schemaDir.query_exists(null)) + schemaSource = GioSSS.new_from_directory(schemaDir.get_path(), + GioSSS.get_default(), + false); + else + schemaSource = GioSSS.get_default(); + + let schemaObj = schemaSource.lookup(schema, true); + if (!schemaObj) + throw new Error('Schema ' + schema + ' could not be found for extension ' + + extension.metadata.uuid + '. Please check your installation.'); + + return new Gio.Settings({ settings_schema: schemaObj }); +} + diff --git a/.local/share/gnome-shell/extensions/move-workspaceSwitcherPopup@GithubUser699.github.com/extension.js b/.local/share/gnome-shell/extensions/move-workspaceSwitcherPopup@GithubUser699.github.com/extension.js new file mode 100644 index 0000000..4913a2a --- /dev/null +++ b/.local/share/gnome-shell/extensions/move-workspaceSwitcherPopup@GithubUser699.github.com/extension.js @@ -0,0 +1,71 @@ +// gpl v3 +// Based on: +// https://github.com/maoschanz/Move-OSD-Windows-GNOME-Extension/issues/2 --> Thank you @maoschanz +// https://discourse.gnome.org/t/how-can-i-move-the-workspace-switcher-popup-to-the-right/6940 --> Thank you @GdH + +const Main = imports.ui.main; +const WorkspaceSwitcherPopup = imports.ui.workspaceSwitcherPopup; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Convenience = Me.imports.convenience; + +let initWSP; + +function init() { + Convenience.initTranslations(); + initWSP = WorkspaceSwitcherPopup.WorkspaceSwitcherPopup.prototype._show; +} + +//------------------------------------------------------------------------------ + +function injectToFunction(parent, name, func) { + let origin = parent[name]; + parent[name] = function() { + let ret; + ret = origin.apply(this, arguments); + if (ret === undefined) + ret = func.apply(this, arguments); + return ret; + } + return origin; +} + +function removeInjection(object, injection, name) { + if (injection[name] === undefined) + delete object[name]; + else + object[name] = injection[name]; +} + +let injections=[]; + +//------------------------------------------------------------------------------ + +function enable() { + let settings = Convenience.getSettings('org.gnome.shell.extensions.move-workspaceSwitcherPopup'); + + injections['_redisplay'] = injectToFunction( + WorkspaceSwitcherPopup.WorkspaceSwitcherPopup.prototype, '_redisplay', function() { + + if(settings.get_boolean('hide')) { + WorkspaceSwitcherPopup.WorkspaceSwitcherPopup.prototype._show = function() { return false }; + } else { + WorkspaceSwitcherPopup.WorkspaceSwitcherPopup.prototype._show = initWSP; + let workArea = Main.layoutManager.getWorkAreaForMonitor(Main.layoutManager.primaryIndex); + let [, containerNatHeight] = this._container.get_preferred_height(global.screen_width); + let [, containerNatWidth] = this._container.get_preferred_width(containerNatHeight); + let h_percent = settings.get_int('horizontal'); + let v_percent = settings.get_int('vertical'); + + this._container.x = workArea.x + Math.floor((workArea.width - containerNatWidth) * (h_percent/100)); + this._container.y = workArea.y + Math.floor((workArea.height - containerNatHeight) * (v_percent/100)); + } + } + ); +} + +function disable() { + WorkspaceSwitcherPopup.WorkspaceSwitcherPopup.prototype._show = initWSP; + removeInjection(WorkspaceSwitcherPopup.WorkspaceSwitcherPopup.prototype, injections, '_redisplay'); +} diff --git a/.local/share/gnome-shell/extensions/move-workspaceSwitcherPopup@GithubUser699.github.com/metadata.json b/.local/share/gnome-shell/extensions/move-workspaceSwitcherPopup@GithubUser699.github.com/metadata.json new file mode 100644 index 0000000..fc7e78c --- /dev/null +++ b/.local/share/gnome-shell/extensions/move-workspaceSwitcherPopup@GithubUser699.github.com/metadata.json @@ -0,0 +1,14 @@ +{ + "_generated": "Generated by SweetTooth, do not edit", + "description": "Change the position of the WorkspaceSwitcherPopup", + "gettext-domain": "move-workspaceswitcherpopup", + "name": "Move Workspace Switcher Popup", + "shell-version": [ + "3.36", + "3.38", + "40" + ], + "url": "https://github.com/GithubUser699/Move-workspaceSwitcherPopup-GNOME-Extension", + "uuid": "move-workspaceSwitcherPopup@GithubUser699.github.com", + "version": 3 +} \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/move-workspaceSwitcherPopup@GithubUser699.github.com/prefs.js b/.local/share/gnome-shell/extensions/move-workspaceSwitcherPopup@GithubUser699.github.com/prefs.js new file mode 100644 index 0000000..047b3f4 --- /dev/null +++ b/.local/share/gnome-shell/extensions/move-workspaceSwitcherPopup@GithubUser699.github.com/prefs.js @@ -0,0 +1,140 @@ + +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + +const Gettext = imports.gettext.domain('move-workspaceswitcherpopup'); +const _ = Gettext.gettext; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Convenience = Me.imports.convenience; + +//------------------------------------------------------------------------------ +const WSPSettingsWidget = GObject.registerClass( +class WSPSettingsWidget extends Gtk.Grid { + + _init() { + super._init({ + row_homogeneous: true, + margin_top: 25, + margin_bottom: 25, + margin_start: 25, + margin_end: 25, + row_spacing: 30, + column_spacing: 20, + halign: Gtk.Align.FILL, + visible: true + }); + + this.SETTINGS = Convenience.getSettings('org.gnome.shell.extensions.move-workspaceSwitcherPopup'); + + //---------------------------------------------------------------------- + + let horizontalPercentageLabel = new Gtk.Label({ + label: _("Horizontal position (percentage)"), + use_markup: true, + halign: Gtk.Align.START, + visible: true + }); + + let horizontalPercentage = new Gtk.Scale({ + orientation: Gtk.Orientation.HORIZONTAL, + draw_value: true, + has_origin: false, + digits: 0, + halign: Gtk.Align.FILL, + hexpand: true, + visible: true + }); + + horizontalPercentage.add_mark(50, Gtk.PositionType.BOTTOM, null); + horizontalPercentage.set_range(0, 100); + horizontalPercentage.set_value(this.SETTINGS.get_int('horizontal')); + horizontalPercentage.set_increments(1, 1); + + horizontalPercentage.connect('value-changed', function(w) { + var value = w.get_value(); + this.SETTINGS.set_int('horizontal', value); + }.bind(this)); + + //---------------------------------------------------------------------- + + let verticalPercentageLabel = new Gtk.Label({ + label: _("Vertical position (percentage)"), + use_markup: true, + halign: Gtk.Align.START, + visible: true + }); + + let verticalPercentage = new Gtk.Scale({ + orientation: Gtk.Orientation.VERTICAL, + draw_value: true, + has_origin: false, + value_pos: Gtk.PositionType.LEFT, + digits: 0, + valign: Gtk.Align.FILL, + halign: Gtk.Align.CENTER, + hexpand: false, + vexpand: true, + visible: true + }); + + verticalPercentage.add_mark(50, Gtk.PositionType.RIGHT, null); + verticalPercentage.set_range(0, 100); + verticalPercentage.set_value(this.SETTINGS.get_int('vertical')); + verticalPercentage.set_increments(1, 1); + + verticalPercentage.connect('value-changed', function(w) { + var value = w.get_value(); + this.SETTINGS.set_int('vertical', value); + }.bind(this)); + + //---------------------------------------------------------------------- + + let hideSwitchLabel = new Gtk.Label({ + label: _("Hide Workspace Switcher Popup Window"), + use_markup: true, + halign: Gtk.Align.START, + visible: true + }); + + let hideSwitch = new Gtk.Switch({ + visible: true, + hexpand: true, + halign: Gtk.Align.CENTER, + valign: Gtk.Align.CENTER + }); + + hideSwitch.set_state(false); + hideSwitch.set_halign(Gtk.Align.START) + hideSwitch.set_state(this.SETTINGS.get_boolean('hide')); + + hideSwitch.connect('notify::active', function(widget) { + this.SETTINGS.set_boolean('hide', widget.active); + }.bind(this)); + + //---------------------------------------------------------------------- + + this.attach(horizontalPercentageLabel, 0, 0, 1, 1); + this.attach(horizontalPercentage, 1, 0, 1, 1); + this.attach(verticalPercentageLabel, 0, 1, 1, 2); + this.attach(verticalPercentage, 1, 1, 1, 2); + this.attach(hideSwitchLabel, 0, 3, 1, 1); + this.attach(hideSwitch, 1, 3, 1, 1); + } +}); + +//------------------------------------------------------------------------------ + +function init() { + Convenience.initTranslations(); +} + +// This is like the "enable" in extension.js : something called each time the +// user tries to access the settings' window +function buildPrefsWidget() { + let widget = new WSPSettingsWidget(); + + return widget; +} + diff --git a/.local/share/gnome-shell/extensions/move-workspaceSwitcherPopup@GithubUser699.github.com/schemas/gschemas.compiled b/.local/share/gnome-shell/extensions/move-workspaceSwitcherPopup@GithubUser699.github.com/schemas/gschemas.compiled new file mode 100644 index 0000000000000000000000000000000000000000..fc0b5221fb061ba8c6963e8772bd7ca32ce4f9a8 GIT binary patch literal 429 zcmaJ-y-EZz82tQM5G_Tpv)wC9K*ZY4Ev^tmxx&JA4VU0vvLta&M12E0!3VIh5o<f& zK^v<BUqt82-f^8FUuKw0@+EU{QhHO`NYk+i9CSQioQc3}gXkBd+v_LKDfIQ@_cyjN z-=l&af%yUvCOJP8L+eIL1g+{?sOzX~FwO=sa;@4PJAWCPvQkH56DzI!p=+)h9K;n_ zf^OlvmZmj(2lpWAr(V0@lkcrhR+z6)OZ&hACBGgD)|szD9{|r#A9wd_{2%5SbPaq) zO@5wk+0T3#`U~(iaU#wGlv?GZsmhvtJG|tkr=2%9&c;ri>!K;6hD9O_nZl4M3^@-& Q{u72I3ff#KZgSrH15YGhd;kCd literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/notification-position@drugo.dev/extension.js b/.local/share/gnome-shell/extensions/notification-position@drugo.dev/extension.js new file mode 100644 index 0000000..6e790bb --- /dev/null +++ b/.local/share/gnome-shell/extensions/notification-position@drugo.dev/extension.js @@ -0,0 +1,57 @@ +'use strict'; + +// This is a handy import we'll use to grab our extension's object +const Main = imports.ui.main; +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); + +// Like `init()` below, code *here* in the top-level of your script is executed +// when your extension is loaded. You MUST NOT make any changes to GNOME Shell +// here and typically you should do nothing but assign variables. +const PADDING = 20; +const monitorWidth = Main.layoutManager.primaryMonitor.width; +const monitorHeight = Main.layoutManager.primaryMonitor.height; +const messageListWidth = Main.panel.statusArea.dateMenu._messageList.actor.width; +const messageListHeight = Main.panel.statusArea.dateMenu._messageList.actor.height; + +// This function is called once when your extension is loaded, not enabled. This +// is a good time to setup translations or anything else you only do once. +// +// You MUST NOT make any changes to GNOME Shell, connect any signals or add any +// MainLoop sources here. +function init() { + log(`initializing ${Me.metadata.name} version ${Me.metadata.version}`); +} + +function left() +{ + return - monitorWidth + messageListWidth + PADDING; +} + +function right() +{ + return monitorWidth - messageListWidth - PADDING; +} + +// This function could be called after your extension is enabled, which could +// be done from GNOME Tweaks, when you log in or when the screen is unlocked. +// +// This is when you setup any UI for your extension, change existing widgets, +// connect signals or modify GNOME Shell's behaviour. +function enable() { + log(`enabling ${Me.metadata.name} version ${Me.metadata.version}`); + + Main.messageTray._bannerBin.x = right(); +} + +// This function could be called after your extension is uninstalled, disabled +// in GNOME Tweaks, when you log out or when the screen locks. +// +// Anything you created, modifed or setup in enable() MUST be undone here. Not +// doing so is the most common reason extensions are rejected during review! +function disable() { + log(`disabling ${Me.metadata.name} version ${Me.metadata.version}`); + Main.messageTray._bannerBin.x = 0; + Main.messageTray._bannerBin.y = 0; +} + diff --git a/.local/share/gnome-shell/extensions/notification-position@drugo.dev/metadata.json b/.local/share/gnome-shell/extensions/notification-position@drugo.dev/metadata.json new file mode 100644 index 0000000..4543525 --- /dev/null +++ b/.local/share/gnome-shell/extensions/notification-position@drugo.dev/metadata.json @@ -0,0 +1,14 @@ +{ + "_generated": "Generated by SweetTooth, do not edit", + "description": "Changes position of the notification banner from the default to the right side of the screen.", + "name": "Notification Banner Position", + "shell-version": [ + "3.36", + "3.38", + "40", + "41" + ], + "url": "https://github.com/brunodrugowick/notification-position-gnome-extension", + "uuid": "notification-position@drugo.dev", + "version": 5 +} \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/base.js b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/base.js new file mode 100644 index 0000000..f47c0fd --- /dev/null +++ b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/base.js @@ -0,0 +1,729 @@ +/******************************************************************************* + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see http://www.gnu.org/licenses/. + * ***************************************************************************** + * Original Author: Gopi Sankar Karmegam + ******************************************************************************/ +/* jshint moz:true */ + +const { GObject, GLib, Gvc } = imports.gi; + +const Signals = imports.signals; + +const PopupMenu = imports.ui.popupMenu; +const VolumeMenu = imports.ui.status.volume; +const Main = imports.ui.main; +const MessageTray = imports.ui.messageTray; + +const Config = imports.misc.config; +const ExtensionUtils = imports.misc.extensionUtils; +const Gettext = imports.gettext; + +const Me = ExtensionUtils.getCurrentExtension(); +const Lib = Me.imports.convenience; +const Prefs = Me.imports.prefs; + +ExtensionUtils.initTranslations(Me.metadata["gettext-domain"]); +const Domain = Gettext.domain(Me.metadata["gettext-domain"]); +const _ = Domain.gettext; +//const _ = Gettext.gettext; +const _d = Lib._log; + +const DISPLAY_OPTIONS = Prefs.DISPLAY_OPTIONS; +const SignalManager = Lib.SignalManager; + +var ProfileMenuItem = class ProfileMenuItem + extends PopupMenu.PopupMenuItem { + constructor(title, profileName) { + super(title); + this._init(title, profileName); + } + + _init(title, profileName) { + if (super._init) { + super._init(title); + } + _d("ProfileMenuItem: _init:" + title); + this.profileName = profileName; + this._ornamentLabel.set_style("min-width: 3em;margin-left: 3em;"); + this.setProfileActive(false); + } + + setProfileActive(active) { + if (active) { + this.setOrnament(PopupMenu.Ornament.DOT); + // this._ornamentLabel.text = "\u2727"; + this._ornamentLabel.text = "\u266A"; + if (this.add_style_pseudo_class) { + this.remove_style_pseudo_class('insensitive'); + } + else { + this.actor.remove_style_pseudo_class('insensitive'); + } + } + else { + this.setOrnament(PopupMenu.Ornament.NONE); + if (this.add_style_pseudo_class) { + this.add_style_pseudo_class('insensitive'); + } + else { + this.actor.add_style_pseudo_class('insensitive'); + } + } + } + + setVisibility(visibility) { + this.actor.visible = visibility; + } +} + +var SoundDeviceMenuItem = class SoundDeviceMenuItem extends PopupMenu.PopupImageMenuItem { + constructor(id, title, icon_name, profiles) { + super(title, icon_name); + this._init(id, title, icon_name, profiles); + } + + _init(id, title, icon_name, profiles) { + if (super._init) { + super._init(title, icon_name); + } + _d("SoundDeviceMenuItem: _init:" + title); + this.id = id; + this.title = title; + this.icon_name = icon_name; + this.profiles = (profiles) ? profiles : []; + + this.profilesitems = new Map(); + for (let profile of this.profiles) { + let profileName = profile.name; + if (!this.profilesitems.has(profileName)) { + let pItem = new ProfileMenuItem(_("Profile: ") + profile.human_name, profileName); + this.profilesitems.set(profileName, pItem); + pItem.connect('activate', () => { + _d("Activating Profile:" + id + profileName); + this.emit("profile-activated", this.id, profileName); + }); + } + } + + this.connect('activate', () => { + _d("Device Change request for " + id); + _d("Emitting Signal..."); + this.emit("device-activated", this.id); + }); + this.available = true; + this.activeProfile = ""; + this.activeDevice = false; + this.visible = false; + this._displayOption = DISPLAY_OPTIONS.INITIAL; + } + + isAvailable() { + return this.available; + } + + setAvailable(_ac) { + this.available = _ac; + } + + setActiveProfile(_p) { + if (_p && this.activeProfile != _p) { + if (this.profilesitems.has(this.activeProfile)) { + this.profilesitems.get(this.activeProfile).setProfileActive(false); + } + this.activeProfile = _p; + if (this.profilesitems.has(_p)) { + this.profilesitems.get(_p).setProfileActive(true); + } + } + } + + setVisibility(_v) { + this.actor.visible = _v; + if (!_v) { + this.profilesitems.forEach((p) => p.setVisibility(false)); + } + this.visible = _v; + }; + + setTitle(_t) { + _d("SoundDeviceMenuItem: " + "setTitle: " + this.title + "->" + _t); + this.title = _t; + this.label.text = _t; + } + + isVisible() { + return this.visible; + } + + setActiveDevice(_a) { + this.activeDevice = _a; + if (!_a) { + this.setOrnament(PopupMenu.Ornament.NONE); + } + else { + this.setOrnament(PopupMenu.Ornament.CHECK); + this._ornamentLabel.text = '\u266B'; + } + } + + setProfileVisibility(_v) { + this.profilesitems.forEach(p => + p.setVisibility(_v && this.canShowProfile())); + } + + canShowProfile() { + return (this.isVisible() && this.profilesitems.size >= 1); + } + + setDisplayOption(displayOption) { + _d("Setting Display Option to : " + displayOption); + this._displayOption = displayOption; + } + + getDisplayOption() { + return this._displayOption; + } +} + +if (parseFloat(Config.PACKAGE_VERSION) >= 3.34) { + ProfileMenuItem = GObject.registerClass({ GTypeName: 'ProfileMenuItem' }, ProfileMenuItem); + + SoundDeviceMenuItem = GObject.registerClass({ + GTypeName: "SoundDeviceMenuItem", + Signals: { + "device-activated": { + param_types: [GObject.TYPE_INT] + }, + "profile-activated": { + param_types: [GObject.TYPE_INT, GObject.TYPE_STRING] + } + } + }, SoundDeviceMenuItem); +} + +var SoundDeviceChooserBase = class SoundDeviceChooserBase { + + constructor(deviceType) { + _d("SDC: init"); + this.menuItem = new PopupMenu.PopupSubMenuMenuItem(_("Extension initialising..."), true); + this.deviceType = deviceType; + this._devices = new Map(); + let _control = this._getMixerControl(); + this._settings = ExtensionUtils.getSettings(); + _d("Constructor:" + deviceType); + + this._setLog(); + this._signalManager = new SignalManager(); + this._signalManager.addSignal(this._settings, "changed::" + Prefs.ENABLE_LOG, this._setLog.bind(this)); + + if (_control.get_state() == Gvc.MixerControlState.READY) { + this._onControlStateChanged(_control); + } + else { + this._controlStateChangeSignal = this._signalManager.addSignal(_control, "state-changed", this._onControlStateChanged.bind(this)); + } + + this._signalManager.addSignal(this.menuItem.menu, "open-state-changed", this._onSubmenuOpenStateChanged.bind(this)); + } + + _getMixerControl() { return VolumeMenu.getMixerControl(); } + + _setLog() { Lib.setLog(this._settings.get_boolean(Prefs.ENABLE_LOG)); } + + _onControlStateChanged(control) { + if (control.get_state() == Gvc.MixerControlState.READY) { + + this._signalManager.addSignal(control, this.deviceType + "-added", this._deviceAdded.bind(this)); + this._signalManager.addSignal(control, this.deviceType + "-removed", this._deviceRemoved.bind(this)); + this._signalManager.addSignal(control, "active-" + this.deviceType + "-update", this._deviceActivated.bind(this)); + + this._signalManager.addSignal(this._settings, "changed::" + Prefs.HIDE_ON_SINGLE_DEVICE, this._setChooserVisibility.bind(this)); + this._signalManager.addSignal(this._settings, "changed::" + Prefs.SHOW_PROFILES, this._setProfileVisibility.bind(this)); + this._signalManager.addSignal(this._settings, "changed::" + Prefs.ICON_THEME, this._setIcons.bind(this)); + this._signalManager.addSignal(this._settings, "changed::" + Prefs.HIDE_MENU_ICONS, this._setIcons.bind(this)); + this._signalManager.addSignal(this._settings, "changed::" + Prefs.PORT_SETTINGS, this._resetDevices.bind(this)); + this._signalManager.addSignal(this._settings, "changed::" + Prefs.OMIT_DEVICE_ORIGIN, this._refreshDeviceTitles.bind(this)); + + this._show_device_signal = Prefs["SHOW_" + this.deviceType.toUpperCase() + "_DEVICES"]; + + this._signalManager.addSignal(this._settings, "changed::" + this._show_device_signal, this._setVisibility.bind(this)); + + this._portsSettings = Prefs.getPortsFromSettings(this._settings); + + /** + * There is no direct way to get all the UI devices from + * mixercontrol. When enabled after shell loads, the signals + * will not be emitted, a simple hack to look for ids, until any + * uidevice is not found. The UI devices are always serialed + * from from 1 to n + */ + + let id = 0; + + let dummyDevice = new Gvc.MixerUIDevice(); + let maxId = dummyDevice.get_id(); + + _d("Max Id:" + maxId); + + while (++id < maxId) { + this._deviceAdded(control, id); + } + let defaultStream = this.getDefaultStream(control); + if (defaultStream) { + let defaultDevice = control.lookup_device_from_stream(defaultStream); + if (defaultDevice) { + this._deviceActivated(control, defaultDevice.get_id()); + } + } + + if (this._controlStateChangeSignal) { + this._controlStateChangeSignal.disconnect(); + delete this._controlStateChangeSignal; + } + this._setVisibility(); + } + } + + _onSubmenuOpenStateChanged(_menu, opened) { + _d(this.deviceType + "-Submenu is now open?: " + opened); + if (opened) { // Actions when submenu is opening + this._setActiveProfile(); + } + else { // Actions when submenu is closing + } + } + + _deviceAdded(control, id, dontcheck) { + let obj = this._devices.get(id); + let uidevice = this.lookupDeviceById(control, id); + + _d("Added - " + id); + + if (!obj) { + if (this._isDeviceInValid(uidevice)) { + return null; + } + + let title = this._getDeviceTitle(uidevice); + + let icon = uidevice.get_icon_name(); + if (icon == null || icon.trim() == "") + icon = this.getDefaultIcon(); + icon = this._getIcon(icon); + + obj = new SoundDeviceMenuItem(id, title, icon, Lib.getProfiles(control, uidevice)); + obj.connect("device-activated", (item, id) => this._changeDeviceBase(id)); + obj.connect("profile-activated", (item, id, name) => this._profileChangeCallback(id, name)); + + this.menuItem.menu.addMenuItem(obj); + obj.profilesitems.forEach(i => this.menuItem.menu.addMenuItem(i)); + + this._devices.set(id, obj); + } + else if (!obj.isAvailable()) + obj.setAvailable(true); + else + return; + + + _d("Device Name:" + obj.title); + + _d("Added: " + id + ":" + uidevice.description + ":" + uidevice.port_name + ":" + uidevice.origin); + + let stream = control.get_stream_from_device(uidevice); + if (stream) { + obj.setActiveProfile(uidevice.get_active_profile()); + } + + if (!dontcheck && !this._canShowDevice(control, uidevice, obj, uidevice.port_available)) { + _d("This device is hidden in settings, lets hide...") + this._deviceRemoved(control, id, true); + } + else { + this._setChooserVisibility(); + this._setVisibility(); + } + } + + _profileChangeCallback(id, profileName) { + let control = this._getMixerControl(); + let uidevice = this.lookupDeviceById(control, id); + if (!uidevice) { + this._deviceRemoved(control, id); + } + else { + _d("i am setting profile, " + profileName + ":" + uidevice.description + ":" + uidevice.port_name); + if (id != this._activeDeviceId) { + _d("Changing active device to " + uidevice.description + ":" + uidevice.port_name); + this._changeDeviceBase(id, control); + } + control.change_profile_on_selected_device(uidevice, profileName); + //this._setDeviceActiveProfile(control, this._devices.get(id)); //"Races" change_profile_...(...) and reports the old state + } + } + + _deviceRemoved(control, id, dontcheck) { + let obj = this._devices.get(id); + + if (obj && obj.isAvailable()) { + _d("Removed: " + id + ":" + obj.title); + /* + let uidevice = this.lookupDeviceById(control,id); + if (!dontcheck && this._canShowDevice(control, uidevice, obj, false)) { + _d('Device removed, but not hiding as its set to be shown always'); + return; + }*/ + obj.setVisibility(false); + obj.setAvailable(false); + + /* + if (this.deviceRemovedTimout) { + GLib.source_remove(this.deviceRemovedTimout); + this.deviceRemovedTimout = null; + } + */ + /** + * If the active uidevice is removed, then need to activate the + * first available uidevice. However for some cases like Headphones, + * when the uidevice is removed, Speakers are automatically + * activated. So, lets wait for sometime before activating. + */ + /* THIS MAY NOT BE NEEDED AS SHELL SEEMS TO ACTIVATE NEXT DEVICE + this.deviceRemovedTimout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1500, function() { + _d("Device Removed timeout"); + if (obj === this._activeDevice) { + let device = Object.keys(this._devices).map((id) => this._devices[id]).find(({active}) => active === true); + if(device){ + this._changeDeviceBase(device.id, this._getMixerControl()); + } + } + this.deviceRemovedTimout = null; + return false; + }.bind(this)); + */ + this._setChooserVisibility(); + this._setVisibility(); + } + } + + _deviceActivated(control, id) { + _d("Activated:- " + id); + let obj = this._devices.get(id); + if (!obj) { + _d("Activated device not found in the list of devices, try to add"); + this._deviceAdded(control, id); + obj = this._devices.get(id); + } + if (obj && id != this._activeDeviceId) { + _d("Activated: " + id + ":" + obj.title); + if (this._settings.get_boolean(Prefs.CANNOT_ACTIVATE_HIDDEN_DEVICE) + && obj.getDisplayOption() === DISPLAY_OPTIONS.HIDE_ALWAYS) { + _d("Preference does not allow this hidden device to be activated, fallback to the previous aka original device"); + let device = null; + + if (this._activeDeviceId) { + device = this._devices.get(this._activeDeviceId); + } + else { + device = Array.from(this._devices.values()).find(x => x.isAvailable()); + } + + if (device) { + _notify(Me.metadata["name"] + " " + _("Extension changed active sound device."), + _("Activated device is hidden in Port Settings.") + " \n" + + _("Deactivated Device: ") + obj.title + " \n" + _("Activated Device: ") + device.title + " \n" + + _("Disable in extension preferences to avoid this behaviour."), + device.icon_name); + this._changeDeviceBase(device.id, control); + } + else { + this._activateDeviceMenuItem(control, id, obj); + } + + } + else { + this._activateDeviceMenuItem(control, id, obj); + } + } + } + + _activateDeviceMenuItem(control, id, obj) { + let prevActiveDevce = this._activeDeviceId; + this._activeDeviceId = id; + if (prevActiveDevce) { + let prevObj = this._devices.get(prevActiveDevce); + if (prevObj) { + prevObj.setActiveDevice(false); + if (prevObj.getDisplayOption() === DISPLAY_OPTIONS.HIDE_ALWAYS) { + _d("Hiding previously activated device as it is set to hidden always"); + this._deviceRemoved(control, prevActiveDevce, true); + } + } + } + obj.setActiveDevice(true); + if (!obj.isAvailable()) { + _d("Activated device hidden, try to add"); + this._deviceAdded(control, id); + } + + this.menuItem.label.text = obj.title; + + if (!this._settings.get_boolean(Prefs.HIDE_MENU_ICONS)) { + this.menuItem.icon.icon_name = obj.icon_name; + } else { + this.menuItem.icon.gicon = null; + } + } + + _changeDeviceBase(id, control) { + if (!control) { + control = this._getMixerControl(); + } + let uidevice = this.lookupDeviceById(control, id); + if (uidevice) { + this.changeDevice(control, uidevice); + } + else { + this._deviceRemoved(control, id); + } + } + + _setActiveProfile() { + let control = this._getMixerControl(); + this._devices.forEach(device => { + if (device.isAvailable()) { + this._setDeviceActiveProfile(control, device); + } + }); + } + + _setDeviceActiveProfile(control, device) { + if (!device || !device.isAvailable()) { + return; + } + + let uidevice = this.lookupDeviceById(control, device.id); + if (!uidevice) { + this._deviceRemoved(control, device.id); + } + else { + let activeProfile = uidevice.get_active_profile(); + _d("Active Profile:" + activeProfile); + device.setActiveProfile(activeProfile); + } + } + + _getAvailableDevices() { + return Array.from(this._devices.values()).filter(x => x.isAvailable()); + } + + _getDeviceVisibility() { + let hideChooser = this._settings.get_boolean(Prefs.HIDE_ON_SINGLE_DEVICE); + if (hideChooser) { + return (this._getAvailableDevices().length > 1); + } + else { + return true; + } + } + + _setChooserVisibility() { + let visibility = this._getDeviceVisibility(); + this._getAvailableDevices().forEach(x => x.setVisibility(visibility)) + + //this.menuItem._triangleBin.visible = visibility; + //this.menuItem.actor.visible = visibility; + this._setProfileVisibility(); + } + + _setVisibility() { + if (!this._settings.get_boolean(this._show_device_signal)) + this.menuItem.actor.visible = false; + else + // if setting says to show device, check for any device, otherwise + // hide the "actor" + this.menuItem.actor.visible = this._getDeviceVisibility();//(Array.from(this._devices.values()).some(x => x.isAvailable())); + + this.emit('update-visibility', this.menuItem.actor.visible); + } + + _setProfileVisibility() { + let visibility = this._settings.get_boolean(Prefs.SHOW_PROFILES); + this._getAvailableDevices().forEach(device => device.setProfileVisibility(visibility)); + } + + _getIcon(name) { + let iconsType = this._settings.get_string(Prefs.ICON_THEME); + switch (iconsType) { + case Prefs.ICON_THEME_COLORED: + return name; + case Prefs.ICON_THEME_MONOCHROME: + return name + "-symbolic"; + default: + //return "none"; + return null; + } + } + + _setIcons() { + // Set the icons in the selection list + let control = this._getMixerControl(); + this._devices.forEach((device, id) => { + let uidevice = this.lookupDeviceById(control, id); + if (uidevice) { + let icon = uidevice.get_icon_name(); + if (icon == null || icon.trim() == "") + icon = this.getDefaultIcon(); + _d(icon + " _setIcons") + device.setIcon(this._getIcon(icon)); + } + }); + + // These indicate the active device, which is displayed directly in the + // Gnome menu, not in the list. + if (!this._settings.get_boolean(Prefs.HIDE_MENU_ICONS)) { + this.menuItem.icon.icon_name = this._getIcon(this._devices.get(this._activeDeviceId).icon_name); + } else { + this.menuItem.icon.icon_name = null; + } + } + + _getDeviceDisplayOption(control, uidevice, obj) { + let displayOption = DISPLAY_OPTIONS.DEFAULT; + if (uidevice && uidevice.port_name != null && uidevice.description != null) { + let stream = control.get_stream_from_device(uidevice); + let cardName = null; + if (stream) { + let cardId = stream.get_card_index(); + if (cardId != null) { + _d("Card Index:" + cardId); + let _card = Lib.getCard(cardId); + if (_card) { + cardName = _card.name; + } + else { + //card id found, but not available in list + return DISPLAY_OPTIONS.DEFAULT; + } + _d("Card Name:" + cardName); + } + } + + _d("P:" + uidevice.port_name + "==" + uidevice.description + "==" + cardName + "==" + uidevice.origin); + + let matchedPort = this._portsSettings.find(port => (port + && port.name == uidevice.port_name + && port.human_name == uidevice.description + && (!cardName || port.card_name == cardName) + && (cardName || port.card_description == uidevice.origin))); + + if (matchedPort) { + displayOption = matchedPort.display_option; + } + } + + obj && obj.setDisplayOption(displayOption); + + return displayOption; + } + + _canShowDevice(control, uidevice, obj, defaultValue) { + if (!uidevice || !this._portsSettings || uidevice.port_name == null + || uidevice.description == null || (this._activeDeviceId && this._activeDeviceId == uidevice.get_id())) { + return defaultValue; + } + + let displayOption = obj.getDisplayOption(); + if (displayOption === DISPLAY_OPTIONS.INITIAL) { + displayOption = this._getDeviceDisplayOption(control, uidevice, obj); + } + + if (displayOption === DISPLAY_OPTIONS.SHOW_ALWAYS) { + _d("Display Device due Preference:" + displayOption); + return true; + } + else if (displayOption === DISPLAY_OPTIONS.HIDE_ALWAYS) { + _d("Hide Device due Preference:" + displayOption); + return false; + } + else { + _d("Default Device due Preference:" + displayOption); + return defaultValue; + } + } + + _resetDevices() { + this._portsSettings = Prefs.getPortsFromSettings(this._settings); + let control = this._getMixerControl(); + this._devices.forEach((device, id) => { + device.setDisplayOption(DISPLAY_OPTIONS.INITIAL); + let uidevice = this.lookupDeviceById(control, id); + if (this._isDeviceInValid(uidevice)) + _d("Device is invalid"); + else if (this._canShowDevice(control, uidevice, device, uidevice.port_available)) + this._deviceAdded(control, id, true); + else + this._deviceRemoved(control, id, true); + }); + } + + _isDeviceInValid(uidevice) { + return (!uidevice || (uidevice.description != null && uidevice.description.match(/Dummy\s+(Output|Input)/gi))); + } + + _refreshDeviceTitles(){ + let control = this._getMixerControl(); + this._devices.forEach((device, id) => { + let uidevice = this.lookupDeviceById(control, id); + let title = this._getDeviceTitle(uidevice); + + device.setTitle(title); + }); + + let activeDevice = this._devices.get(this._activeDeviceId); + this.menuItem.label.text = activeDevice.title; + } + + _getDeviceTitle(uidevice) { + let title = uidevice.description; + if (!this._settings.get_boolean(Prefs.OMIT_DEVICE_ORIGIN) && uidevice.origin != "") + title += " - " + uidevice.origin; + + return title; + } + + destroy() { + this._signalManager.disconnectAll(); + if (this.deviceRemovedTimout) { + GLib.source_remove(this.deviceRemovedTimout); + this.deviceRemovedTimout = null; + } + if (this.activeProfileTimeout) { + GLib.source_remove(this.activeProfileTimeout); + this.activeProfileTimeout = null; + } + this.menuItem.destroy(); + } + +}; + +Signals.addSignalMethods(SoundDeviceChooserBase.prototype); + +function _notify(msg, details, icon_name) { + let source = new MessageTray.Source(Me.metadata["name"], icon_name); + Main.messageTray.add(source); + let notification = new MessageTray.Notification(source, msg, details); + //notification.setTransient(true); + source.showNotification(notification); +} diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/convenience.js b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/convenience.js new file mode 100644 index 0000000..85f711c --- /dev/null +++ b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/convenience.js @@ -0,0 +1,352 @@ +/******************************************************************************* + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see <http://www.gnu.org/licenses/>. + * ***************************************************************************** + * Original Author: Gopi Sankar Karmegam + ******************************************************************************/ +/* jshint moz:true */ + +const ByteArray = imports.byteArray; +const { Gio, GLib } = imports.gi; +const ExtensionUtils = imports.misc.extensionUtils; + +const Me = ExtensionUtils.getCurrentExtension(); +const Prefs = Me.imports.prefs; + +var DEBUG = false; + +var logWrap; +if (log != undefined) { + logWrap = log; +} +else { + logWrap = global.log +} + + +let cards; + +function getCard(card_index) { + if (!cards || Object.keys(cards).length == 0) { + refreshCards(); + } + return cards[card_index]; +} + +function getCardByName(card_name) { + if (!cards || Object.keys(cards).length == 0) { + refreshCards(); + } + return Object.keys(cards).map((index) => cards[index]).find(({ name }) => name === card_name); +} + +function getProfiles(control, uidevice) { + let stream = control.lookup_stream_id(uidevice.get_stream_id()); + if (stream) { + if (!cards || Object.keys(cards).length == 0 || !cards[stream.card_index]) { + refreshCards(); + } + if (cards && cards[stream.card_index]) { + _log("Getting profile form stream id " + uidevice.port_name); + let profiles; + if ((profiles = getProfilesForPort(uidevice.port_name, cards[stream.card_index]))) { + return profiles; + } + } + } + else { + /* Device is not active device, lets try match with port name */ + refreshCards(); + for (let card of Object.values(cards)) { + let profiles; + _log("Getting profile from cards " + uidevice.port_name + " for card id " + card.id); + if ((profiles = getProfilesForPort(uidevice.port_name, card))) { + return profiles; + } + } + } + return []; +} + +let ports; +function getPorts(refresh) { + if (!ports || ports.length == 0 || refresh) { + refreshCards(); + } + return ports; +} + +function isCmdFound(cmd) { + try { + let [result, out, err, exit_code] = GLib.spawn_command_line_sync(cmd); + return true; + } + catch (e) { + _log("ERROR: " + cmd + " execution failed. " + e); + return false; + } +} + +function refreshCards() { + cards = {}; + ports = []; + let _settings = ExtensionUtils.getSettings(); + let error = false; + let newProfLogic = _settings.get_boolean(Prefs.NEW_PROFILE_ID_DEPRECATED); + + /** This block should be removed in the next release along the setting schema correct */ + if (!newProfLogic) { + _settings.set_boolean(Prefs.NEW_PROFILE_ID, false); + _settings.reset(Prefs.NEW_PROFILE_ID_DEPRECATED); + } + else { + newProfLogic = _settings.get_boolean(Prefs.NEW_PROFILE_ID); + } + + if (newProfLogic) { + _log("New logic"); + let pyLocation = Me.dir.get_child("utils/pa_helper.py").get_path(); + let pythonExec = ["python", "python3", "python2"].find(cmd => isCmdFound(cmd)); + if (!pythonExec) { + _log("ERROR: Python not found. fallback to default mode"); + _settings.set_boolean(Prefs.NEW_PROFILE_ID, false); + Gio.Settings.sync(); + newProfLogic = false; + } + else { + try { + _log("Python found." + pythonExec); + let [result, out, err, exit_code] = GLib.spawn_command_line_sync(pythonExec + " " + pyLocation); + // _log("result" + result +" out"+out + " exit_code" + + // exit_code + "err" +err); + if (result && !exit_code) { + if (out instanceof Uint8Array) { + out = ByteArray.toString(out); + } + let obj = JSON.parse(out); + cards = obj["cards"]; + ports = obj["ports"]; + } + } + catch (e) { + error = true; + _log("ERROR: Python execution failed. fallback to default mode" + e); + _settings.set_boolean(Prefs.NEW_PROFILE_ID, false); + Gio.Settings.sync(); + } + } + } + //error = true; + if (!newProfLogic || error) { + _log("Old logic"); + try { + let env = GLib.get_environ(); + env = GLib.environ_setenv(env, "LANG", "C", true); + let [result, out, err, exit_code] = GLib.spawn_sync(null, ["pactl", "list", "cards"], env, GLib.SpawnFlags.SEARCH_PATH, null); + //_log(result+"--"+out+"--"+ err+"--"+ exit_code) + if (result && !exit_code) { + parseOutput(out); + } + } + catch (e) { + _log("ERROR: pactl execution failed. No ports/profiles will be displayed." + e); + } + } + //_log(Array.isArray(cards)); + //_log(JSON.stringify(cards)); + //_log(Array.isArray(ports)); + //_log(JSON.stringify(ports)); +} + +function parseOutput(out) { + let lines; + if (out instanceof Uint8Array) { + lines = ByteArray.toString(out).split("\n"); + } else { + lines = out.toString().split("\n"); + } + + let cardIndex; + let parseSection = "CARDS"; + let port; + let matches; + // _log("Unmatched line:" + out); + while (lines.length > 0) { + let line = lines.shift(); + + if ((matches = /^Card\s#(\d+)$/.exec(line))) { + cardIndex = matches[1]; + if (!cards[cardIndex]) { + cards[cardIndex] = { "index": cardIndex, "profiles": [], "ports": [] }; + } + } + else if ((matches = /^\t*Name:\s+(.*?)$/.exec(line)) && cards[cardIndex]) { + cards[cardIndex].name = matches[1]; + parseSection = "CARDS" + } + else if (line.match(/^\tProperties:$/) && parseSection == "CARDS") { + parseSection = "PROPS"; + } + else if (line.match(/^\t*Profiles:$/)) { + parseSection = "PROFILES"; + } + else if (line.match(/^\t*Ports:$/)) { + parseSection = "PORTS"; + } + else if (cards[cardIndex]) { + switch (parseSection) { + case "PROPS": + if ((matches = /alsa\.card_name\s+=\s+"(.*?)"/.exec(line))) { + cards[cardIndex].alsa_name = matches[1]; + } + else if ((matches = /device\.description\s+=\s+"(.*?)"/.exec(line))) { + cards[cardIndex].card_description = matches[1]; + } + break; + case "PROFILES": + if ((matches = /.*?((?:output|input)[^+]*?):\s(.*?)\s\(sinks:.*?(?:available:\s*(.*?))*\)/.exec(line))) { + let availability = matches[3] ? matches[3] : "yes" //If no availability in out, assume profile is available + + cards[cardIndex].profiles.push({ + "name": matches[1], + "human_name": matches[2], + "available": (availability === "yes") ? 1 : 0 + }); + } + break; + case "PORTS": + if ((matches = /\t*(.*?):\s(.*)\s\(.*?priority:/.exec(line))) { + port = { + "name": matches[1], + "human_name": matches[2], + "card_name": cards[cardIndex].name, + "card_description": cards[cardIndex].card_description + }; + cards[cardIndex].ports.push(port); + ports.push(port); + } + else if (port && (matches = /\t*Part of profile\(s\):\s(.*)/.exec(line))) { + let profileStr = matches[1]; + port.profiles = profileStr.split(", "); + port = null; + } + break; + } + } + } + if (ports) { + ports.forEach(p => { + p.direction = p.profiles + .filter(pr => pr.indexOf("+input:") == -1) + .some(pr => (pr.indexOf("output:") >= 0)) ? "Output" : "Input"; + }); + } +} + +var Signal = class Signal { + + constructor(signalSource, signalName, callback) { + this._signalSource = signalSource; + this._signalName = signalName; + this._signalCallback = callback; + } + + connect() { + this._signalId = this._signalSource.connect(this._signalName, this._signalCallback); + } + + disconnect() { + if (this._signalId) { + this._signalSource.disconnect(this._signalId); + this._signalId = null; + } + } +} + +var SignalManager = class SignalManager { + constructor() { + this._signalsBySource = new Map(); + } + + addSignal(signalSource, signalName, callback) { + let obj = null; + if (signalSource && signalName && callback) { + obj = new Signal(signalSource, signalName, callback); + obj.connect(); + + if (!this._signalsBySource.has(signalSource)) { + this._signalsBySource.set(signalSource, []); + } + this._signalsBySource.get(signalSource).push(obj) + //_log(this._signalsBySource.get(signalSource).length + "Signal length"); + } + return obj; + } + + disconnectAll() { + this._signalsBySource.forEach(signals => this._disconnectSignals(signals)); + } + + disconnectBySource(signalSource) { + if (this._signalsBySource.has(signalSource)) { + this._disconnectSignals(this._signalsBySource.get(signalSource)); + } + } + + _disconnectSignals(signals) { + while (signals.length) { + var signal = signals.shift(); + signal.disconnect(); + signal = null; + } + } +} + + +function getProfilesForPort(portName, card) { + if (card.ports) { + let port = card.ports.find(port => (portName === port.name)); + if (port) { + if (port.profiles) { + return card.profiles.filter(profile => ( + profile.name.indexOf("+input:") == -1 + && profile.available === 1 + && port.profiles.includes(profile.name) + )); + } + } + } + return null; +} + +function setLog(value) { + DEBUG = value; +} + +function _log(msg) { + if (DEBUG == true) { + // global.log("SDC Debug: " + msg); + logWrap("SDC Debug: " + msg); + } +} + +function dump(obj) { + var propValue; + for (var propName in obj) { + try { + propValue = obj[propName]; + _log(propName + "=" + propValue); + } + catch (e) { _log(propName + "!!!Error!!!"); } + } +} diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/extension.js b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/extension.js new file mode 100644 index 0000000..81f5774 --- /dev/null +++ b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/extension.js @@ -0,0 +1,267 @@ +/******************************************************************************* + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see <http://www.gnu.org/licenses/>. + * ***************************************************************************** + * Original Author: Gopi Sankar Karmegam + ******************************************************************************/ +/* jshint moz:true */ + +const { GObject } = imports.gi; +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Base = Me.imports.base; +const Lib = Me.imports.convenience; +const _d = Lib._log; +const SignalManager = Lib.SignalManager; +const Prefs = Me.imports.prefs; +const Main = imports.ui.main; +const PopupMenu = imports.ui.popupMenu; + +var SoundOutputDeviceChooser = class SoundOutputDeviceChooser + extends Base.SoundDeviceChooserBase { + constructor() { + super("output"); + } + lookupDeviceById(control, id) { + return control.lookup_output_id(id); + } + changeDevice(control, uidevice) { + control.change_output(uidevice); + } + getDefaultStream(control) { + return control.get_default_sink(); + } + getDefaultIcon() { + return "audio-card"; + } +}; + +var SoundInputDeviceChooser = class SoundInputDeviceChooser + extends Base.SoundDeviceChooserBase { + constructor() { + super("input"); + } + lookupDeviceById(control, id) { + return control.lookup_input_id(id); + } + changeDevice(control, uidevice) { + control.change_input(uidevice); + } + getDefaultStream(control) { + return control.get_default_source(); + } + getDefaultIcon() { + return "audio-input-microphone"; + } +}; + +var VolumeMenuInstance = class VolumeMenuInstance { + constructor(volumeMenu, settings) { + this._settings = settings; + + this._volumeMenu = volumeMenu; + this._input = this._volumeMenu._input; + + this._overrideFunctions(); + this._setSliderVisiblity(); + + this._signalManager = new SignalManager(); + this._signalManager.addSignal(this._settings, "changed::" + + Prefs.SHOW_INPUT_SLIDER, this._setSliderVisiblity.bind(this)); + } + _overrideFunctions() { + // Fix the indicator when using SHOW_INPUT_SLIDER. + // If not applied when SHOW_INPUT_SLIDER=True indication of mic being used will be on (even when not used) + this._volumeMenu._getInputVisibleOriginal = this._volumeMenu.getInputVisible; + this._volumeMenu._getInputVisibleCustom = function() { + return this._input._stream != null && this._input._showInput; + }; + this._volumeMenu.getInputVisible = this._volumeMenu._getInputVisibleCustom; + + this._input._updateVisibilityOriginal = this._input._updateVisibility; + this._input._updateVisibilityCustom = function() { + let old_state_visible = this.item.visible; + let visible = this._shouldBeVisible(); + + if(old_state_visible != visible){ + this.item.visible = visible; + } else { + this.item.notify('visible'); + } + }; + this._input._updateVisibility = this._input._updateVisibilityCustom; + + // Makes slider visible when SHOW_INPUT_SLIDER=True + this._input._showInputSlider = this._settings.get_boolean(Prefs.SHOW_INPUT_SLIDER); + this._input._shouldBeVisibleOriginal = this._input._shouldBeVisible; + this._input._shouldBeVisibleCustom = function() { + return this._showInputSlider && (this._stream != null) || this._shouldBeVisibleOriginal(); + }; + this._input._shouldBeVisible = this._input._shouldBeVisibleCustom; + } + _setSliderVisiblity() { + this._input._showInputSlider = this._settings.get_boolean(Prefs.SHOW_INPUT_SLIDER); + this._input._maybeShowInput(); + } + destroy() { + this._signalManager.disconnectAll(); + delete this._signalManager; + this._volumeMenu.getInputVisible = this._volumeMenu._getInputVisibleOriginal; + this._input._updateVisibility = this._input._updateVisibilityOriginal; + this._input._shouldBeVisible = this._input._shouldBeVisibleOriginal; + + this._input._maybeShowInput(); + + delete this._volumeMenu['_getInputVisibleOriginal']; + delete this._volumeMenu['_getInputVisibleCustom']; + delete this._input['_updateVisibilityOriginal']; + delete this._input['_updateVisibilityCustom']; + delete this._input['_shouldBeVisibleOriginal']; + delete this._input['_shouldBeVisibleCustom']; + delete this._input['_showInputSlider']; // variable + } +} + +var SDCInstance = class SDCInstance { + constructor() { + } + + enable() { + this._settings = ExtensionUtils.getSettings(); + this._signalManager = new SignalManager(); + this._aggregateMenu = Main.panel.statusArea.aggregateMenu; + this._volume = this._aggregateMenu._volume; + this._volumeMenu = this._volume._volumeMenu; + this._aggregateLayout = this._aggregateMenu.menu.box.get_layout_manager(); + let theme = imports.gi.Gtk.IconTheme.get_default(); + if (theme != null) { + let iconPath = Me.dir.get_child('icons'); + if (iconPath != null && iconPath.query_exists(null)) { + theme.append_search_path(iconPath.get_path()); + } + } + + if (this._outputInstance == null) { + this._outputInstance = new SoundOutputDeviceChooser(); + } + if (this._inputInstance == null) { + this._inputInstance = new SoundInputDeviceChooser(); + } + + if (this._volumeMenuInstance == null) { + this._volumeMenuInstance = new VolumeMenuInstance(this._volumeMenu, this._settings); + } + + this._addMenuItem(this._volumeMenu, this._volumeMenu._output.item, this._outputInstance.menuItem); + this._addMenuItem(this._volumeMenu, this._volumeMenu._input.item, this._inputInstance.menuItem); + this._expandVolMenu(); + + this._signalManager.addSignal(this._settings, "changed::" + Prefs.EXPAND_VOL_MENU, this._expandVolMenu.bind(this)); + this._signalManager.addSignal(this._settings, "changed::" + Prefs.INTEGRATE_WITH_SLIDER, this._switchSubmenuMenu.bind(this)); + this._signalManager.addSignal(this._outputInstance, "update-visibility", this._updateMenuVisibility.bind(this)); + this._signalManager.addSignal(this._inputInstance, "update-visibility", this._updateMenuVisibility.bind(this)); + + } + + _addMenuItem(_volumeMenu, checkItem, menuItem) { + let menuItems = _volumeMenu._getMenuItems(); + let i = menuItems.findIndex(elem => elem === checkItem); + if (i < 0) { + i = menuItems.length; + } + _volumeMenu.addMenuItem(menuItem, ++i); + this._integrateMenu(_volumeMenu, checkItem, menuItem, this._canIntegrateMenuItem(menuItem)); + } + + _canIntegrateMenuItem(menuItem) { + return menuItem.visible && this._settings.get_boolean(Prefs.INTEGRATE_WITH_SLIDER); + } + + _expandVolMenu() { + if (this._settings.get_boolean(Prefs.EXPAND_VOL_MENU)) { + this._aggregateLayout.addSizeChild(this._volumeMenu.actor); + } else { + this._revertVolMenuChanges(); + } + } + + _revertVolMenuChanges() { + this._aggregateLayout._sizeChildren = this._aggregateLayout._sizeChildren.filter(item => item !== this._volumeMenu.actor); + this._aggregateLayout.layout_changed(); + } + + _updateMenuVisibility(menuInstance) { + let canIntegrate = this._canIntegrateMenuItem(menuInstance.menuItem); + if(menuInstance instanceof SoundOutputDeviceChooser) { + this._integrateMenu(this._volumeMenu, this._volumeMenu._output.item, menuInstance.menuItem, canIntegrate); + } else { + this._integrateMenu(this._volumeMenu, this._volumeMenu._input.item, menuInstance.menuItem, canIntegrate); + } + } + + _switchSubmenuMenu() { + this._updateMenuVisibility(this._outputInstance, this._outputInstance.menuItem.visibile); + this._updateMenuVisibility(this._inputInstance, this._inputInstance.menuItem.visibile); + + } + + _integrateMenu(_volumeMenu, _sliderItem, selectorItem, canIntegrate) { + if(canIntegrate == true) { + _d("Integrating with Volume menu"); + if(_volumeMenu.box.contains(_sliderItem) == true){ + _volumeMenu.box.remove_child(_sliderItem); + } + _sliderItem.set_x_expand(true); + selectorItem.insert_child_above(_sliderItem, selectorItem.label); + selectorItem.label.hide(); + _sliderItem.get_next_sibling().hide(); //expander + selectorItem.icon.hide(); + } else { + _d("Not integrating with Volume menu") + if(selectorItem.contains(_sliderItem) == true) { + selectorItem.remove_child(_sliderItem); + } + _sliderItem.set_x_expand(false); + selectorItem.label.show(); + selectorItem.label.get_next_sibling().show(); //expander + selectorItem.icon.show(); + if(_volumeMenu.box.contains(_sliderItem) == false){ + _volumeMenu.box.insert_child_below(_sliderItem, selectorItem); + } + } + } + + disable() { + this._settings = null; + this._signalManager.disconnectAll(); + this._signalManager = null; + this._revertVolMenuChanges(); + if (this._outputInstance) { + this._outputInstance.destroy(); + this._outputInstance = null; + } + if (this._inputInstance) { + this._inputInstance.destroy(); + this._inputInstance = null; + } + if (this._volumeMenuInstance) { + this._volumeMenuInstance.destroy(); + this._volumeMenuInstance = null; + } + } +}; + +function init() { + ExtensionUtils.initTranslations(Me.metadata["gettext-domain"]); + return new SDCInstance(); +} diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/icons/blank.png b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/icons/blank.png new file mode 100644 index 0000000000000000000000000000000000000000..23568c66ae427c6f153f5492dea442fcc385c1b5 GIT binary patch literal 133 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmPNCzA+|#{XsCn}9+>o-U3d7N?UFBv=<Gi1aXVFfgzP WFfg{-uPOq{GI+ZBxvX<aXaWF#%O1f1 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/license b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/license new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/license @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/de_DE/LC_MESSAGES/sound-output-device-chooser.mo b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/de_DE/LC_MESSAGES/sound-output-device-chooser.mo new file mode 100644 index 0000000000000000000000000000000000000000..4929de7ee23d928c4c0affd278b264c507717314 GIT binary patch literal 1582 zcmY+D&u<(x6vy4t@?-g3C~yGeN>ynDv+QmgQ8omTHa}Q3X;w`@h)d+1c{^+3wO6($ zArUwJ0uEe2LLgO0aN)!aPPrgai5nLr)Eh_WkprUNH#6xb*3Nw9+3)#%{rvXNGp9Zm z7|-E(5zlQrFX8#*1YQ{bfDeIpYyJzKhkfE?#S7pwu&;wpfm`4yZ~|Tg3$Oux1)c`K z10M%}sQGif{wv6Fzt{W&d;<0zkp2Iz=l=uGz&`mvmG>-o0rm{ogSI~SAnY%&*~8#B z;5qPn@H}`MWW9I67r@gH#rj?bXTcfpF>njya~qI#M38lS3O)jU24V~GC5WH+8ZY+y z7G!-tf~?~g@OkjJnty>!*#Co&E1t!}`)~~B<-z;&;CSBiQ8q9*|I_7Hm6JdD-kz-I zxc(?lc|O*TQj}*q3vw;Wam^X$DHDq{RE3FRQHP09dr}pdc5>p~P#X$z=)4SMD2>|L z`%XG_Hb`g9p44`_E{zURELk-$Bv)N7#}tCfDTo!a<du;d6r;9z5bx@Y%2yqYG<qoG zh&;Vg@9ZqejCOQNqKABPb426nGl-s@6j3ggt3j;kkdiUVk}F_2>(Y^TaK54Vg^k2| z5syanosUy78zZ+X_=VH3SCutXQ?80UHn-zUF(wIB(K}NXDXZ#usJQRlM<`m-%aZrW z2NbU>8#if!yvbU~b(UN$@{nvgO!wE5n>YzOC5tLjS9V(MH<NZNX}=*mm%5i{Uu|_- zt;VVfG1>IW2BV_$UAb<plPj9SSXnFIvPIk{f2rM?eJ39)ZJMbYzl|uH7r2CElhhd5 z4(%?aZY?e~)_QA8Q;qFrt8op-B8yae6KIL_aeN)Eq3xVZN6H6^^Y3jgCs(F^ENMtS zS+c3iaJ^l5WuRl@gQU9lQfuE)rlagMJI%|DzA4ZR$+Gj~(3N&lnqj^(C#%o-i&mbS zm+hIkhFDg9pmVVimCcmT!~!>H-Jf#5@N%IDyJ}>lviqcSvSMRzJaFhhi$%0&z0SQ! zQ>Uxsbvoji?WlJ&`p$|4V{-JEM&m7M3wL7*%RBNFMoPEY=P+9l(B(EG`<mP(l%KO9 zRIZRexE1A&^CKuGEBQZBNy;eb{JuVc9Jv}$UItK^Bpqo&4E`}#99AbUoA^4mCQg1d vg1b0@;}NAFRwX#T_4L%oH<-?^sv?GXaLaF_5V;{QhX+4-d06yH;?(^f7uuyT literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/it_IT/LC_MESSAGES/sound-output-device-chooser.mo b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/it_IT/LC_MESSAGES/sound-output-device-chooser.mo new file mode 100644 index 0000000000000000000000000000000000000000..63937ad8598bd502e98eabb666a4af865db2fc99 GIT binary patch literal 1661 zcmZXT&u<(x6vy3Cer@?#C>#Lk(L$RD&9b`+qTMJ`Nt42gEM2ABh+E~%yqz`j*dyB$ z8pMqge*jmG6-Y=uazIE(T#(?*1%Wtl2Tq6sh_7cR*@TYP^O<Kq`~BFje>rpN6M^R$ zjOQ_az<2@U!xMPn`2&0q{IliX;7hPioNRajd<yp4;1l32@Dx~rOJD_dz)!*Z!Oy`* z!MiQLZr8sD<GvqT{scY-`&Tgf|K8631D=6>^1deSS?~hvW$-3)n}83&zJtSJ-gm*5 z!7srC{26=}JdI?|g6F~Le;IrYTnA&mw?X{GM|j1)eB9c1z?jz;AdV7WficcE;B(-2 zV9f6~@B#2I@I3f0cn&;=$=AV65b27iG2%IK&m$P|dALEnKgK%U+OZE$w%<6ee&YZ3 zc>5jukM`96N6ZPWsB4LJ#v1GAs@2wUCN_8`D;>m!@}-vhB&$qWDxIAv&7Nk~k!Q_m zP}Uqesni-z*64jwX0c9M`5-op+|^85Hm8EUmpOa!CL4BA(+v(mncR!FRL1qI-HkA6 zMq$p5FSaLJ!<6x!N?D8$kEU}jnoloArmO;O)T>^Ml}}k~Y1me<tg-3bSvbEV;|m9g z>s2_qV`5!cBy(eK-xu+PYNA&Y)i+bN*?DYk&+4jRLRP_9T{kIf`goA>#M$@Iv}BYe zJM27~H>A!smx!B;eXg=(y~=$uwV0k<PqtA>bV@d4;2{lq{mV(emkj#U>kU^{Ug-^b zz0Q{OA(=X9yq3W_?A($juh8IR%CZ>JR+UOxmnKt2(bAP2TkOJdnG(`i>Fzc5mAsHj zcT-y&?jMp0w?@;>cr@Nz<lFD|I#*F58-(32kyqf`;Z?{)nN>>X(s>SRH>cN<OAEi4 z-i%$cX;O<k@*!Q?RiX24(nz4rdty0Lfd<_{ccnAY6_k-&v##(%GG%T0wZSSipKD8o zUR<O8@@hxaqD6-eRXesmIN8co9f+45X;};F1N4-kh}sLHjKXE|&_0={m|C%tGIhlJ zja{Wxkga%g6<4**#i%H8j*LetY6YzzF^@B|#YcToK%I!g^zCOzjy+n)RN<<`$QiDh z^(v@6E2dmXit=uhjmi!e>%iNb<4(CcDvKTIR4fMqhrVS{={)K?iX1nsGWMSB3O6~c znl>COAl?AkcIKc#i^S^K?pW-$iU0RG8lZJi2Ny#tYHv!3zeMSymB+5OcSigNL<qf% literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/ko/LC_MESSAGES/sound-output-device-chooser.mo b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/ko/LC_MESSAGES/sound-output-device-chooser.mo new file mode 100644 index 0000000000000000000000000000000000000000..570937de51aa047adba5dc9a80bac09aebcf1a4b GIT binary patch literal 1594 zcmY+BPiz}S6vhWAp*4R~pr|T>r>X)3+G3|t368>nCZ&-QYNZBo-FnCN!g|NrSvTZ{ zh>ILz36Nsdt?V?B5?V?QfmS6IRN}^s6Bi`JiBop=z>Oo{tjDoC+Bd(QH}Adq=FQ)S z540G@3$Pbqf52XbH6Ox+aqwZr9s>^<j)Et!K5F<5_$<~1@Hn^tJ_B9@4}jl;uYo&Y z3j7m%6#NH#65KQV&+Lyp(#sn)JOVz2xMQG>f8F@U!NXWjg8KX%_zLKNN1=TM)H=2x zVg$Sko&@iMCqM=2{ZWX~cRvp5`dRQ0cn&-b&VxE`+4!%4I&Krxb$>LxXZWk(Z=jC* z-S98t-vgh6|6juq6nP2jW1w!i&e7-VULAzt8p+UafEFb~_XF~i`)low!%&Vr57T25 z_5|!{m_ADneTU(pd#1+`n7#))BRmmshfVRKUGqXV<;tpOFOpqzTtU@96kU%?DvE$4 z_jn<6g})!<7KATJ_}(J9{$QUxSB7lbw=a5}W<`lATuQsdW#8xXg??#r9qxy2(Jk1z zG_m)%&jZ_|3p@;6za-f?L?n|r07~2<g=HS_SIyzV=j8B9Zh^BINTPh1S9-sa&G^+? zNK?tKWOJ@8aL==SE^1f_UlhuLK=Avvo=}LLuZ6=IIT3^dF&D~Wf$0es)=|AvWzQvg znGg6b39nY+1XW=myrhp#uho6Uxj=l3zFISm^&t-=nvkW`Jg?$hYfhG2$9lI`l2%^8 zojY&M<0d-DnzBQlrLpu_#>%9vvD1`!E1N#`M*8h^IyGzir5ZYJ<+)u!-rUUG^q_j? zWIA;Qg3x#?zgR_;kbfG!iFbGX2`ZHBK=N?%gZx=*d=MAdzAW;<n)VCA!NX)}{GuDC zKC*fb7K$KCIl&z_q*JIrYfBW@NuHhTrE*>k?`oYDK}BZCuO>@5nVF#8?__#nUw4*1 z5h<pA-j1$slWNsf_l~;XW_!Ck(d}KP>g(}RH*R%Et!=4$ZPjg)YHvoXtC%TXtw$T3 zxK*e4n~qxPm~e`2eHU$X)s?RY1*03yeh^aje%ht@YDd+7h_9|PwbU3cHSl+TiFR;a zeN(Nsm}=aFD10n#wWH5_QW8<RwpwebZk?i*{?$s22pXjAUykC-&FDsh?K_xSyBw`= znM{56c<D11t=v>g%M1;PzO6HL<qOqqGIh5z6d8ZD9e=soSJ-dDa50K*-_e>M$m=_? WfhL<{lh$^d(e-6g8#`*f!Ttx4mIj>w literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/nl/LC_MESSAGES/sound-output-device-chooser.mo b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/nl/LC_MESSAGES/sound-output-device-chooser.mo new file mode 100644 index 0000000000000000000000000000000000000000..bd5188e72f8150a0a71570eea4b28e9943ea145a GIT binary patch literal 2891 zcmZ{l&5smC7{&`kh4lkP#cwEt$P(#ccOkmYvW8uDfekybVP^?9t(ktOr*?a)hOX*a z;b8P)jM0mR7;h#T6Fhh`QDgMLKj2qP^k58$i7_T79z6TJ)jhKV#L9I4x~r<+dh4mD zYJT0Z<%&Sri|YwoM>Yv@2wdEZ8_L$(gg6dv19yNm@F4gScqjNa_%wJ0d=UJqtp5(Y z3-3RJ&x5~%`@r2>3eJI5yle0Q@FQ>!_&Lb&ekr->b|D_c`zUxHI18dlEQ6!q1#kp> z4a85ph1)&gJ0(8=x&2d+zx$%(*WkT)zXo#u@4;>0Z)N+RAkX)2kn`JoN0HY~a1k0! zgQrpdCPcU&{0Zdw`~hwS_u@7Q9t0l&3FP>%fIRLS;8E~Bkmvm)xF7r%JO=JUXO8<K z$n_UN*71FC3j6_Ny|-g>oZmhWRbmo60?vXw&N_HE_&T^9d>2IO;zJNW@d<9M%V#CO z1X<^AKpy{FkmFtlA%*x0d<gsp+zD>QB0mG}1|dr*Y@Nr-=O;ndSK@jK7d{h@<KmcH zI42%|H@~28Y#yIAVcj4{@FkDUh3CTKa$#Te3$GOyq%kPF@boY))`7Kx1w+9S1r1m? zp5tDw9bKlBnqD4~F}dQ=w@GGnG1nQzsO{+{No^!opER~Za@po4mdoV5Hi<hRW}062 zlt=Hm(7P&!s`>$(Z0k5C!=aXJ<_F(b#BrNij0NUtERFRtwJHt+b~9>GMy5$lt}Muw zPD#v>x^=)gYN<T+Xbkz*FLlvTJJm=-^7OJN<Fqw$IHI>w^{nQhw37{LtDd%bR^czZ zsk$Up9&3wFED!8t%Vx3=36Tb!bkiCqtw}HO+y?D3)eiYsv)@aVrgpur<^Sj)kxhf- zU~XdGgzOh>B0J=qO33x=46XOGk{Y6V-O^3vIjeb|X)Mp%G{;ia$mATjrG0;RqY$m= z76fUwl}WI6VPKLjEN-!Cm7#9hXs;FKgf*{JD&iy=%2Ya7MsW&>gj*>Aw9+kJ#EkZr z@>oM<7GeRS$$FbQ6ds*eFx}jj26CdNU6ayO8L~Ovtg+2@2JeY8$X=X5AaOSL!(ZIs zDX~;68JxJ>w(H@B#o_+N%v@1rix@Z5^$O*XpU-R>);uoE)z2NlSFvZJg*ZA-8H-qz zBj>0KS4TCM=s23q6BpGjs+Z12=U_?h6wP6eROQ6j#CSA57L8BH@n@@J&qZU0$Hqn$ zmGe<OQ^uvr+pH>2s0OSzQ-LJ1mS4tm-8OWyEswUirQ?q!9i`Js({^6K*Cxqfkf=_o zgHikMRcX?Z+Cpu9Bhm57*vN6r4W@~BmsF+4<~RhqYcnNbhRl(lUa6mmCO7(VUf74x zylL7P2B^x(hV~;DqQV6s;U!BkTy_}om(m>8h)&q7<B&o(yt?U$DOo&Dj~cmWT8<x> z8WE$TF&!v24N)fB<gp7pwtWjW#&+m-Ri>2Zt;Mft0?WWg38lG?VMgcS6Qo3@IEbh< z_y((g3gOt;)Ef@vWRJ3j&G1nfw`;L@!@=0#YboVf9Ad02L=AFYVQ2MJ$fSoa*3ga+ zYp5|ABdyzvPdMbJK(V4d_ADe_Bb;mfnXo50kWz@f%;OH4#B$MDvXD8hV$wrH*-mJ~ ze$7;Zm6M?j8^ct@3?EkEtim~t^BS8ZOb=OJX3=snlct2au5NmpWU7P8vYYM7&@I)g zIJD05+&HL=p?p8_A<=lo<z$iKE#e8BSq)2TvY}VoHfJr)Xv){AIJ77oKC}3IE=5AW zlf}Anx?w#0FCYy}8{0C4!IF)2<bw?PQ!xdc{47}`Bh3zlk*MW^o%-uvT4pevgx2-y zWu3$oF-ss$Sue518TQ8YS3ibYrPns}7ufF&V&90<eN{u6p$_hJ5*~K=M{Rp2uU)-% z)ojS8omlECk7Kc;HeN9i9DqHG6E*l98Hls+s7?lFpY7o6QymC|HaP3X&PzA@w>SYQ lh1=?Egqy>cqTL**tS<gN8qk{zi^-l3?q|~u1Pl%n{{fntD!2du literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/pt_BR/LC_MESSAGES/sound-output-device-chooser.mo b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/pt_BR/LC_MESSAGES/sound-output-device-chooser.mo new file mode 100644 index 0000000000000000000000000000000000000000..2a27c47fead646b750d9fecb5def1c2eb50f0126 GIT binary patch literal 3129 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/pt_PT/LC_MESSAGES/sound-output-device-chooser.mo b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/pt_PT/LC_MESSAGES/sound-output-device-chooser.mo new file mode 100644 index 0000000000000000000000000000000000000000..4b47f43956ae5f252972209cf9816ce3bacdde0b GIT binary patch literal 1642 zcmZ{j%Wot#5Qn`>cn$A`00$n0gqB^Q&dyAT6io;Mk}O1Q$Y?VlZY!tT(_`pywQP6T zAWqyla^Zkj?E&p!g+M}_;4(+Jf{>69e*upJ2M$R5?Dmi>2utp-%T=x_m&-q$TKGU< zJc#)S=C_!SV!nF<AB^9?TfyHG{{)|eeBxxw)8PG(Ujgq0FM$hS4W0oTumgSs-V9y? z?*czf{3_|c0mHuU6MqEnhWs-a?0-$`e}ShUpS-D!y9l0!d=}hAZbR@k$RER5$om=? z^q+xGgFk=|feT0`o*4{w%iuY14GeMLO6%`{!Tvok`28^HuYzzTJ_m0BzXXH7ufd1G z@6!4&;Ny`003QPvVY~u910r4V0A}zL&U7bc7`L~GenT#IBstXKzVr`wafF!nr2kNV zlqcRtI1@?{YrhDF+~e5bimfWdrW(tp@?ulFx{`+^n?f_ymdCnMj>c@ssY>Nuv-!tJ z=WHCYsSZh-BRy5xd9h{Wex>LFmo!z*$x=D-k}}H5iuRQE+LTVbtP2%CX*a^?G5LwI z>Z#<JjiN#w=v;|y#G}zfP1|25woTo5+KgH!cC^b?RY{|`fy9RMiDekSD#Hhk#BSq{ z?-;W6N6G9>_-zqBI8Cr>qq=s9+nqP89dOl56(K8cxr#+9+B(iN9$J1AMa#B}>~&=w ziZ`InMO7niw&O}&WG^<Q%SMdl;cj*jCkaN`ru1q+{cgXTbyu?9D)oAU{z}$e?shvD zr1ROxO5-Z&*$!x<YHw>#ntX!GdNw2k)$@6)aP9h1&eP|x+msD1BO56>Mbx=GpxR#= zjygNrJ6lJ&_LjPx4R})qArEWh<ke+=4lSb18s!sdo$~9ijm~GQM|L5#v9j5g$+^Ha z4`_8?`_3C#d;KxzA*(`r>Mt#KhE;=}$j-B!x&fIwif+BXM(ywV8ADI3Q}66rN5pp3 z1&g69?CdIwt;>2}TdI{EYlo`S>`L3Y(#qIVvrmqh&90*~sF19ck&cXvtt!L5(A}-= z3tk^ci>5W9Y0==&R$L;s8S4_c!t4t;DbdQ>iQOFUM%p=}BODQ=2{#AI&Z9L`D)744 zX#`*~Qd0zGYI72^_hV}DZ6%W!8I`xPkXVN+$EmN;*0U=f?+<MqdRv)EGyTtACui5O w_1}r(JfQ()Djr^nLkxR7yBk9PpFX*ttC4?r2H~|*dKq=N1wQVW>U42`17a4yM*si- literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/sk/LC_MESSAGES/sound-output-device-chooser.mo b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/sk/LC_MESSAGES/sound-output-device-chooser.mo new file mode 100644 index 0000000000000000000000000000000000000000..9c773a6667576acc15d73bce601091928726cc7d GIT binary patch literal 1632 zcmaKrO>A355XZNr&|<y|g$t;2s6r}u!FEbNoVp;I1WcRMN=$?hQnmIQ+Z#Xcn%#Y- zv3lYRN4SBMLk~eh2niu^$OShikhpO{LKR0Yz>xzI|9yUqQ>kL*x4)g;*`5E)u75jz z<Wqt6EWYRQ{e<rYd^ZoFL;DLn3jUq*AMh0HLx(4v0iT9_1$+{`4jusq;4GMe1@H^- zL2w6r4E#FhZod8_820^=^H=b3*muC-|7Sk`FL)gG;RhyhPk=M9PlNA6TOB+G`zsvw z2>2a%68r%?1^x~`2Ofo(kn>6KA@D3X4_3jT_X7|=aT8t0?;|kyeFi=XexBQ3f}uX& zfG5E3!BB^v!H2;+V5sLGV9;?4hnxdvLC6));De+rr*Lj)VP9w=UZ_FHBeYP5r?SyR zTNs8Mp2+7yT$Cr%8#JL5SuR0Ws7cl?o5Wa-#Ui(5ns~9O+#r!#B-2<K8d%d-2|H>V zOO6w6d1dszlWH05$mnE?l%B3rqMR2?S~e4=tENMJc20KKi8ooZl?ko0_eyu1cuU2c zb@^_DQEl>Fw)|p#ve8U24^@jr4e_Ya<^E*oL`@G;Pm9^A6D!KKI7y^tlfp9Ew7S;7 zc~yoE2Z?K`Ke(fAte=Wm@0t%q=#WkDnnZPzDKpu5&)m=?X`czIyfsNyq<B)tyNc`9 zY@%pUEsi$WIux%$g*6@^ZnWY$DvsVrJ1%M%OxLePYse%xMT^pNh04X!g{XKTDxab9 zeC5@%FBQwhV&STEK5AI0T_U})6?)q^rZv@-ba4~I(z#OU^-f=^WUgiUm*B5EDas!; zSoRU;u3aJ5yS{#<uu@xDnyM?!6$_V<5Nib48bF2T8~znM3Z)mQ)s@z<ufEe*j?Pd0 zf|xejXi2wBjEk+%`KI!PccaOLXDaK4W96wlSDq^s>Pd=s5G@<qcNNlu%yiZH1)2=2 zv$|R>(Q8yBEMKaYX=aA>#p)SMl`vI4y-*O#(sEOew?!Q<7<a*Xyd&N-O)E!g?-mWm zyG^$F8#cr2Ny`K4s%P{T4QXUX8V`hAzQgoI-Qc)z2L1rv^za`UO08II>}|7Yatnb- z;~M0UwUT1JXSeol<(Mw;%OrcmH}G#rH<?sZ(k92+&`4S(V{B#781M8Y1u3Ey^oR{O z55oETPExFmza8Q6-u_sz)uyR9kz#ecJu*YFw~Lds6dU8+`)pj@*GS>XrPJ)EBO2ak i<2_aU-*q!g#@n&H{Rwpw>6AClE0k^jLb?aimEu2@aKR%0 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/sv/LC_MESSAGES/sound-output-device-chooser.mo b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/sv/LC_MESSAGES/sound-output-device-chooser.mo new file mode 100644 index 0000000000000000000000000000000000000000..2bb16f258b6e03b48c210521a71a0661aa35f642 GIT binary patch literal 1883 zcmZ9L&u<$=6vqc>fSTW>{1g&{C;<sJI3X34OA(<-sinA08YcpAKvVDYdXm{0Yi8Ds zIUp`bNL=C2D@w!#aX_L7sW*!gf-6EoIdJ9x{R{ZM-8E@5^6tm;_RX93^Wem>uLRb! zc%H-aH=dX9+`J1fthKv^xDQ+hXTcBZ{iomq*nbY50>7;J6Zkmx2jC0fEs)Qhyhn&f z!L#5oa20$7Tn8KA$KVY38Tb(RMa}E=_isQx_kGPD!H2QG39|k#wfqn81oj6Y`|&Tx z_s`()Da7;|cpUq6Ty_rp5WF9}4zm1fkp2F)=5OHB*#85*2p+#zh<PvpS^g1-PhuCG z1wR4V-ad$*_ztf}z#l-)<If=9^DD^l{0{Q@KS9>}3uOOq)!%1u=p^=yn$Lr?*e`-y zqQ~$&h=&*Ff$PZk<9KNwDDQg)<a)7fUTl-^<rt<5sS;04Hz51Ni}TFusS0IYaecYC zrd&UiSv<j#^UgR=DOi|PS{J2pDwT0Sa_Ff~o@|eT+-%FfHYAp*uL={vk?xV~j)#!x zFf?i`Rgr3kQ;s!*>^m<58R|vt?Egx-=WLM9nz7XO@VhiRM6qJkRYP*sWiqD_R7PPc zvvh50TI!T+)P3DkrRkO3NH&#Q&J;Px4%tFR2X~`PD=)2rHSr!<^2*2!ic#Avh>NgO zzG^q%ukLf?o?fn9be3u(q7BDocR)Fne}ib-VG(7Oh#)#T^vD=x$rac+>v{w4pnM6J zi%T#hE*J6k6Kl@LLuWPy?wa5i(!yGm>!A|4>f{}=Q57z{ir$$r+i6vtsmE*HZ6o_h zJ59F82UMXY8&_zEdy`Jcbeg<dWFhG~h}SMBSCA}LN|sfmmRx8q%qPv$N%KuPzt}qc z#;eVRX0x%XLQJ||*<e(3z9l=(XUfVY8&TJQ6up&~k~uY%yp!cho4KCL-^Ov96)0=c zC6(ja>AoeysL^S6Rt`O#pKCVGBRG^g;Wlf@NW1ZMbegtjWpAK-pm=Vxdm%Y<sK<_> zXOk7%b1914l4q{!*myswieGxX<|svPEX*y=Ei}Z1$)3-Q%DAPY%ye%D1&Ab7#3ZOR zRWkFFJ%na;jDb7b%KqdATE&Jq*)zuC8s)`CR5n#UMZXRxhBzLIEgh6J+eKQBgs%>c z6b6l0tJ2UrXbulairh*q3lELJ^PsF)+KGCEw#8ItSx#w6Xfd?^;fLukkcT5ujiepd zgCaKL<i?JY=#+7ulO1E;Ci`V%<@Et`Vass#y+mGP5KG47h}Np3;Vn;Ym~BzMCESxP zN0-H^(|v6*)G$?r)kZ{-awYf=c}z++*~i3{U$I~2kOGxcvV$RsJS3LYkdw`Lr!tSp hkz8|P>)ai&T&BD@Vxbh4NnpP)ESMIqAir)a#(&>y{=xtN literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/ta/LC_MESSAGES/sound-output-device-chooser.mo b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/locale/ta/LC_MESSAGES/sound-output-device-chooser.mo new file mode 100644 index 0000000000000000000000000000000000000000..d3809ee2a44472ab13d20c6888330439d78abdda GIT binary patch literal 4780 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/metadata.json b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/metadata.json new file mode 100644 index 0000000..6172206 --- /dev/null +++ b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/metadata.json @@ -0,0 +1,19 @@ +{ + "_generated": "Generated by SweetTooth, do not edit", + "description": "Shows a list of sound output and input devices (similar to gnome sound settings) in the status menu below the volume slider. Various active ports like HDMI , Speakers etc. of the same device are also displayed for selection. V20+ needs python as dependency. If you want to continue with the old method without Python, use options to switch off New Port identification. But it works with only English", + "gettext-domain": "sound-output-device-chooser", + "name": "Sound Input & Output Device Chooser", + "original-author": "GopI", + "settings-schema": "org.gnome.shell.extensions.sound-output-device-chooser", + "shell-version": [ + "3.34", + "3.32", + "3.36", + "3.38", + "40", + "41" + ], + "url": "https://github.com/kgshank/gse-sound-output-device-chooser", + "uuid": "sound-output-device-chooser@kgshank.net", + "version": 40 +} \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/prefs.js b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/prefs.js new file mode 100644 index 0000000..90ad206 --- /dev/null +++ b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/prefs.js @@ -0,0 +1,328 @@ +/******************************************************************************* + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see <http://www.gnu.org/licenses/>. + * + * Original Author: Gopi Sankar Karmegam + ******************************************************************************/ +/* jshint moz:true */ + +const { Gio, GObject, Gtk } = imports.gi; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Lib = Me.imports.convenience; +const _d = Lib._log; +const SignalManager = Lib.SignalManager; + +const Gettext = imports.gettext; +const _ = Gettext.gettext; + +var HIDE_ON_SINGLE_DEVICE = "hide-on-single-device"; +var HIDE_MENU_ICONS = "hide-menu-icons"; +var SHOW_PROFILES = "show-profiles"; +var PORT_SETTINGS = "ports-settings"; +var SHOW_INPUT_SLIDER = "show-input-slider"; +var SHOW_INPUT_DEVICES = "show-input-devices"; +var SHOW_OUTPUT_DEVICES = "show-output-devices"; +var ENABLE_LOG = "enable-log"; +var NEW_PROFILE_ID_DEPRECATED = "new-profile-indentification"; +var NEW_PROFILE_ID = "new-profile-identification"; +var EXPAND_VOL_MENU = "expand-volume-menu"; +var CANNOT_ACTIVATE_HIDDEN_DEVICE = "cannot-activate-hidden-device"; +var OMIT_DEVICE_ORIGIN = "omit-device-origins"; +var INTEGRATE_WITH_SLIDER = "integrate-with-slider"; + +var ICON_THEME = "icon-theme"; +var ICON_THEME_COLORED = "colored"; +var ICON_THEME_MONOCHROME = "monochrome"; +var ICON_THEME_NONE = "none"; + +var DISPLAY_OPTIONS = { SHOW_ALWAYS: 1, HIDE_ALWAYS: 2, DEFAULT: 3, INITIAL: -1 }; + +const PORT_SETTINGS_VERSION = 3; + +function init() { + ExtensionUtils.initTranslations(); +} + +function getPortsFromSettings(_settings) { + //_d(_settings.get_string(PORT_SETTINGS)); + let obj = JSON.parse(_settings.get_string(PORT_SETTINGS)); + let currentSettingsVersion = PORT_SETTINGS_VERSION; + if (Array.isArray(obj)) { + currentSettingsVersion = 1; + } + else { + currentSettingsVersion = obj.version; + } + + if (currentSettingsVersion < PORT_SETTINGS_VERSION) { + obj = migratePortSettings(currentSettingsVersion, obj, _settings); + } + return obj.ports; +} + +function setPortsSettings(ports, _settings) { + let settingsObj = { "version": PORT_SETTINGS_VERSION }; + settingsObj.ports = ports; + //_d(JSON.stringify(settingsObj)); + _settings.set_string(PORT_SETTINGS, JSON.stringify(settingsObj)); + return settingsObj; +} + +function getPortDisplayName(port) { + return `${port.human_name} - ${port.card_description}`; +} + +function migratePortSettings(currVersion, currSettings, _settings) { + let ports = []; + let _lPorts = Lib.getPorts(true).slice(); + switch (currVersion) { + case 1: + for (let port of currSettings) { + for (var i = 0; i < _lPorts.length; i++) { + let _lPort = _lPorts[i]; + if (port.human_name == _lPort.human_name && port.name == _lPort.name) { + port.card_name = _lPort.card_name; + port.card_description = _lPort.card_description; + port.display_name = getPortDisplayName(_lPort); + _lPorts.splice(i, 1); + ports.push(port); + break; + } + } + } + break; + + case 2: + for (let port of currSettings.ports) { + for (var i = 0; i < _lPorts.length; i++) { + let _lPort = _lPorts[i]; + if (port.human_name == _lPort.human_name && port.name == _lPort.name && port.card_name == _lPort.card_name) { + port.card_description = _lPort.card_description; + _lPorts.splice(i, 1); + ports.push(port); + break; + } + } + } + break; + } + return setPortsSettings(ports, _settings); +} + +const SDCSettingsWidget = new GObject.Class({ + Name: "SDC.Prefs.Widget", + GTypeName: "SDCSettingsWidget", + Extends: Gtk.Box, + + _init: function(params) { + this.parent(params); + this.orientation = Gtk.Orientation.VERTICAL; + this.spacing = 0; + let uiFileSuffix = ""; + + if (Gtk.get_major_version() >= "4") { + uiFileSuffix = "40"; + this.__addFn = this.append; + this.__showFn = this.show; + } + else { + this.__addFn = x => this.pack_start(x, true, true, 0); + this.__showFn = this.show_all; + } + // creates the settings + this._settings = ExtensionUtils.getSettings(); + + Lib.setLog(this._settings.get_boolean(ENABLE_LOG)); + + // creates the ui builder and add the main resource file + let uiFilePath = Me.path + "/ui/prefs-dialog" + uiFileSuffix + ".glade"; + let builder = new Gtk.Builder(); + builder.set_translation_domain("sound-output-device-chooser"); + + if (builder.add_from_file(uiFilePath) == 0) { + _d("JS LOG: could not load the ui file: %s".format(uiFilePath)); + let label = new Gtk.Label({ + label: _("Could not load the preferences UI file"), + vexpand: true + }); + this.__addFn(label); + } else { + _d("JS LOG:_UI file receive and load: " + uiFilePath); + + let mainContainer = builder.get_object("main-container"); + + this.__addFn(mainContainer); + + this._signalManager = new SignalManager(); + + let showProfileSwitch = builder.get_object(SHOW_PROFILES); + let volMenuSwitch = builder.get_object(EXPAND_VOL_MENU); + let singleDeviceSwitch = builder.get_object(HIDE_ON_SINGLE_DEVICE); + let showInputSliderSwitch = builder.get_object(SHOW_INPUT_SLIDER); + let showInputDevicesSwitch = builder.get_object(SHOW_INPUT_DEVICES); + let showOutputDevicesSwitch = builder.get_object(SHOW_OUTPUT_DEVICES); + let hideMenuIconsSwitch = builder.get_object(HIDE_MENU_ICONS); + let iconThemeCombo = builder.get_object(ICON_THEME); + let logSwitch = builder.get_object(ENABLE_LOG); + let newProfileIdSwitch = builder.get_object(NEW_PROFILE_ID); + let cantActHiddSwitch = builder.get_object(CANNOT_ACTIVATE_HIDDEN_DEVICE); + let omitDeviceOrigin = builder.get_object(OMIT_DEVICE_ORIGIN); + let integrateWithSlider = builder.get_object(INTEGRATE_WITH_SLIDER); + + this._settings.bind(HIDE_ON_SINGLE_DEVICE, singleDeviceSwitch, "active", Gio.SettingsBindFlags.DEFAULT); + this._settings.bind(SHOW_PROFILES, showProfileSwitch, "active", Gio.SettingsBindFlags.DEFAULT); + this._settings.bind(EXPAND_VOL_MENU, volMenuSwitch, "active", Gio.SettingsBindFlags.DEFAULT); + this._settings.bind(SHOW_INPUT_SLIDER, showInputSliderSwitch, "active", Gio.SettingsBindFlags.DEFAULT); + this._settings.bind(SHOW_INPUT_DEVICES, showInputDevicesSwitch, "active", Gio.SettingsBindFlags.DEFAULT); + this._settings.bind(SHOW_OUTPUT_DEVICES, showOutputDevicesSwitch, "active", Gio.SettingsBindFlags.DEFAULT); + this._settings.bind(HIDE_MENU_ICONS, hideMenuIconsSwitch, "active", Gio.SettingsBindFlags.DEFAULT); + this._settings.bind(ENABLE_LOG, logSwitch, "active", Gio.SettingsBindFlags.DEFAULT); + this._settings.bind(NEW_PROFILE_ID, newProfileIdSwitch, "active", Gio.SettingsBindFlags.DEFAULT); + this._settings.bind(CANNOT_ACTIVATE_HIDDEN_DEVICE, cantActHiddSwitch, "active", Gio.SettingsBindFlags.DEFAULT); + this._settings.bind(OMIT_DEVICE_ORIGIN, omitDeviceOrigin, "active", Gio.SettingsBindFlags.DEFAULT); + this._settings.bind(INTEGRATE_WITH_SLIDER, integrateWithSlider, "active", Gio.SettingsBindFlags.DEFAULT); + this._settings.bind(ICON_THEME, iconThemeCombo, "active-id", Gio.SettingsBindFlags.DEFAULT); + + //Show always is not working always, hidden in the UI directly + let showAlwaysToggleRender = builder.get_object("ShowAlwaysToggleRender"); + let hideAlwaysToggleRender = builder.get_object("HideAlwaysToggleRender"); + let showActiveToggleRender = builder.get_object("ShowActiveToggleRender"); + + this._signalManager.addSignal(showAlwaysToggleRender, "toggled", this._showAlwaysToggleRenderCallback.bind(this)); + this._signalManager.addSignal(hideAlwaysToggleRender, "toggled", this._hideAlwaysToggleRenderCallback.bind(this)); + this._signalManager.addSignal(showActiveToggleRender, "toggled", this._showActiveToggleRenderCallback.bind(this)); + + this._portsStore = builder.get_object("ports-store"); + + this._populatePorts(); + this._restorePortsFromSettings(); + } + }, + + _populatePorts: function() { + let ports = Lib.getPorts(true); + ports.sort((a, b) => (b.direction.localeCompare(a.direction)) || getPortDisplayName(a).localeCompare(getPortDisplayName(b))).forEach(port => { + this._portsStore.set(this._portsStore.append(), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [port.human_name, false, false, true, port.name, 3, port.card_name, port.card_description, getPortDisplayName(port), port.direction]); + }); + }, + + _showAlwaysToggleRenderCallback: function(widget, path) { + //this._toggleCallback(widget, path, 1, [2, 3]); + this._toggleCallback(widget, path, DISPLAY_OPTIONS.SHOW_ALWAYS, [2, 3]); + }, + + _hideAlwaysToggleRenderCallback: function(widget, path) { + //this._toggleCallback(widget, path, 2, [1, 3]); + this._toggleCallback(widget, path, DISPLAY_OPTIONS.HIDE_ALWAYS, [1, 3]); + }, + + _showActiveToggleRenderCallback: function(widget, path) { + //this._toggleCallback(widget, path, 3, [1, 2]); + this._toggleCallback(widget, path, DISPLAY_OPTIONS.DEFAULT, [1, 2]); + }, + + _toggleCallback: function(widget, path, activeCol, inactiveCols) { + let active = !widget.active; + if (!active) { + return; + } + let [success, iter] = this._portsStore.get_iter_from_string(path); + if (!success) { + return; + } + /*Dont support non-pci cards for show always*/ + let card_name = this._portsStore.get_value(iter, 6); + if (!/\.pci-/.exec(card_name) && activeCol == 1) { + //this._toggleCallback(widget, path, 3, [1, 2]); + this._toggleCallback(widget, path, DISPLAY_OPTIONS.DEFAULT, [1, 2]); + } + else { + this._portsStore.set_value(iter, activeCol, active); + this._portsStore.set_value(iter, 5, activeCol); + for (let col of inactiveCols) { + this._portsStore.set_value(iter, col, !active); + } + this._commitSettings(); + } + }, + + _commitSettings: function() { + let ports = []; + let [success, iter] = this._portsStore.get_iter_first(); + while (iter && success) { + if (!this._portsStore.get_value(iter, 3)) { + let display_option = this._portsStore.get_value(iter, 5); + //if (display_option != 3) {//Dont store default value + if (display_option != DISPLAY_OPTIONS.DEFAULT) {//Dont store default value + ports.push({ + human_name: this._portsStore.get_value(iter, 0), + name: this._portsStore.get_value(iter, 4), + display_option: display_option, + card_name: this._portsStore.get_value(iter, 6), + card_description: this._portsStore.get_value(iter, 7), + display_name: this._portsStore.get_value(iter, 8) + }); + } + } + success = this._portsStore.iter_next(iter); + } + setPortsSettings(ports, this._settings); + }, + + _restorePortsFromSettings: function() { + let ports = getPortsFromSettings(this._settings); + + let found; + for (let port of ports) { + found = false; + if (!port || !port.human_name || !port.name) { + continue; + } + + let [success, iter] = this._portsStore.get_iter_first(); + + while (iter && success) { + let human_name = this._portsStore.get_value(iter, 0); + let name = this._portsStore.get_value(iter, 4); + let card_name = this._portsStore.get_value(iter, 6); + + if (port.name == name && port.human_name == human_name && port.card_name == card_name) { + this._portsStore.set_value(iter, 3, false); + this._portsStore.set_value(iter, port.display_option, true); + this._portsStore.set_value(iter, 5, port.display_option); + found = true; + break; + } + success = this._portsStore.iter_next(iter); + } + + if (!found) { + iter = this._portsStore.append(); + this._portsStore.set(iter, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [port.human_name, false, false, false, port.name, port.display_option, port.card_name, port.card_description, port.display_name, ""]); + this._portsStore.set_value(iter, port.display_option, true); + } + } + } +}); + + +function buildPrefsWidget() { + let _settingsWidget = new SDCSettingsWidget(); + _settingsWidget.__showFn(); + + return _settingsWidget; +} diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/schemas/gschemas.compiled b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/schemas/gschemas.compiled new file mode 100644 index 0000000000000000000000000000000000000000..7c5c5c13e178dad8219965fa3e2e4ad9ad9a059e GIT binary patch literal 1248 zcmaJ>J!lj`7#)8uYGM)-2^>m54vPfbJ3&-TWh06Ne+olD18z2x+kxGgV|VT@pwWnk z_C^Ik2!iQ^Alg}IWsqoNx_}1MDi8uTcJaO0%b8fX@OW?E&dm4C?Dx&tAwN;RwxaRm z!NZyE+rD9fb3H<=<H^1ax-r+_?>he@<}m6L;+&WUPh;XXuu00rvNo|SrlE|YqMWc& zr^@KGn3}2{^GwxNs@5BmH5EuN2#rZ)f{8-(bvw$|VIDhymIii%TmL~D-uD6>4RnI{ zHE{U(#Pu#{=5;{S#2oky@bKm3xAdvI;Ln3U0Us9^KGUb}gue{_39R0I`i(wyEBrO^ zI`C_@{+d2@H~jn#A$oy&a%ho0bvyi{;N!siFCQM!r|yA&348_k9$i?YPrV<02#$eh zb)in5n(I%2?*XswTrAS3J_vsX`~rA4d8>{8iaG~>4!i>N++R7#@lM0%q2_?0-w&S9 zr|y8C2loOuJH9@qPu&auH24f)=LhoishQ_6cm!BlJGDfgn&%UOWBRi#x9L-J{VDJ> zV56p}g%}hw;AKF|x>rfexQe8w#-z4tTm`;G;jkY1M*Gvy)IFtHJ86Uws2S&n&|WNc z)l&hgjH{xQM*I2Z*-*w(#73JSOi+~gl5SoptHQ>lO>r8TG(NaEN?Tb@d@H@WvSBla zW;_$CW>uR+l@)VA9&)1MqwH%Ys>WHOS)45!)?0S$;uR~t4YSotXsVKHs*-D}(%+^k z{imr)LZt3=a1g1|Wt?obUzb-kMK+rb2BBQsz}MPX&kwAsakp3j$u(y(BR9_dk1frd zI$vsk6p<oR#z^eHViKErsk9a;WCwbqP?HJogu=kFBLz30FmQEb()}IGY6sZM?0K}8 cV4tIu-CY0@dw^X4d!1#(dxv*G4&Z(82U^-25C8xG literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/schemas/org.gnome.shell.extensions.sound-output-device-chooser.gschema.xml b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/schemas/org.gnome.shell.extensions.sound-output-device-chooser.gschema.xml new file mode 100644 index 0000000..d34d510 --- /dev/null +++ b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/schemas/org.gnome.shell.extensions.sound-output-device-chooser.gschema.xml @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE schemalist PUBLIC "gschema" "https://gitlab.gnome.org/GNOME/glib/raw/master/gio/gschema.dtd"> +<schemalist > + <schema id="org.gnome.shell.extensions.sound-output-device-chooser" path="/org/gnome/shell/extensions/sound-output-device-chooser/"> + <key name="hide-on-single-device" type="b"> + <default>false</default> + <summary>Preference to show the chooser when only one device is available</summary> + <description>Value set to false hides the device chooser when only one device is available</description> + </key> + + <key name="show-profiles" type="b"> + <default>true</default> + <summary>Preference to show the available profiles for all devices</summary> + <description>Value set to true displays the individual available profiles for each device</description> + </key> + + <key name="expand-volume-menu" type="b"> + <default>true</default> + <summary>Preference to expand volume menu to fit the name of the sound devices</summary> + <description>Value set to true expands the volume menu to fit the names of sound devices displayed in the selector</description> + </key> + + <key name="use-monochrome" type="b"> + <default>true</default> + <summary>Preference to use monochrome icons instead of default icons</summary> + <description>Value set to true uses monochrome icons</description> + </key> + + <key name="show-input-slider" type="b"> + <default>true</default> + <summary>Preference to show input slider always</summary> + <description>Value set to true displays the slider control for input device volume</description> + </key> + + <key name="show-input-devices" type="b"> + <default>true</default> + <summary>Preference to show input device chooser</summary> + <description>Value set to true displays the device chooser for input devices</description> + </key> + + <key name="show-output-devices" type="b"> + <default>true</default> + <summary>Preference to show output device chooser</summary> + <description>Value set to true displays the device chooser for output devices</description> + </key> + + <key name="ports-settings" type="s"> + <default>"{\"version\":3,\"ports\":[]}"</default> + <summary>Preference to hide/show different ports always</summary> + <description></description> + </key> + + <key name="icon-theme" type="s"> + <default>"monochrome"</default> + <summary>Preference indicating the type of icons used by the extension</summary> + <description>Value can be "colored", "monochrome", "none", etc.</description> + </key> + + <key name="hide-menu-icons" type="b"> + <default>false</default> + <summary>Preference indicating whether the icons are hidden in the drop-down menu (but are visible in the expanded list).</summary> + <description></description> + </key> + + <key name="enable-log" type="b"> + <default>false</default> + <summary>Preference indicating log messages should be written to console</summary> + <description></description> + </key> + + <key name="new-profile-indentification" type="b"> + <default>true</default> + <summary>(Deprecated)Old preference name with a typo. To be removed in the next shell version</summary> + <description></description> + </key> + + <key name="new-profile-identification" type="b"> + <default>true</default> + <summary>Preference to enable python script to identify port profiles</summary> + <description></description> + </key> + + <key name="cannot-activate-hidden-device" type="b"> + <default>true</default> + <summary>Preference to avoid activation of hidden devices in Port Settings</summary> + <description></description> + </key> + + <key name="omit-device-origins" type="b"> + <default>false</default> + <summary>Preference to omit device origin at Volume Menu</summary> + <description></description> + </key> + + <key name="integrate-with-slider" type="b"> + <default>false</default> + <summary>Preference to integrate selector with slider</summary> + <description></description> + </key> + </schema> +</schemalist> diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/ui/prefs-dialog.glade b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/ui/prefs-dialog.glade new file mode 100644 index 0000000..b6e62e7 --- /dev/null +++ b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/ui/prefs-dialog.glade @@ -0,0 +1,956 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.38.2 --> +<interface> + <requires lib="gtk+" version="3.12"/> + <object class="GtkListStore" id="icon-theme-store"> + <columns> + <!-- column-name Key --> + <column type="gchararray"/> + <!-- column-name Value --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0">monochrome</col> + <col id="1" translatable="yes">Monochrome</col> + </row> + <row> + <col id="0">colored</col> + <col id="1" translatable="yes">Colored</col> + </row> + <row> + <col id="0">none</col> + <col id="1" translatable="yes">None</col> + </row> + </data> + </object> + <object class="GtkListStore" id="ports-store"> + <columns> + <!-- column-name Port --> + <column type="gchararray"/> + <!-- column-name ShowAlways --> + <column type="gboolean"/> + <!-- column-name HideAlways --> + <column type="gboolean"/> + <!-- column-name ShowOnActive --> + <column type="gboolean"/> + <!-- column-name PortRealName --> + <column type="gchararray"/> + <!-- column-name SelectedColumn --> + <column type="gint"/> + <!-- column-name CardName --> + <column type="gchararray"/> + <!-- column-name CardDescription --> + <column type="gchararray"/> + <!-- column-name DisplayName --> + <column type="gchararray"/> + <!-- column-name DeviceType --> + <column type="gchararray"/> + </columns> + </object> + <object class="GtkBox" id="main-container"> + <property name="can-focus">False</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <property name="homogeneous">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkFrame"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-start">10</property> + <property name="margin-end">10</property> + <property name="label-xalign">0.029999999329447746</property> + <property name="shadow-type">out</property> + <child> + <object class="GtkListBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="selection-mode">none</property> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Integrate selector with slider</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSwitch" id="integrate-with-slider"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="halign">end</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Hide selector if there's only one device</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSwitch" id="hide-on-single-device"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="halign">end</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Display audio profiles for selection</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSwitch" id="show-profiles"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="halign">end</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Omit device origins at Volume Menu</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSwitch" id="omit-device-origins"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="halign">end</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Extend Volume Menu to fit device names</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSwitch" id="expand-volume-menu"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="halign">end</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">6</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Don't allow device hidden in Port Settings to be activated</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSwitch" id="cannot-activate-hidden-device"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="halign">end</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">(Select / deselect the required device in the Gnome Sound Settings)</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes">General Settings</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkFrame"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-start">10</property> + <property name="margin-end">10</property> + <property name="label-xalign">0.029999999329447746</property> + <property name="shadow-type">out</property> + <child> + <object class="GtkListBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="valign">center</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Show output devices</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSwitch" id="show-output-devices"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="halign">end</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes">Output Devices</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkFrame"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-start">10</property> + <property name="margin-end">10</property> + <property name="label-xalign">0.029999999329447746</property> + <property name="shadow-type">out</property> + <child> + <object class="GtkListBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="selection-mode">none</property> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="hexpand">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="valign">center</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Show input devices</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSwitch" id="show-input-devices"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="halign">end</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="valign">center</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Show volume control for default device</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSwitch" id="show-input-slider"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="halign">end</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes">Input Devices</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkFrame"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-start">10</property> + <property name="margin-end">10</property> + <property name="label-xalign">0.029999999329447746</property> + <property name="shadow-type">in</property> + <child> + <object class="GtkListBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="selection-mode">none</property> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <child> + <object class="GtkBox"> + <property name="name">6</property> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Icon Theme</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="icon-theme"> + <property name="width-request">100</property> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="model">icon-theme-store</property> + <property name="id-column">0</property> + <child> + <object class="GtkCellRendererText" id="Text"/> + <attributes> + <attribute name="text">1</attribute> + </attributes> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Display icons only in selection list</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSwitch" id="hide-menu-icons"> + <property name="visible">True</property> + <property name="can-focus">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes">Icons</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkFrame"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-start">10</property> + <property name="margin-end">10</property> + <property name="label-xalign">0.029999999329447746</property> + <property name="shadow-type">in</property> + <child> + <object class="GtkListBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="selection-mode">none</property> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <child> + <object class="GtkBox"> + <property name="name">6</property> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Enable Log messages</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSwitch" id="enable-log"> + <property name="visible">True</property> + <property name="can-focus">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Enable new profile identification</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSwitch" id="new-profile-identification"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="active">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes">Miscellaneous</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkFrame"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-start">10</property> + <property name="margin-end">10</property> + <property name="vexpand">True</property> + <property name="label-xalign">0.029999999329447746</property> + <property name="shadow-type">out</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-bottom">4</property> + <property name="vexpand">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow1"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="opacity">0.9999999986588954</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <property name="resize-mode">queue</property> + <property name="shadow-type">in</property> + <property name="min-content-width">500</property> + <child> + <object class="GtkTreeView" id="port-treeview"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="vexpand">True</property> + <property name="model">ports-store</property> + <property name="headers-clickable">False</property> + <property name="enable-grid-lines">both</property> + <child internal-child="selection"> + <object class="GtkTreeSelection" id="treeview-selection1"/> + </child> + <child> + <object class="GtkTreeViewColumn" id="PortNameColumn"> + <property name="resizable">True</property> + <property name="sizing">autosize</property> + <property name="min-width">100</property> + <property name="title" translatable="yes">Name</property> + <property name="expand">True</property> + <child> + <object class="GtkCellRendererText" id="PortNameRenderer"/> + <attributes> + <attribute name="text">8</attribute> + </attributes> + </child> + </object> + </child> + <child> + <object class="GtkTreeViewColumn" id="DeviceTypeColumn"> + <property name="sizing">autosize</property> + <property name="min-width">100</property> + <property name="title" translatable="yes">Device Type</property> + <property name="expand">True</property> + <property name="sort-order">descending</property> + <child> + <object class="GtkCellRendererText" id="DeviceTypeRenderer"/> + <attributes> + <attribute name="text">9</attribute> + </attributes> + </child> + </object> + </child> + <child> + <object class="GtkTreeViewColumn" id="ShowAlwaysColumn"> + <property name="visible">False</property> + <property name="sizing">autosize</property> + <property name="title" translatable="yes">Show</property> + <child> + <object class="GtkCellRendererToggle" id="ShowAlwaysToggleRender"> + <property name="radio">True</property> + </object> + <attributes> + <attribute name="active">1</attribute> + </attributes> + </child> + </object> + </child> + <child> + <object class="GtkTreeViewColumn" id="HideAlwaysColumn"> + <property name="sizing">autosize</property> + <property name="title" translatable="yes">Hide</property> + <child> + <object class="GtkCellRendererToggle" id="HideAlwaysToggleRender"> + <property name="radio">True</property> + </object> + <attributes> + <attribute name="active">2</attribute> + </attributes> + </child> + </object> + </child> + <child> + <object class="GtkTreeViewColumn" id="ShowOnActiveColumn"> + <property name="sizing">autosize</property> + <property name="title" translatable="yes">Default</property> + <child> + <object class="GtkCellRendererToggle" id="ShowActiveToggleRender"> + <property name="radio">True</property> + </object> + <attributes> + <attribute name="active">3</attribute> + </attributes> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes">Port Settings</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> +</interface> diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/ui/prefs-dialog40.glade b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/ui/prefs-dialog40.glade new file mode 100644 index 0000000..7e30ccc --- /dev/null +++ b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/ui/prefs-dialog40.glade @@ -0,0 +1,655 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="4.0"/> + <object class="GtkListStore" id="icon-theme-store"> + <columns> + <!-- column-name Key --> + <column type="gchararray"/> + <!-- column-name Value --> + <column type="gchararray"/> + </columns> + <data> + <row> + <col id="0">monochrome</col> + <col id="1" translatable="yes">Monochrome</col> + </row> + <row> + <col id="0">colored</col> + <col id="1" translatable="yes">Colored</col> + </row> + <row> + <col id="0">none</col> + <col id="1" translatable="yes">None</col> + </row> + </data> + </object> + <object class="GtkListStore" id="ports-store"> + <columns> + <!-- column-name Port --> + <column type="gchararray"/> + <!-- column-name ShowAlways --> + <column type="gboolean"/> + <!-- column-name HideAlways --> + <column type="gboolean"/> + <!-- column-name ShowOnActive --> + <column type="gboolean"/> + <!-- column-name PortRealName --> + <column type="gchararray"/> + <!-- column-name SelectedColumn --> + <column type="gint"/> + <!-- column-name CardName --> + <column type="gchararray"/> + <!-- column-name CardDescription --> + <column type="gchararray"/> + <!-- column-name DisplayName --> + <column type="gchararray"/> + <!-- column-name DeviceType --> + <column type="gchararray"/> + </columns> + </object> + <object class="GtkBox" id="main-container"> + <property name="visible">1</property> + <property name="margin-start">6</property> + <property name="margin-end">6</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child> + <object class="GtkBox"> + <property name="can-focus">0</property> + <property name="hexpand">1</property> + <property name="vexpand">1</property> + <property name="homogeneous">1</property> + <child> + <object class="GtkBox"> + <property name="margin-start">12</property> + <property name="margin-end">6</property> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkFrame"> + <property name="can-focus">0</property> + <child> + <object class="GtkListBox"> + <property name="can-focus">0</property> + <property name="selection-mode">none</property> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="child"> + <object class="GtkBox"> + <property name="can-focus">0</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="hexpand">1</property> + <property name="halign">start</property> + <property name="margin-start">5</property> + <property name="label" translatable="yes">Integrate selector with slider</property> + </object> + </child> + <child> + <object class="GtkSwitch" id="integrate-with-slider"> + <property name="halign">end</property> + <property name="margin-end">5</property> + </object> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="child"> + <object class="GtkBox"> + <property name="can-focus">0</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="hexpand">1</property> + <property name="halign">start</property> + <property name="margin-start">5</property> + <property name="label" translatable="yes">Hide selector if there's only one device</property> + </object> + </child> + <child> + <object class="GtkSwitch" id="hide-on-single-device"> + <property name="halign">end</property> + <property name="margin-end">5</property> + </object> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="child"> + <object class="GtkBox"> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="hexpand">1</property> + <property name="halign">start</property> + <property name="margin-start">5</property> + <property name="label" translatable="yes">Display audio profiles for selection</property> + </object> + </child> + <child> + <object class="GtkSwitch" id="show-profiles"> + <property name="halign">end</property> + <property name="margin-end">5</property> + </object> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="child"> + <object class="GtkBox"> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="hexpand">1</property> + <property name="halign">start</property> + <property name="margin-start">5</property> + <property name="margin-end">20</property> + <property name="label" translatable="yes">Omit device origins at Volume Menu</property> + </object> + </child> + <child> + <object class="GtkSwitch" id="omit-device-origins"> + <property name="halign">end</property> + <property name="margin-end">5</property> + </object> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="child"> + <object class="GtkBox"> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="hexpand">1</property> + <property name="halign">start</property> + <property name="margin-start">5</property> + <property name="margin-end">20</property> + <property name="label" translatable="yes">Extend Volume Menu to fit device names</property> + </object> + </child> + <child> + <object class="GtkSwitch" id="expand-volume-menu"> + <property name="halign">end</property> + <property name="margin-end">5</property> + </object> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="child"> + <object class="GtkBox"> + <property name="margin-top">6</property> + <child> + <object class="GtkLabel"> + <property name="hexpand">1</property> + <property name="halign">start</property> + <property name="margin-start">5</property> + <property name="margin-end">20</property> + <property name="label" translatable="yes">Don't allow device hidden in Port Settings to be activated</property> + </object> + </child> + <child> + <object class="GtkSwitch" id="cannot-activate-hidden-device"> + <property name="halign">end</property> + <property name="margin-end">5</property> + </object> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <child> + <object class="GtkBox"> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="hexpand">1</property> + <property name="halign">start</property> + <property name="margin-start">5</property> + <property name="margin-end">20</property> + <property name="label" translatable="yes">(Select / deselect the required device in the Gnome Sound Settings)</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel"> + <property name="label" translatable="yes">General Settings</property> + <attributes> + <attribute name="weight" value="bold"></attribute> + </attributes> + </object> + </child> + </object> + </child> + <child> + <object class="GtkFrame"> + <child> + + <object class="GtkListBox"> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="child"> + <object class="GtkBox"> + <property name="valign">center</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="hexpand">1</property> + <property name="halign">start</property> + <property name="margin-start">5</property> + <property name="label" translatable="yes">Show output devices</property> + </object> + </child> + <child> + <object class="GtkSwitch" id="show-output-devices"> + <property name="halign">end</property> + <property name="margin-end">5</property> + </object> + </child> + </object> + </property> + </object> + </child> + </object> + + </child> + <child type="label"> + <object class="GtkLabel"> + <property name="label" translatable="yes">Output Devices</property> + <attributes> + <attribute name="weight" value="bold"></attribute> + </attributes> + </object> + </child> + </object> + </child> + <child> + <object class="GtkFrame"> + <child> + + <object class="GtkListBox"> + <property name="selection-mode">none</property> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="hexpand">1</property> + <property name="child"> + <object class="GtkBox"> + <property name="valign">center</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="hexpand">1</property> + <property name="halign">start</property> + <property name="margin-start">5</property> + <property name="label" translatable="yes">Show input devices</property> + </object> + </child> + <child> + <object class="GtkSwitch" id="show-input-devices"> + <property name="halign">end</property> + <property name="margin-end">5</property> + </object> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="valign">center</property> + <property name="child"> + <object class="GtkBox"> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="hexpand">1</property> + <property name="halign">start</property> + <property name="margin-start">5</property> + <property name="label" translatable="yes">Show volume control for default device</property> + </object> + </child> + <child> + <object class="GtkSwitch" id="show-input-slider"> + <property name="halign">end</property> + <property name="margin-end">5</property> + </object> + </child> + </object> + </property> + </object> + </child> + </object> + + </child> + <child type="label"> + <object class="GtkLabel"> + <property name="label" translatable="yes">Input Devices</property> + <attributes> + <attribute name="weight" value="bold"></attribute> + </attributes> + </object> + </child> + </object> + </child> + <child> + <object class="GtkFrame"> + <child> + + <object class="GtkListBox"> + <property name="selection-mode">none</property> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="child"> + <object class="GtkBox"> + <property name="name">6</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="hexpand">1</property> + <property name="halign">start</property> + <property name="margin-start">5</property> + <property name="label" translatable="yes">Icon Theme</property> + </object> + </child> + <child> + <object class="GtkComboBox" id="icon-theme"> + <property name="width-request">100</property> + <property name="margin-end">5</property> + <property name="model">icon-theme-store</property> + <property name="id-column">0</property> + <child> + <object class="GtkCellRendererText" id="Text" /> + <attributes> + <attribute name="text">1</attribute> + </attributes> + </child> + </object> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="child"> + <object class="GtkBox"> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="hexpand">1</property> + <property name="halign">start</property> + <property name="margin-start">5</property> + <property name="label" translatable="yes">Display icons only in selection list</property> + </object> + </child> + <child> + <object class="GtkSwitch" id="hide-menu-icons"> + <property name="margin-end">5</property> + </object> + </child> + </object> + </property> + </object> + </child> + </object> + + </child> + <child type="label"> + <object class="GtkLabel"> + <property name="label" translatable="yes">Icons</property> + <attributes> + <attribute name="weight" value="bold"></attribute> + </attributes> + </object> + </child> + </object> + </child> + <child> + <object class="GtkFrame"> + <child> + + <object class="GtkListBox"> + <property name="selection-mode">none</property> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="child"> + <object class="GtkBox"> + <property name="name">6</property> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="hexpand">1</property> + <property name="halign">start</property> + <property name="margin-start">5</property> + <property name="label" translatable="yes">Enable Log messages</property> + </object> + </child> + <child> + <object class="GtkSwitch" id="enable-log" /> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="child"> + <object class="GtkBox"> + <property name="margin-top">6</property> + <property name="margin-bottom">6</property> + <child> + <object class="GtkLabel"> + <property name="hexpand">1</property> + <property name="halign">start</property> + <property name="margin-start">5</property> + <property name="label" translatable="yes">Enable new profile identification</property> + </object> + </child> + <child> + <object class="GtkSwitch" id="new-profile-identification"> + <property name="active">1</property> + </object> + </child> + </object> + </property> + </object> + </child> + </object> + > + </child> + <child type="label"> + <object class="GtkLabel"> + <property name="label" translatable="yes">Miscellaneous</property> + <attributes> + <attribute name="weight" value="bold"></attribute> + </attributes> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="margin-start">6</property> + <property name="margin-end">12</property> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkFrame"> + <property name="vexpand">1</property> + + <property name="child"> + <object class="GtkBox"> + <property name="margin-start">4</property> + <property name="margin-end">4</property> + <property name="margin-bottom">4</property> + <property name="vexpand">1</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow1"> + <property name="opacity">0.9999999986588954</property> + <property name="hexpand">1</property> + <property name="vexpand">1</property> + <property name="min-content-width">500</property> + <property name="child"> + <object class="GtkBox"> <!--TODO: Delete when Gnome 40 get the scrolled window bug(?) get fixed--> + <child> <!--TODO: Delete when Gnome 40 get the scrolled window bug(?) get fixed--> + <object class="GtkTreeView" id="port-treeview"> + <property name="vexpand">1</property> + <property name="model">ports-store</property> + <child internal-child="selection"> + <object class="GtkTreeSelection" id="treeview-selection1" /> + </child> + <child> + <object class="GtkTreeViewColumn" id="PortNameColumn"> + <property name="resizable">1</property> + <property name="sizing">autosize</property> + <property name="min-width">100</property> + <property name="title" translatable="yes">Name</property> + <property name="expand">1</property> + <property name="sort-order">descending</property> + <child> + <object class="GtkCellRendererText" id="PortNameRenderer" /> + <attributes> + <attribute name="text">8</attribute> + </attributes> + </child> + </object> + </child> + <child> + <object class="GtkTreeViewColumn" id="DeviceTypeColumn"> + <property name="sizing">autosize</property> + <property name="min-width">100</property> + <property name="title" translatable="yes">Device Type</property> + <property name="expand">True</property> + <property name="sort-order">descending</property> + <child> + <object class="GtkCellRendererText" id="DeviceTypeRenderer"/> + <attributes> + <attribute name="text">9</attribute> + </attributes> + </child> + </object> + </child> + <child> + <object class="GtkTreeViewColumn" id="ShowAlwaysColumn"> + <property name="visible">False</property> + <property name="sizing">autosize</property> + <property name="title" translatable="yes">Show</property> + <child> + <object class="GtkCellRendererToggle" id="ShowAlwaysToggleRender"> + <property name="radio">1</property> + </object> + <attributes> + <attribute name="active">1</attribute> + </attributes> + </child> + </object> + </child> + <child> + <object class="GtkTreeViewColumn" id="HideAlwaysColumn"> + <property name="sizing">autosize</property> + <property name="title" translatable="yes">Hide</property> + <child> + <object class="GtkCellRendererToggle" id="HideAlwaysToggleRender"> + <property name="radio">1</property> + </object> + <attributes> + <attribute name="active">2</attribute> + </attributes> + </child> + </object> + </child> + <child> + <object class="GtkTreeViewColumn" id="ShowOnActiveColumn"> + <property name="sizing">autosize</property> + <property name="title" translatable="yes">Default</property> + <child> + <object class="GtkCellRendererToggle" id="ShowActiveToggleRender"> + <property name="radio">1</property> + </object> + <attributes> + <attribute name="active">3</attribute> + </attributes> + </child> + </object> + </child> + </object> + </child> <!--TODO: Delete when Gnome 40 get the scrolled window bug(?) get fixed--> + </object> <!--TODO: Delete when Gnome 40 get the scrolled window bug(?) get fixed--> + </property> + </object> + </child> + </object> + </property> + <child type="label"> + <object class="GtkLabel"> + <property name="label" translatable="yes">Port Settings</property> + <attributes> + <attribute name="weight" value="bold"></attribute> + </attributes> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> +</interface> diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/utils/__pycache__/libpulse_introspect.cpython-310.pyc b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/utils/__pycache__/libpulse_introspect.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cf3a19f9fe1e92c21cf01e000facdb6637e292f6 GIT binary patch literal 9748 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 K)0)PUfBhc_DYv}< literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/utils/__pycache__/libpulse_introspect.cpython-39.pyc b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/utils/__pycache__/libpulse_introspect.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..778f7f943ee22a3cb62bfbac8054053d93347b9b GIT binary patch literal 9908 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/utils/libpulse_introspect.py b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/utils/libpulse_introspect.py new file mode 100644 index 0000000..af4209a --- /dev/null +++ b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/utils/libpulse_introspect.py @@ -0,0 +1,544 @@ +# This file is generated using clang2py script. The following files are used +# '/usr/include/pulse/introspect.h' '/usr/include/pulse/mainloop.h' '/usr/include/pulse/context.h' +# Refer additional licensing requirements for the files included +# sample commands used +# python3 /usr/bin/clang2py --clang-args="-I/usr/include/clang/6.0/include -I/usr/include/pulse" -l /usr/lib/libpulse.so '/usr/include/pulse/introspect.h' '/usr/include/pulse/mainloop.h' '/usr/include/pulse/proplist.h' +# python3 /usr/local/bin/clang2py --clang-args="-I/usr/include/clang/6.0/include -I/usr/include/pulse" -l /usr/lib/x86_64-linux-gnu/libpulse.so '/usr/include/pulse/introspect.h' '/usr/include/pulse/mainloop.h' +# python3 /usr/local/bin/clang2py --clang-args="-I/usr/include/clang/6.0/include -I/usr/include/pulse" -l /usr/lib/x86_64-linux-gnu/libpulse.so '/usr/include/pulse/context.h' +################################################################################ +# # This program is free software: you can redistribute it and/or modify it under +# # the terms of the GNU General Public License as published by the Free Software +# # Foundation, either version 3 of the License, or (at your option) any later +# # version. +# # +# # This program is distributed in the hope that it will be useful, but WITHOUT +# # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# # details. +# # +# # You should have received a copy of the GNU General Public License along with +# # this program. If not, see <http://www.gnu.org/licenses/>. +# # +# # Original Author: Gopi Sankar Karmegam +# ############################################################################## +# -#- coding: utf-8 -#- +# +# TARGET arch is: ['-I/usr/include/clang/6.0/include', '-I/usr/include/pulse'] +# WORD_SIZE is: 8 +# POINTER_SIZE is: 8 +# LONGDOUBLE_SIZE is: 16 +# + +# Updated to determine libpulse.so location +import ctypes +from ctypes.util import find_library + +c_int128 = ctypes.c_ubyte*16 +c_uint128 = c_int128 +void = None +if ctypes.sizeof(ctypes.c_longdouble) == 16: + c_long_double_t = ctypes.c_longdouble +else: + c_long_double_t = ctypes.c_ubyte*16 + +# if local wordsize is same as target, keep ctypes pointer function. +if ctypes.sizeof(ctypes.c_void_p) == 8: + POINTER_T = ctypes.POINTER +else: + # required to access _ctypes + import _ctypes + # Emulate a pointer class using the approriate c_int32/c_int64 type + # The new class should have : + # ['__module__', 'from_param', '_type_', '__dict__', '__weakref__', '__doc__'] + # but the class should be submitted to a unique instance for each base type + # to that if A == B, POINTER_T(A) == POINTER_T(B) + ctypes._pointer_t_type_cache = {} + def POINTER_T(pointee): + # a pointer should have the same length as LONG + fake_ptr_base_type = ctypes.c_uint64 + # specific case for c_void_p + if pointee is None: # VOID pointer type. c_void_p. + pointee = type(None) # ctypes.c_void_p # ctypes.c_ulong + clsname = 'c_void' + else: + clsname = pointee.__name__ + if clsname in ctypes._pointer_t_type_cache: + return ctypes._pointer_t_type_cache[clsname] + # make template + class _T(_ctypes._SimpleCData,): + _type_ = 'L' + _subtype_ = pointee + def _sub_addr_(self): + return self.value + def __repr__(self): + return '%s(%d)'%(clsname, self.value) + def contents(self): + raise TypeError('This is not a ctypes pointer.') + def __init__(self, **args): + raise TypeError('This is not a ctypes pointer. It is not instanciable.') + _class = type('LP_%d_%s'%(8, clsname), (_T,),{}) + ctypes._pointer_t_type_cache[clsname] = _class + return _class + +_libraries = {} + +libpulse_library_name = find_library('pulse') +if libpulse_library_name is None: + raise Exception('No libpulse.so library found!') + +try: + _libraries['libpulse.so'] = ctypes.cdll.LoadLibrary(libpulse_library_name) +except OSError: + raise Exception('Cannot load libpulse.so library!') + + +uint32_t = ctypes.c_uint32 + +size_t = ctypes.c_uint64 +class struct_pa_context(ctypes.Structure): + pass + +pa_context = struct_pa_context +pa_context_notify_cb_t = ctypes.CFUNCTYPE(None, POINTER_T(struct_pa_context), POINTER_T(None)) +pa_context_success_cb_t = POINTER_T(ctypes.CFUNCTYPE(None, POINTER_T(struct_pa_context), ctypes.c_int32, POINTER_T(None))) + +class struct_pa_proplist(ctypes.Structure): + pass + +pa_context_event_cb_t = POINTER_T(ctypes.CFUNCTYPE(None, POINTER_T(struct_pa_context), ctypes.c_char_p, POINTER_T(struct_pa_proplist), POINTER_T(None))) +class struct_pa_mainloop_api(ctypes.Structure): + pass + +pa_context_new = _libraries['libpulse.so'] .pa_context_new +pa_context_new.restype = POINTER_T(struct_pa_context) +pa_context_new.argtypes = [POINTER_T(struct_pa_mainloop_api), ctypes.c_char_p] + +# pa_context_new_with_proplist = _libraries['libpulse.so'] .pa_context_new_with_proplist +# pa_context_new_with_proplist.restype = POINTER_T(struct_pa_context) +# pa_context_new_with_proplist.argtypes = [POINTER_T(struct_pa_mainloop_api), ctypes.c_char_p, POINTER_T(struct_pa_proplist)] + +pa_context_unref = _libraries['libpulse.so'] .pa_context_unref +pa_context_unref.restype = None +pa_context_unref.argtypes = [POINTER_T(struct_pa_context)] + +# pa_context_ref = _libraries['libpulse.so'] .pa_context_ref +# pa_context_ref.restype = POINTER_T(struct_pa_context) +# pa_context_ref.argtypes = [POINTER_T(struct_pa_context)] + +pa_context_set_state_callback = _libraries['libpulse.so'] .pa_context_set_state_callback +pa_context_set_state_callback.restype = None +pa_context_set_state_callback.argtypes = [POINTER_T(struct_pa_context), pa_context_notify_cb_t, POINTER_T(None)] +# +# pa_context_set_event_callback = _libraries['libpulse.so'] .pa_context_set_event_callback +# pa_context_set_event_callback.restype = None +# pa_context_set_event_callback.argtypes = [POINTER_T(struct_pa_context), pa_context_event_cb_t, POINTER_T(None)] +# +# pa_context_errno = _libraries['libpulse.so'] .pa_context_errno +# pa_context_errno.restype = ctypes.c_int32 +# pa_context_errno.argtypes = [POINTER_T(struct_pa_context)] +# +# pa_context_is_pending = _libraries['libpulse.so'] .pa_context_is_pending +# pa_context_is_pending.restype = ctypes.c_int32 +# pa_context_is_pending.argtypes = [POINTER_T(struct_pa_context)] + + + +# values for enumeration 'pa_context_state' +pa_context_state__enumvalues = { + 0: 'PA_CONTEXT_UNCONNECTED', + 1: 'PA_CONTEXT_CONNECTING', + 2: 'PA_CONTEXT_AUTHORIZING', + 3: 'PA_CONTEXT_SETTING_NAME', + 4: 'PA_CONTEXT_READY', + 5: 'PA_CONTEXT_FAILED', + 6: 'PA_CONTEXT_TERMINATED', +} + +PA_CONTEXT_UNCONNECTED = 0 +PA_CONTEXT_CONNECTING = 1 +PA_CONTEXT_AUTHORIZING = 2 +PA_CONTEXT_SETTING_NAME = 3 +PA_CONTEXT_READY = 4 +PA_CONTEXT_FAILED = 5 +PA_CONTEXT_TERMINATED = 6 + +pa_context_state = ctypes.c_int # enum +pa_context_state_t = pa_context_state +pa_context_state_t__enumvalues = pa_context_state__enumvalues + +pa_context_get_state = _libraries['libpulse.so'] .pa_context_get_state +pa_context_get_state.restype = pa_context_state_t +pa_context_get_state.argtypes = [POINTER_T(struct_pa_context)] + +# values for enumeration 'pa_context_flags' +pa_context_flags__enumvalues = { + 0: 'PA_CONTEXT_NOFLAGS', + 1: 'PA_CONTEXT_NOAUTOSPAWN', + 2: 'PA_CONTEXT_NOFAIL', +} +PA_CONTEXT_NOFLAGS = 0 +PA_CONTEXT_NOAUTOSPAWN = 1 +PA_CONTEXT_NOFAIL = 2 +pa_context_flags = ctypes.c_int # enum +pa_context_flags_t = pa_context_flags +pa_context_flags_t__enumvalues = pa_context_flags__enumvalues +class struct_pa_spawn_api(ctypes.Structure): + pass + +pa_context_connect = _libraries['libpulse.so'] .pa_context_connect +pa_context_connect.restype = ctypes.c_int32 +pa_context_connect.argtypes = [POINTER_T(struct_pa_context), ctypes.c_char_p, pa_context_flags_t, POINTER_T(struct_pa_spawn_api)] +pa_context_disconnect = _libraries['libpulse.so'] .pa_context_disconnect +pa_context_disconnect.restype = None +pa_context_disconnect.argtypes = [POINTER_T(struct_pa_context)] + +class struct_pa_operation(ctypes.Structure): + pass + +# pa_context_drain = _libraries['libpulse.so'] .pa_context_drain +# pa_context_drain.restype = POINTER_T(struct_pa_operation) +# pa_context_drain.argtypes = [POINTER_T(struct_pa_context), pa_context_notify_cb_t, POINTER_T(None)] +# pa_context_exit_daemon = _libraries['libpulse.so'] .pa_context_exit_daemon +# pa_context_exit_daemon.restype = POINTER_T(struct_pa_operation) +# pa_context_exit_daemon.argtypes = [POINTER_T(struct_pa_context), pa_context_success_cb_t, POINTER_T(None)] +# pa_context_set_default_sink = _libraries['libpulse.so'] .pa_context_set_default_sink +# pa_context_set_default_sink.restype = POINTER_T(struct_pa_operation) +# pa_context_set_default_sink.argtypes = [POINTER_T(struct_pa_context), ctypes.c_char_p, pa_context_success_cb_t, POINTER_T(None)] +# pa_context_set_default_source = _libraries['libpulse.so'] .pa_context_set_default_source +# pa_context_set_default_source.restype = POINTER_T(struct_pa_operation) +# pa_context_set_default_source.argtypes = [POINTER_T(struct_pa_context), ctypes.c_char_p, pa_context_success_cb_t, POINTER_T(None)] +# pa_context_is_local = _libraries['libpulse.so'] .pa_context_is_local +# pa_context_is_local.restype = ctypes.c_int32 +# pa_context_is_local.argtypes = [POINTER_T(struct_pa_context)] +# pa_context_set_name = _libraries['libpulse.so'] .pa_context_set_name +# pa_context_set_name.restype = POINTER_T(struct_pa_operation) +# pa_context_set_name.argtypes = [POINTER_T(struct_pa_context), ctypes.c_char_p, pa_context_success_cb_t, POINTER_T(None)] +# pa_context_get_server = _libraries['libpulse.so'] .pa_context_get_server +# pa_context_get_server.restype = ctypes.c_char_p +# pa_context_get_server.argtypes = [POINTER_T(struct_pa_context)] + +# pa_context_get_protocol_version = _libraries['libpulse.so'] .pa_context_get_protocol_version +# pa_context_get_protocol_version.restype = uint32_t +# pa_context_get_protocol_version.argtypes = [POINTER_T(struct_pa_context)] +# pa_context_get_server_protocol_version = _libraries['libpulse.so'] .pa_context_get_server_protocol_version +# pa_context_get_server_protocol_version.restype = uint32_t +# pa_context_get_server_protocol_version.argtypes = [POINTER_T(struct_pa_context)] +class struct_pa_card_profile_info(ctypes.Structure): + _pack_ = True # source:False + _fields_ = [ + ('name', ctypes.c_char_p), + ('description', ctypes.c_char_p), + ('n_sinks', ctypes.c_uint32), + ('n_sources', ctypes.c_uint32), + ('priority', ctypes.c_uint32), + ('PADDING_0', ctypes.c_ubyte * 4), + ] + +pa_card_profile_info = struct_pa_card_profile_info +class struct_pa_card_profile_info2(ctypes.Structure): + _pack_ = True # source:False + _fields_ = [ + ('name', ctypes.c_char_p), + ('description', ctypes.c_char_p), + ('n_sinks', ctypes.c_uint32), + ('n_sources', ctypes.c_uint32), + ('priority', ctypes.c_uint32), + ('available', ctypes.c_int32), + ] + +pa_card_profile_info2 = struct_pa_card_profile_info2 +class struct_pa_card_port_info(ctypes.Structure): + _pack_ = True # source:False + _fields_ = [ + ('name', ctypes.c_char_p), + ('description', ctypes.c_char_p), + ('priority', ctypes.c_uint32), + ('available', ctypes.c_int32), + ('direction', ctypes.c_int32), + ('n_profiles', ctypes.c_uint32), + ('profiles', POINTER_T(POINTER_T(struct_pa_card_profile_info))), + ('proplist', POINTER_T(struct_pa_proplist)), + ('latency_offset', ctypes.c_int64), + ('profiles2', POINTER_T(POINTER_T(struct_pa_card_profile_info2))), + ] + +pa_card_port_info = struct_pa_card_port_info +class struct_pa_card_info(ctypes.Structure): + _pack_ = True # source:False + _fields_ = [ + ('index', ctypes.c_uint32), + ('PADDING_0', ctypes.c_ubyte * 4), + ('name', ctypes.c_char_p), + ('owner_module', ctypes.c_uint32), + ('PADDING_1', ctypes.c_ubyte * 4), + ('driver', ctypes.c_char_p), + ('n_profiles', ctypes.c_uint32), + ('PADDING_2', ctypes.c_ubyte * 4), + ('profiles', POINTER_T(struct_pa_card_profile_info)), + ('active_profile', POINTER_T(struct_pa_card_profile_info)), + ('proplist', POINTER_T(struct_pa_proplist)), + ('n_ports', ctypes.c_uint32), + ('PADDING_3', ctypes.c_ubyte * 4), + ('ports', POINTER_T(POINTER_T(struct_pa_card_port_info))), + ('profiles2', POINTER_T(POINTER_T(struct_pa_card_profile_info2))), + ('active_profile2', POINTER_T(struct_pa_card_profile_info2)), + ] + +pa_card_info = struct_pa_card_info +pa_card_info_cb_t = ctypes.CFUNCTYPE(None, POINTER_T(struct_pa_context), POINTER_T(struct_pa_card_info), ctypes.c_int32, POINTER_T(None)) +pa_context_get_card_info_by_index = _libraries['libpulse.so'].pa_context_get_card_info_by_index +pa_context_get_card_info_by_index.restype = POINTER_T(struct_pa_operation) +pa_context_get_card_info_by_index.argtypes = [POINTER_T(struct_pa_context), uint32_t, pa_card_info_cb_t, POINTER_T(None)] + + +pa_context_get_card_info_list = _libraries['libpulse.so'].pa_context_get_card_info_list +pa_context_get_card_info_list.restype = POINTER_T(struct_pa_operation) +pa_context_get_card_info_list.argtypes = [POINTER_T(struct_pa_context), pa_card_info_cb_t, POINTER_T(None)] + + +# values for enumeration 'pa_update_mode' +# pa_update_mode__enumvalues = { +# 0: 'PA_UPDATE_SET', +# 1: 'PA_UPDATE_MERGE', +# 2: 'PA_UPDATE_REPLACE', +# } +# PA_UPDATE_SET = 0 +# PA_UPDATE_MERGE = 1 +# PA_UPDATE_REPLACE = 2 +# pa_update_mode = ctypes.c_int # enum +# pa_update_mode_t = pa_update_mode +# pa_update_mode_t__enumvalues = pa_update_mode__enumvalues +# pa_context_proplist_update = _libraries['libpulse.so'] .pa_context_proplist_update +# pa_context_proplist_update.restype = POINTER_T(struct_pa_operation) +# pa_context_proplist_update.argtypes = [POINTER_T(struct_pa_context), pa_update_mode_t, POINTER_T(struct_pa_proplist), pa_context_success_cb_t, POINTER_T(None)] +# pa_context_proplist_remove = _libraries['libpulse.so'] .pa_context_proplist_remove +# pa_context_proplist_remove.restype = POINTER_T(struct_pa_operation) +# pa_context_proplist_remove.argtypes = [POINTER_T(struct_pa_context), ctypes.c_char_p * 0, pa_context_success_cb_t, POINTER_T(None)] +# pa_context_get_index = _libraries['libpulse.so'] .pa_context_get_index +# pa_context_get_index.restype = uint32_t +# pa_context_get_index.argtypes = [POINTER_T(struct_pa_context)] +class struct_pa_time_event(ctypes.Structure): + pass + +# pa_usec_t = ctypes.c_uint64 +class struct_timeval(ctypes.Structure): + pass +# +# pa_time_event_cb_t = POINTER_T(ctypes.CFUNCTYPE(None, POINTER_T(struct_pa_mainloop_api), POINTER_T(struct_pa_time_event), POINTER_T(struct_timeval), POINTER_T(None))) +# pa_context_rttime_new = _libraries['libpulse.so'] .pa_context_rttime_new +# pa_context_rttime_new.restype = POINTER_T(struct_pa_time_event) +# pa_context_rttime_new.argtypes = [POINTER_T(struct_pa_context), pa_usec_t, pa_time_event_cb_t, POINTER_T(None)] +# pa_context_rttime_restart = _libraries['libpulse.so'] .pa_context_rttime_restart +# pa_context_rttime_restart.restype = None +# pa_context_rttime_restart.argtypes = [POINTER_T(struct_pa_context), POINTER_T(struct_pa_time_event), pa_usec_t] +class struct_pa_sample_spec(ctypes.Structure): + pass + +# pa_context_get_tile_size = _libraries['libpulse.so'] .pa_context_get_tile_size +# pa_context_get_tile_size.restype = size_t +# pa_context_get_tile_size.argtypes = [POINTER_T(struct_pa_context), POINTER_T(struct_pa_sample_spec)] +# pa_context_load_cookie_from_file = _libraries['libpulse.so'] .pa_context_load_cookie_from_file +# pa_context_load_cookie_from_file.restype = ctypes.c_int32 +# pa_context_load_cookie_from_file.argtypes = [POINTER_T(struct_pa_context), ctypes.c_char_p] +# struct_pa_spawn_api._pack_ = True # source:False +# struct_pa_spawn_api._fields_ = [ +# ('prefork', POINTER_T(ctypes.CFUNCTYPE(None))), +# ('postfork', POINTER_T(ctypes.CFUNCTYPE(None))), +# ('atfork', POINTER_T(ctypes.CFUNCTYPE(None))), +# ] +# + +# values for enumeration 'pa_io_event_flags' +pa_io_event_flags__enumvalues = { + 0: 'PA_IO_EVENT_NULL', + 1: 'PA_IO_EVENT_INPUT', + 2: 'PA_IO_EVENT_OUTPUT', + 4: 'PA_IO_EVENT_HANGUP', + 8: 'PA_IO_EVENT_ERROR', +} +PA_IO_EVENT_NULL = 0 +PA_IO_EVENT_INPUT = 1 +PA_IO_EVENT_OUTPUT = 2 +PA_IO_EVENT_HANGUP = 4 +PA_IO_EVENT_ERROR = 8 +pa_io_event_flags = ctypes.c_int # enum +class struct_pa_io_event(ctypes.Structure): + pass + +class struct_pa_defer_event(ctypes.Structure): + pass + +struct_pa_mainloop_api._pack_ = True # source:False +struct_pa_mainloop_api._fields_ = [ + ('userdata', POINTER_T(None)), + ('io_new', POINTER_T(ctypes.CFUNCTYPE(POINTER_T(struct_pa_io_event), POINTER_T(struct_pa_mainloop_api), ctypes.c_int32, pa_io_event_flags, POINTER_T(ctypes.CFUNCTYPE(None, POINTER_T(struct_pa_mainloop_api), POINTER_T(struct_pa_io_event), ctypes.c_int32, pa_io_event_flags, POINTER_T(None))), POINTER_T(None)))), + ('io_enable', POINTER_T(ctypes.CFUNCTYPE(None, POINTER_T(struct_pa_io_event), pa_io_event_flags))), + ('io_free', POINTER_T(ctypes.CFUNCTYPE(None, POINTER_T(struct_pa_io_event)))), + ('io_set_destroy', POINTER_T(ctypes.CFUNCTYPE(None, POINTER_T(struct_pa_io_event), POINTER_T(ctypes.CFUNCTYPE(None, POINTER_T(struct_pa_mainloop_api), POINTER_T(struct_pa_io_event), POINTER_T(None)))))), + ('time_new', POINTER_T(ctypes.CFUNCTYPE(POINTER_T(struct_pa_time_event), POINTER_T(struct_pa_mainloop_api), POINTER_T(struct_timeval), POINTER_T(ctypes.CFUNCTYPE(None, POINTER_T(struct_pa_mainloop_api), POINTER_T(struct_pa_time_event), POINTER_T(struct_timeval), POINTER_T(None))), POINTER_T(None)))), + ('time_restart', POINTER_T(ctypes.CFUNCTYPE(None, POINTER_T(struct_pa_time_event), POINTER_T(struct_timeval)))), + ('time_free', POINTER_T(ctypes.CFUNCTYPE(None, POINTER_T(struct_pa_time_event)))), + ('time_set_destroy', POINTER_T(ctypes.CFUNCTYPE(None, POINTER_T(struct_pa_time_event), POINTER_T(ctypes.CFUNCTYPE(None, POINTER_T(struct_pa_mainloop_api), POINTER_T(struct_pa_time_event), POINTER_T(None)))))), + ('defer_new', POINTER_T(ctypes.CFUNCTYPE(POINTER_T(struct_pa_defer_event), POINTER_T(struct_pa_mainloop_api), POINTER_T(ctypes.CFUNCTYPE(None, POINTER_T(struct_pa_mainloop_api), POINTER_T(struct_pa_defer_event), POINTER_T(None))), POINTER_T(None)))), + ('defer_enable', POINTER_T(ctypes.CFUNCTYPE(None, POINTER_T(struct_pa_defer_event), ctypes.c_int32))), + ('defer_free', POINTER_T(ctypes.CFUNCTYPE(None, POINTER_T(struct_pa_defer_event)))), + ('defer_set_destroy', POINTER_T(ctypes.CFUNCTYPE(None, POINTER_T(struct_pa_defer_event), POINTER_T(ctypes.CFUNCTYPE(None, POINTER_T(struct_pa_mainloop_api), POINTER_T(struct_pa_defer_event), POINTER_T(None)))))), + ('quit', POINTER_T(ctypes.CFUNCTYPE(None, POINTER_T(struct_pa_mainloop_api), ctypes.c_int32))), +] + +class struct_pollfd(ctypes.Structure): + pass + +class struct_pa_mainloop(ctypes.Structure): + pass + +pa_mainloop = struct_pa_mainloop +pa_mainloop_new = _libraries['libpulse.so'] .pa_mainloop_new +pa_mainloop_new.restype = POINTER_T(struct_pa_mainloop) +pa_mainloop_new.argtypes = [] +pa_mainloop_free = _libraries['libpulse.so'] .pa_mainloop_free +pa_mainloop_free.restype = None +pa_mainloop_free.argtypes = [POINTER_T(struct_pa_mainloop)] + +# pa_mainloop_prepare = _libraries['libpulse.so'] .pa_mainloop_prepare +# pa_mainloop_prepare.restype = ctypes.c_int32 +# pa_mainloop_prepare.argtypes = [POINTER_T(struct_pa_mainloop), ctypes.c_int32] +# pa_mainloop_poll = _libraries['libpulse.so'] .pa_mainloop_poll +# pa_mainloop_poll.restype = ctypes.c_int32 +# pa_mainloop_poll.argtypes = [POINTER_T(struct_pa_mainloop)] +# pa_mainloop_dispatch = _libraries['libpulse.so'] .pa_mainloop_dispatch +# pa_mainloop_dispatch.restype = ctypes.c_int32 +# pa_mainloop_dispatch.argtypes = [POINTER_T(struct_pa_mainloop)] +# pa_mainloop_get_retval = _libraries['libpulse.so'] .pa_mainloop_get_retval +# pa_mainloop_get_retval.restype = ctypes.c_int32 +# pa_mainloop_get_retval.argtypes = [POINTER_T(struct_pa_mainloop)] + +pa_mainloop_iterate = _libraries['libpulse.so'] .pa_mainloop_iterate +pa_mainloop_iterate.restype = ctypes.c_int32 +pa_mainloop_iterate.argtypes = [POINTER_T(struct_pa_mainloop), ctypes.c_int32, POINTER_T(ctypes.c_int32)] + +# pa_mainloop_run = _libraries['libpulse.so'] .pa_mainloop_run +# pa_mainloop_run.restype = ctypes.c_int32 +# pa_mainloop_run.argtypes = [POINTER_T(struct_pa_mainloop), POINTER_T(ctypes.c_int32)] +pa_mainloop_get_api = _libraries['libpulse.so'] .pa_mainloop_get_api +pa_mainloop_get_api.restype = POINTER_T(struct_pa_mainloop_api) +pa_mainloop_get_api.argtypes = [POINTER_T(struct_pa_mainloop)] +# pa_mainloop_quit = _libraries['libpulse.so'] .pa_mainloop_quit +# pa_mainloop_quit.restype = None +# pa_mainloop_quit.argtypes = [POINTER_T(struct_pa_mainloop), ctypes.c_int32] +# pa_mainloop_wakeup = _libraries['libpulse.so'] .pa_mainloop_wakeup +# pa_mainloop_wakeup.restype = None +# pa_mainloop_wakeup.argtypes = [POINTER_T(struct_pa_mainloop)] +# pa_poll_func = POINTER_T(ctypes.CFUNCTYPE(ctypes.c_int32, POINTER_T(struct_pollfd), ctypes.c_uint64, ctypes.c_int32, POINTER_T(None))) +# pa_mainloop_set_poll_func = _libraries['libpulse.so'] .pa_mainloop_set_poll_func +# pa_mainloop_set_poll_func.restype = None +# pa_mainloop_set_poll_func.argtypes = [POINTER_T(struct_pa_mainloop), pa_poll_func, POINTER_T(None)] + +pa_operation_unref = _libraries['libpulse.so'] .pa_operation_unref +pa_operation_unref.restype = None +pa_operation_unref.argtypes = [POINTER_T(struct_pa_operation)] + +# values for enumeration 'pa_sample_format' +# pa_sample_format__enumvalues = { +# 0: 'PA_SAMPLE_U8', +# 1: 'PA_SAMPLE_ALAW', +# 2: 'PA_SAMPLE_ULAW', +# 3: 'PA_SAMPLE_S16LE', +# 4: 'PA_SAMPLE_S16BE', +# 5: 'PA_SAMPLE_FLOAT32LE', +# 6: 'PA_SAMPLE_FLOAT32BE', +# 7: 'PA_SAMPLE_S32LE', +# 8: 'PA_SAMPLE_S32BE', +# 9: 'PA_SAMPLE_S24LE', +# 10: 'PA_SAMPLE_S24BE', +# 11: 'PA_SAMPLE_S24_32LE', +# 12: 'PA_SAMPLE_S24_32BE', +# 13: 'PA_SAMPLE_MAX', +# -1: 'PA_SAMPLE_INVALID', +# } +# PA_SAMPLE_U8 = 0 +# PA_SAMPLE_ALAW = 1 +# PA_SAMPLE_ULAW = 2 +# PA_SAMPLE_S16LE = 3 +# PA_SAMPLE_S16BE = 4 +# PA_SAMPLE_FLOAT32LE = 5 +# PA_SAMPLE_FLOAT32BE = 6 +# PA_SAMPLE_S32LE = 7 +# PA_SAMPLE_S32BE = 8 +# PA_SAMPLE_S24LE = 9 +# PA_SAMPLE_S24BE = 10 +# PA_SAMPLE_S24_32LE = 11 +# PA_SAMPLE_S24_32BE = 12 +# PA_SAMPLE_MAX = 13 +# PA_SAMPLE_INVALID = -1 +# pa_sample_format = ctypes.c_int # enum +# struct_pa_sample_spec._pack_ = True # source:False +# struct_pa_sample_spec._fields_ = [ +# ('format', pa_sample_format), +# ('rate', ctypes.c_uint32), +# ('channels', ctypes.c_ubyte), +# ('PADDING_0', ctypes.c_ubyte * 3), +# ] +# +# struct_timeval._pack_ = True # source:False +# struct_timeval._fields_ = [ +# ('tv_sec', ctypes.c_int64), +# ('tv_usec', ctypes.c_int64), +# ] + +pa_proplist_to_string = _libraries['libpulse.so'].pa_proplist_to_string +pa_proplist_to_string.restype = POINTER_T(ctypes.c_char) +pa_proplist_to_string.argtypes = [POINTER_T(struct_pa_proplist)] + +pa_proplist_gets = _libraries['libpulse.so'].pa_proplist_gets +pa_proplist_gets.restype = POINTER_T(ctypes.c_char) +pa_proplist_gets.argtypes = [POINTER_T(struct_pa_proplist), POINTER_T(ctypes.c_char)] +PA_DIRECTION_OUTPUT = 0x0001 +PA_DIRECTION_INPUT = 0x0002 + + +__all__ = \ + ['PA_CONTEXT_AUTHORIZING', 'PA_CONTEXT_CONNECTING', + 'PA_CONTEXT_FAILED', 'PA_CONTEXT_NOAUTOSPAWN', + 'PA_CONTEXT_NOFAIL', 'PA_CONTEXT_NOFLAGS', 'PA_CONTEXT_READY', + 'PA_CONTEXT_SETTING_NAME', 'PA_CONTEXT_TERMINATED', + 'PA_CONTEXT_UNCONNECTED', 'PA_IO_EVENT_ERROR', + 'PA_IO_EVENT_HANGUP', 'PA_IO_EVENT_INPUT', 'PA_IO_EVENT_NULL', + 'PA_IO_EVENT_OUTPUT', 'PA_SAMPLE_ALAW', 'PA_SAMPLE_FLOAT32BE', + 'PA_SAMPLE_FLOAT32LE', 'PA_SAMPLE_INVALID', 'PA_SAMPLE_MAX', + 'PA_SAMPLE_S16BE', 'PA_SAMPLE_S16LE', 'PA_SAMPLE_S24BE', + 'PA_SAMPLE_S24LE', 'PA_SAMPLE_S24_32BE', 'PA_SAMPLE_S24_32LE', + 'PA_SAMPLE_S32BE', 'PA_SAMPLE_S32LE', 'PA_SAMPLE_U8', + 'PA_SAMPLE_ULAW', 'PA_UPDATE_MERGE', 'PA_UPDATE_REPLACE', + 'PA_UPDATE_SET', 'pa_context', 'pa_context_connect', + 'pa_context_disconnect', 'pa_context_drain', 'pa_context_errno', + 'pa_context_event_cb_t', 'pa_context_exit_daemon', + 'pa_context_flags', 'pa_context_flags_t', + 'pa_context_flags_t__enumvalues', 'pa_context_get_index', + 'pa_context_get_protocol_version', 'pa_context_get_server', + 'pa_context_get_server_protocol_version', 'pa_context_get_state', + 'pa_context_get_tile_size', 'pa_context_is_local', + 'pa_context_is_pending', 'pa_context_load_cookie_from_file', + 'pa_context_new', 'pa_context_new_with_proplist', + 'pa_context_notify_cb_t', 'pa_context_proplist_remove', + 'pa_context_proplist_update', 'pa_context_ref', + 'pa_context_rttime_new', 'pa_context_rttime_restart', + 'pa_context_set_default_sink', 'pa_context_set_default_source', + 'pa_context_set_event_callback', 'pa_context_set_name', + 'pa_context_set_state_callback', 'pa_context_state', + 'pa_context_state_t', 'pa_context_state_t__enumvalues', + 'pa_context_success_cb_t', 'pa_context_unref', + 'pa_io_event_flags', 'pa_mainloop', 'pa_mainloop_dispatch', + 'pa_mainloop_free', 'pa_mainloop_get_api', + 'pa_mainloop_get_retval', 'pa_mainloop_iterate', + 'pa_mainloop_new', 'pa_mainloop_poll', 'pa_mainloop_prepare', + 'pa_mainloop_quit', 'pa_mainloop_run', + 'pa_mainloop_set_poll_func', 'pa_mainloop_wakeup', 'pa_poll_func', + 'pa_sample_format', 'pa_time_event_cb_t', 'pa_update_mode', + 'pa_update_mode_t', 'pa_update_mode_t__enumvalues', 'pa_usec_t', + 'size_t', 'struct_pa_context', 'struct_pa_defer_event', + 'struct_pa_io_event', 'struct_pa_mainloop', + 'struct_pa_mainloop_api', 'struct_pa_operation', + 'struct_pa_proplist', 'struct_pa_sample_spec', + 'struct_pa_spawn_api', 'struct_pa_time_event', 'struct_pollfd', + 'struct_timeval', 'uint32_t','pa_proplist_to_string','pa_proplist_gets','PA_DIRECTION_OUTPUT', 'PA_DIRECTION_INPUT'] diff --git a/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/utils/pa_helper.py b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/utils/pa_helper.py new file mode 100644 index 0000000..6ec79a0 --- /dev/null +++ b/.local/share/gnome-shell/extensions/sound-output-device-chooser@kgshank.net/utils/pa_helper.py @@ -0,0 +1,141 @@ +#!/usr/bin/python +############################################################################### + # This program is free software: you can redistribute it and/or modify it under + # the terms of the GNU General Public License as published by the Free Software + # Foundation, either version 3 of the License, or (at your option) any later + # version. + # + # This program is distributed in the hope that it will be useful, but WITHOUT + # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + # details. + # + # You should have received a copy of the GNU General Public License along with + # this program. If not, see <http://www.gnu.org/licenses/>. + # + # Original Author: Gopi Sankar Karmegam + ############################################################################## + +import libpulse_introspect as pa +import sys +from ctypes import c_int,byref, c_char_p, cast +import time +from json import dumps + +class PAHelper(): + + _error = { + 'success': False, + 'error': None, + } + _card_op_done = None + _pa_state = pa.PA_CONTEXT_UNCONNECTED + + + def __init__(self): + self._ports = [] + self._cards = {} + self.mainloop = pa.pa_mainloop_new() + self._context = pa.pa_context_new( pa.pa_mainloop_get_api(self.mainloop), b'PAHelper') + self._pa_context_notify_cb_t = pa.pa_context_notify_cb_t(self.pa_context_notify_cb_t) + pa.pa_context_set_state_callback(self._context, self._pa_context_notify_cb_t , None) + pa.pa_context_connect(self._context, None, 0, None) + self._opn_completed = False + + def print_card_info(self, index = None): + operation = None + retVal = c_int() + counter = 0 + + while counter < 10000 and self._opn_completed == False: + counter += 1 + if self._pa_state == pa.PA_CONTEXT_READY and operation == None: + self._pa_card_info_cb_t = pa.pa_card_info_cb_t(self.pa_card_info_cb) +# operation = pa.pa_context_get_card_info_by_index(self._context, +# index, self._pa_card_info_cb_t , None) + operation = pa.pa_context_get_card_info_list(self._context, + self._pa_card_info_cb_t , None) + + pa.pa_mainloop_iterate(self.mainloop, 0, byref(retVal)) + print(dumps({'cards': self._cards, 'ports':self._ports}, indent = 5)) + + try: + if operation: + pa.pa_operation_unref(operation) + + pa.pa_context_disconnect(self._context) + pa.pa_context_unref(self._context) + pa.pa_mainloop_free(self.mainloop) + except: + pass + + def pa_card_info_cb(self, context, card_info, eol, whatever): + + if not card_info or not card_info[0]: + return + + card = card_info[0] + #print (card.index) + card_obj = {} + card_obj['index'] = str(card.index) + self._cards[card.index] = card_obj + card_obj['profiles'] = [] + + card_name = cast(pa.pa_proplist_gets(card.proplist,c_char_p(b'alsa.card_name')),c_char_p) + card_obj['alsa_name'] = card_name.value.decode('utf8') if card_name else '' + description = cast(pa.pa_proplist_gets(card.proplist,c_char_p(b'device.description')),c_char_p) + card_obj['card_description'] = description.value.decode('utf8') if description else '' + + card_obj['name'] = card.name.decode('utf8') if card.name else '' + for k in range(0, card.n_profiles): + if(card.profiles2[k]): + profile = card.profiles2[k].contents + pobj = {} + pobj['name'] = profile.name.decode('utf8') if profile.name else '' + pobj['human_name'] = profile.description.decode('utf8') if profile.description else '' + pobj['available'] = profile.available + card_obj['profiles'].append(pobj) + + card_obj['ports'] = [] + for i in range(0, card.n_ports): + port = card.ports[i].contents +# print ("Port name "+ str(port.name)) + obj = {} + obj['name'] = port.name.decode('utf8') if port.name else '' + obj['human_name'] = port.description.decode('utf8') if port.description else '' + obj['direction'] = 'Output' if (port.direction & pa.PA_DIRECTION_OUTPUT) else 'Input' + obj['available'] = port.available + obj['n_profiles'] = port.n_profiles + obj['profiles'] = [] + obj['card_name'] = card_obj['name'] + obj['card_description'] = card_obj['card_description'] + for j in range(0, port.n_profiles): + if(port.profiles2[j]): + profile = port.profiles2[j].contents +# pobj = {} +# pobj['name'] = profile.name.decode('utf8') if profile.name else '' +# pobj['human_name'] = profile.description.decode('utf8') if profile.description else '' +# pobj['available'] = profile.available +# obj['profiles'].append(pobj) + if profile.name: + obj['profiles'].append(profile.name.decode('utf8')) + + self._ports.append(obj) + card_obj['ports'].append(obj) + + + + self._opn_completed = True + + + def pa_context_notify_cb_t(self, context, userdata): + try: + self._pa_state = pa.pa_context_get_state(context) + + except Exception: + self._pa_state = pa.PA_CONTEXT_FAILED + + +PAHelper().print_card_info() + + diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/README b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/README new file mode 100644 index 0000000..1f14023 --- /dev/null +++ b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/README @@ -0,0 +1,6 @@ +Dependencies: +libclutter, libgtop and NetworkManager gir bindings, and gnome-system-monitor + on Debian and Ubuntu: gir1.2-gtop-2.0, gir1.2-nm-1.0, gnome-system-monitor + on Fedora: libgtop2-devel NetworkManager-libnm-devel, gnome-system-monitor + on Mageia 64-bit: lib64gtop-gir2.0, lib64nm-gir1.0, lib64clutter-gir1.0, gnome-system-monitor + on Arch Linux: libgtop, networkmanager, gnome-system-monitor diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/compat.js b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/compat.js new file mode 100644 index 0000000..2e1be07 --- /dev/null +++ b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/compat.js @@ -0,0 +1,51 @@ +const Config = imports.misc.config; +const Clutter = imports.gi.Clutter; + +/** Compare two dotted version strings (like '10.2.3'). + * @returns {Integer} 0: v1 == v2, -1: v1 < v2, 1: v1 > v2 + */ +function versionCompare(v1, v2) { + let v1parts = ('' + v1).split('.') + let v2parts = ('' + v2).split('.') + let minLength = Math.min(v1parts.length, v2parts.length) + let i, p1, p2; + // Compare tuple pair-by-pair. + for (i = 0; i < minLength; i++) { + // Convert to integer if possible, because "8" > "10". + p1 = parseInt(v1parts[i], 10); + p2 = parseInt(v2parts[i], 10); + if (isNaN(p1)) { + p1 = v1parts[i]; + } + if (isNaN(p2)) { + p2 = v2parts[i]; + } + if (p1 === p2) { + continue; + } else if (p1 > p2) { + return 1; + } else if (p1 < p2) { + return -1; + } + // one operand is NaN + return NaN; + } + // The longer tuple is always considered 'greater' + if (v1parts.length === v2parts.length) { + return 0; + } + return (v1parts.length < v2parts.length) ? -1 : 1; +} + +function color_from_string(color) { + let clutterColor, res; + + if (!Clutter.Color.from_string) { + clutterColor = new Clutter.Color(); + clutterColor.from_string(color); + } else { + [res, clutterColor] = Clutter.Color.from_string(color); + } + + return clutterColor; +} diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/convenience.js b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/convenience.js new file mode 100644 index 0000000..79b85c1 --- /dev/null +++ b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/convenience.js @@ -0,0 +1,94 @@ +/* -*- mode: js; js-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* + Copyright (c) 2011-2012, Giovanni Campagna <scampa.giovanni@gmail.com> + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the GNOME nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +const Gettext = imports.gettext; +const Gio = imports.gi.Gio; + +const Config = imports.misc.config; +const ExtensionUtils = imports.misc.extensionUtils; + +/** + * initTranslations: + * @domain: (optional): the gettext domain to use + * + * Initialize Gettext to load translations from extensionsdir/locale. + * If @domain is not provided, it will be taken from metadata['gettext-domain'] + */ +function initTranslations(domain) { + let extension = ExtensionUtils.getCurrentExtension(); + + domain = domain || extension.metadata['gettext-domain']; + + // check if this extension was built with "make zip-file", and thus + // has the locale files in a subfolder + // otherwise assume that extension has been installed in the + // same prefix as gnome-shell + let localeDir = extension.dir.get_child('locale'); + if (localeDir.query_exists(null)) { + Gettext.bindtextdomain(domain, localeDir.get_path()); + } else { + Gettext.bindtextdomain(domain, Config.LOCALEDIR); + } +} + +/** + * getSettings: + * @schema: (optional): the GSettings schema id + * + * Builds and return a GSettings schema for @schema, using schema files + * in extensionsdir/schemas. If @schema is not provided, it is taken from + * metadata['settings-schema']. + */ +function getSettings(schema) { + let extension = ExtensionUtils.getCurrentExtension(); + + schema = schema || extension.metadata['settings-schema']; + + const GioSSS = Gio.SettingsSchemaSource; + + // check if this extension was built with "make zip-file", and thus + // has the schema files in a subfolder + // otherwise assume that extension has been installed in the + // same prefix as gnome-shell (and therefore schemas are available + // in the standard folders) + let schemaDir = extension.dir.get_child('schemas'); + let schemaSource; + if (schemaDir.query_exists(null)) { + schemaSource = GioSSS.new_from_directory(schemaDir.get_path(), + GioSSS.get_default(), + false); + } else { + schemaSource = GioSSS.get_default(); + } + + let schemaObj = schemaSource.lookup(schema, true); + if (!schemaObj) { + throw new Error('Schema ' + schema + ' could not be found for extension ' + extension.metadata.uuid + '. Please check your installation.'); + } + + return new Gio.Settings({settings_schema: schemaObj}); +} diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/extension.js b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/extension.js new file mode 100644 index 0000000..1c56f0d --- /dev/null +++ b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/extension.js @@ -0,0 +1,2592 @@ +/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ + +// system-monitor: Gnome shell extension displaying system informations in gnome shell status bar, such as memory usage, cpu usage, network rates… +// Copyright (C) 2011 Florian Mounier aka paradoxxxzero + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +// Author: Florian Mounier aka paradoxxxzero + +/* Ugly. This is here so that we don't crash old libnm-glib based shells unnecessarily + * by loading the new libnm.so. Should go away eventually */ + +var libnm_glib = imports.gi.GIRepository.Repository.get_default().is_registered('NMClient', '1.0'); + +var smDepsGtop = true; +var smDepsNM = true; + +var Config = imports.misc.config; +var Clutter = imports.gi.Clutter; +var GLib = imports.gi.GLib; +var GObject = imports.gi.GObject; +var Lang = imports.lang; + +var Gio = imports.gi.Gio; +var Shell = imports.gi.Shell; +var St = imports.gi.St; +const UPower = imports.gi.UPowerGlib; + +// const System = imports.system; +var ModalDialog = imports.ui.modalDialog; + +var ByteArray = imports.byteArray; + +var ExtensionSystem = imports.ui.extensionSystem; +var ExtensionUtils = imports.misc.extensionUtils; + +var Me = ExtensionUtils.getCurrentExtension(); +var Convenience = Me.imports.convenience; +var Compat = Me.imports.compat; + +var Background, GTop, IconSize, Locale, MountsMonitor, NM, NetworkManager, Schema, StatusArea, Style, gc_timeout, menu_timeout; + +try { + GTop = imports.gi.GTop; +} catch (e) { + log('[System monitor] catched error: ' + e); + smDepsGtop = false; +} + +try { + NM = libnm_glib ? imports.gi.NMClient : imports.gi.NM; + NetworkManager = libnm_glib ? imports.gi.NetworkManager : NM; +} catch (e) { + log('[System monitor] catched error: ' + e); + smDepsNM = false; +} + +const Main = imports.ui.main; +const Panel = imports.ui.panel; +const PanelMenu = imports.ui.panelMenu; +const PopupMenu = imports.ui.popupMenu; + +const Gettext = imports.gettext.domain('system-monitor'); +const Mainloop = imports.mainloop; +const Util = imports.misc.util; +const _ = Gettext.gettext; + +const MESSAGE = _('Dependencies Missing\n\ +Please install: \n\ +gnome-system-monitor and libgtop, clutter and Network Manager gir bindings \n\ +\t on Debian and Ubuntu: gir1.2-gtop-2.0, gir1.2-nm-1.0, gir1.2-clutter-1.0, gnome-system-monitor \n\ +\t on Fedora: libgtop2-devel, NetworkManager-libnm-devel, gnome-system-monitor \n\ +\t on Arch: libgtop, networkmanager, gnome-system-monitor\n\ +\t on openSUSE: typelib-1_0-GTop-2_0, typelib-1_0-NetworkManager-1_0, gnome-system-monitor \n\ +\t on Mageia 64-bit: lib64gtop-gir2.0, lib64nm-gir1.0, lib64clutter-gir1.0, gnome-system-monitor\n'); + +// stale network shares will cause the shell to freeze, enable this with caution +const ENABLE_NETWORK_DISK_USAGE = false; + +let extension = imports.misc.extensionUtils.getCurrentExtension(); +let metadata = extension.metadata; +let shell_Version = Config.PACKAGE_VERSION; + +Clutter.Actor.prototype.raise_top = function raise_top() { + const parent = this.get_parent(); + if (!parent) { + return; + } + parent.set_child_above_sibling(this, null); +} +Clutter.Actor.prototype.reparent = function reparent(newParent) { + const parent = this.get_parent(); + if (parent) { + parent.remove_child(this); + } + newParent.add_child(this); +} + +function parse_bytearray(bytearray) { + if (!ByteArray.toString(bytearray).match(/GjsModule byteArray/)) { + return ByteArray.toString(bytearray); + } + return bytearray +} + +function l_limit(t) { + return (t > 0) ? t : 1000; +} + +function change_text() { + this.label.visible = Schema.get_boolean(this.elt + '-show-text'); +} + +function change_style() { + let style = Schema.get_string(this.elt + '-style'); + this.text_box.visible = style === 'digit' || style === 'both'; + this.chart.actor.visible = style === 'graph' || style === 'both'; +} + +function build_menu_info() { + let elts = Main.__sm.elts; + let tray_menu = Main.__sm.tray.menu; + + if (tray_menu._getMenuItems().length && + typeof tray_menu._getMenuItems()[0].actor.get_last_child() !== 'undefined') { + tray_menu._getMenuItems()[0].actor.get_last_child().destroy_all_children(); + for (let elt in elts) { + elts[elt].menu_items = elts[elt].create_menu_items(); + } + } else { + return; + } + + let menu_info_box_table = new St.Widget({ + style: 'padding: 10px 0px 10px 0px; spacing-rows: 10px; spacing-columns: 15px;', + layout_manager: new Clutter.GridLayout({orientation: Clutter.Orientation.VERTICAL}) + }); + let menu_info_box_table_layout = menu_info_box_table.layout_manager; + + // Populate Table + let row_index = 0; + for (let elt in elts) { + if (!elts[elt].menu_visible) { + continue; + } + + // Add item name to table + menu_info_box_table_layout.attach( + new St.Label({ + text: elts[elt].item_name, + style_class: Style.get('sm-title'), + x_align: Clutter.ActorAlign.START, + y_align: Clutter.ActorAlign.CENTER + }), 0, row_index, 1, 1); + + // Add item data to table + let col_index = 1; + for (let item in elts[elt].menu_items) { + menu_info_box_table_layout.attach( + elts[elt].menu_items[item], col_index, row_index, 1, 1); + + col_index++; + } + + row_index++; + } + if (shell_Version < '3.36') { + tray_menu._getMenuItems()[0].actor.get_last_child().add(menu_info_box_table, {expand: true}); + } else { + tray_menu._getMenuItems()[0].actor.get_last_child().add_child(menu_info_box_table); + } +} + +function change_menu() { + this.menu_visible = Schema.get_boolean(this.elt + '-show-menu'); + build_menu_info(); +} + +function change_usage() { + let usage = Schema.get_string('disk-usage-style'); + Main.__sm.pie.show(usage === 'pie'); + Main.__sm.bar.show(usage === 'bar'); +} +let color_from_string = Compat.color_from_string; + +function interesting_mountpoint(mount) { + if (mount.length < 3) { + return false; + } + + return ((mount[0].indexOf('/dev/') === 0 || mount[2].toLowerCase() === 'nfs') && mount[2].toLowerCase() !== 'udf'); +} + + +const smStyleManager = class SystemMonitor_smStyleManager { + constructor() { + this._extension = ''; + this._iconsize = 1; + this._diskunits = _('MiB/s'); + this._netunits_kbytes = _('KiB/s'); + this._netunits_mbytes = _('MiB/s'); + this._netunits_gbytes = _('GiB/s'); + this._netunits_kbits = _('kbit/s'); + this._netunits_mbits = _('Mbit/s'); + this._netunits_gbits = _('Gbit/s'); + this._pie_size = 300; + this._pie_fontsize = 14; + this._bar_width = 300; + this._bar_thickness = 15; + this._bar_fontsize = 14; + this._compact = Schema.get_boolean('compact-display'); + + if (this._compact) { + this._extension = '-compact'; + this._iconsize = 3 / 5; + this._diskunits = _('MB'); + this._netunits_kbytes = _('kB'); + this._netunits_mbytes = _('MB'); + this._netunits_gbytes = _('GB'); + this._netunits_kbits = 'kb'; + this._netunits_mbits = 'Mb'; + this._netunits_gbits = 'Gb'; + this._pie_size *= 4 / 5; + this._pie_fontsize = 12; + this._bar_width *= 3 / 5; + this._bar_thickness = 12; + this._bar_fontsize = 12; + } + } + get(style) { + return style + this._extension; + } + iconsize() { + return this._iconsize; + } + diskunits() { + return this._diskunits; + } + netunits_kbytes() { + return this._netunits_kbytes; + } + netunits_mbytes() { + return this._netunits_mbytes; + } + netunits_gbytes() { + return this._netunits_gbytes; + } + netunits_kbits() { + return this._netunits_kbits; + } + netunits_mbits() { + return this._netunits_mbits; + } + netunits_gbits() { + return this._netunits_gbits; + } + pie_size() { + return this._pie_size; + } + pie_fontsize() { + return this._pie_fontsize; + } + bar_width() { + return this._bar_width; + } + bar_thickness() { + return this._bar_thickness; + } + bar_fontsize() { + return this._bar_fontsize; + } +} + +const smDialog = class SystemMonitor_smDialog extends ModalDialog.ModalDialog { + constructor() { + super({styleClass: 'prompt-dialog'}); + let mainContentBox = new St.BoxLayout({style_class: 'prompt-dialog-main-layout', + vertical: false}); + this.contentLayout.add(mainContentBox, + {x_fill: true, + y_fill: true}); + + let messageBox = new St.BoxLayout({style_class: 'prompt-dialog-message-layout', + vertical: true}); + mainContentBox.add(messageBox, + {y_align: St.Align.START}); + + this._subjectLabel = new St.Label({style_class: 'prompt-dialog-headline', + text: _('System Monitor Extension')}); + + messageBox.add(this._subjectLabel, + {y_fill: false, + y_align: St.Align.START}); + + this._descriptionLabel = new St.Label({style_class: 'prompt-dialog-description', + text: MESSAGE}); + + messageBox.add(this._descriptionLabel, + {y_fill: true, + y_align: St.Align.START}); + + + this.setButtons([ + { + label: _('Cancel'), + action: () => { + this.close(); + }, + key: Clutter.Escape + } + ]); + } +} + +const Chart = class SystemMonitor_Chart { + constructor(width, height, parent) { + this.actor = new St.DrawingArea({style_class: Style.get('sm-chart'), reactive: false}); + this.parentC = parent; + this.width = width; + let themeContext = St.ThemeContext.get_for_stage(global.stage); + this.scale_factor = themeContext.scale_factor; + this.actor.set_width(this.width * this.scale_factor); + this.actor.set_height(height); + this.data = []; + for (let i = 0; i < this.parentC.colors.length; i++) { + this.data[i] = []; + } + themeContext.connect('notify::scale-factor', this.rescale.bind(this)); + this.actor.connect('repaint', this._draw.bind(this)); + } + update() { + let data_a = this.parentC.vals; + if (data_a.length !== this.parentC.colors.length) { + return; + } + let accdata = []; + for (let l = 0; l < data_a.length; l++) { + accdata[l] = (l === 0) ? data_a[0] : accdata[l - 1] + ((data_a[l] > 0) ? data_a[l] : 0); + this.data[l].push(accdata[l]); + if (this.data[l].length > this.width) { + this.data[l].shift(); + } + } + if (!this.actor.visible) { + return; + } + this.actor.queue_repaint(); + } + _draw() { + if (!this.actor.visible) { + return; + } + let [width, height] = this.actor.get_surface_size(); + let cr = this.actor.get_context(); + let max; + if (this.parentC.max) { + max = this.parentC.max; + } else { + max = Math.max.apply(this, this.data[this.data.length - 1]); + max = Math.max(1, Math.pow(2, Math.ceil(Math.log(max) / Math.log(2)))); + } + Clutter.cairo_set_source_color(cr, Background); + cr.rectangle(0, 0, width, height); + cr.fill(); + for (let i = this.parentC.colors.length - 1; i >= 0; i--) { + let samples = this.data[i].length - 1; + if (samples > 0) { + cr.moveTo(width, height); // bottom right + let x = width - 0.25 * this.scale_factor; + cr.lineTo(x, (1 - this.data[i][samples] / max) * height); + x -= 0.5 * this.scale_factor; + for (let j = samples; j >= 0; j--) { + let y = (1 - this.data[i][j] / max) * height; + cr.lineTo(x, y); + x -= 0.5 * this.scale_factor; + cr.lineTo(x, y); + x -= 0.5 * this.scale_factor; + } + x += 0.25 * this.scale_factor; + cr.lineTo(x, (1 - this.data[i][0] / max) * height); + cr.lineTo(x, height); + cr.closePath(); + Clutter.cairo_set_source_color(cr, this.parentC.colors[i]); + cr.fill(); + } + } + cr.$dispose(); + } + resize(width) { + if (this.width === width) { + return; + } + this.width = width; + if (this.width < this.data[0].length) { + for (let i = 0; i < this.parentC.colors.length; i++) { + this.data[i] = this.data[i].slice(-this.width); + } + } + this.actor.set_width(this.width * this.scale_factor); // repaints + } + rescale(themeContext) { + this.scale_factor = themeContext.scale_factor; + this.actor.set_width(this.width * this.scale_factor); // repaints + } +} + +// Class to deal with volumes insertion / ejection +const smMountsMonitor = class SystemMonitor_smMountsMonitor { + constructor() { + this.files = []; + this.num_mounts = -1; + this.listeners = []; + this.connected = false; + + this._volumeMonitor = Gio.VolumeMonitor.get(); + let sys_mounts = ['/home', '/tmp', '/boot', '/usr', '/usr/local']; + this.base_mounts = ['/']; + sys_mounts.forEach((sMount) => { + if (this.is_sys_mount(sMount + '/')) { + this.base_mounts.push(sMount); + } + }); + this.connect(); + } + refresh() { + // try check that number of volumes has changed + // try { + // let num_mounts = this.manager.getMounts().length; + // if (num_mounts == this.num_mounts) + // return; + // this.num_mounts = num_mounts; + // } catch (e) {}; + + // Can't get mountlist: + // GTop.glibtop_get_mountlist + // Error: No symbol 'glibtop_get_mountlist' in namespace 'GTop' + // Getting it with mtab + // let mount_lines = Shell.get_file_contents_utf8_sync('/etc/mtab').split("\n"); + // this.mounts = []; + // for(let mount_line in mount_lines) { + // let mount = mount_lines[mount_line].split(" "); + // if(interesting_mountpoint(mount) && this.mounts.indexOf(mount[1]) < 0) { + // this.mounts.push(mount[1]); + // } + // } + // log("[System monitor] old mounts: " + this.mounts); + this.mounts = []; + for (let base in this.base_mounts) { + // log("[System monitor] " + this.base_mounts[base]); + this.mounts.push(this.base_mounts[base]); + } + let mount_lines = this._volumeMonitor.get_mounts(); + mount_lines.forEach((mount) => { + if ((!this.is_net_mount(mount) || ENABLE_NETWORK_DISK_USAGE) && + !this.is_ro_mount(mount)) { + let mpath = mount.get_root().get_path() || mount.get_default_location().get_path(); + if (mpath) { + this.mounts.push(mpath); + } + } + }); + // log("[System monitor] base: " + this.base_mounts); + // log("[System monitor] mounts: " + this.mounts); + for (let i in this.listeners) { + this.listeners[i](this.mounts); + } + } + add_listener(cb) { + this.listeners.push(cb); + } + remove_listener(cb) { + this.listeners.pop(cb); + } + get_mounts() { + return this.mounts; + } + is_sys_mount(mpath) { + let file = Gio.file_new_for_path(mpath); + let info = file.query_info(Gio.FILE_ATTRIBUTE_UNIX_IS_MOUNTPOINT, + Gio.FileQueryInfoFlags.NONE, null); + return info.get_attribute_boolean(Gio.FILE_ATTRIBUTE_UNIX_IS_MOUNTPOINT); + } + is_ro_mount(mount) { + // FIXME: running this function after "login after waking from suspend" + // can make login hang. Actual issue seems to occur when a former net + // mount got broken (e.g. due to a VPN connection terminated or + // otherwise broken connection) + try { + let file = mount.get_default_location(); + let info = file.query_filesystem_info(Gio.FILE_ATTRIBUTE_FILESYSTEM_READONLY, null); + return info.get_attribute_boolean(Gio.FILE_ATTRIBUTE_FILESYSTEM_READONLY); + } catch (e) { + return false; + } + } + is_net_mount(mount) { + try { + let file = mount.get_default_location(); + let info = file.query_filesystem_info(Gio.FILE_ATTRIBUTE_FILESYSTEM_TYPE, null); + let result = info.get_attribute_string(Gio.FILE_ATTRIBUTE_FILESYSTEM_TYPE); + let net_fs = ['nfs', 'smbfs', 'cifs', 'ftp', 'sshfs', 'sftp', 'mtp', 'mtpfs']; + return !file.is_native() || net_fs.indexOf(result) > -1; + } catch (e) { + return false; + } + } + connect() { + if (this.connected) { + return; + } + try { + this.manager = this._volumeMonitor; + this.mount_added_id = this.manager.connect('mount-added', this.refresh.bind(this)); + this.mount_removed_id = this.manager.connect('mount-removed', this.refresh.bind(this)); + // need to add the other signals here + this.connected = true; + } catch (e) { + log('[System monitor] Failed to register on placesManager notifications'); + log('[System monitor] Got exception : ' + e); + } + this.refresh(); + } + disconnect() { + if (!this.connected) { + return; + } + this.manager.disconnect(this.mount_added_id); + this.manager.disconnect(this.mount_removed_id); + this.connected = false; + } + destroy() { + this.disconnect(); + } +} + +const Graph = class SystemMonitor_Graph { + constructor(width, height) { + this.menu_item = ''; + this.actor = new St.DrawingArea({style_class: Style.get('sm-chart'), reactive: false}); + this.width = width; + this.height = height; + this.gtop = new GTop.glibtop_fsusage(); + this.colors = ['#888', '#aaa', '#ccc']; + for (let color in this.colors) { + this.colors[color] = color_from_string(this.colors[color]); + } + + let themeContext = St.ThemeContext.get_for_stage(global.stage); + themeContext.connect('notify::scale-factor', this.set_scale.bind(this)); + this.scale_factor = themeContext.scale_factor; + let interfaceSettings = new Gio.Settings({ + schema: 'org.gnome.desktop.interface' + }); + interfaceSettings.connect('changed', this.set_text_scaling.bind(this)); + this.text_scaling = interfaceSettings.get_double('text-scaling-factor'); + if (!this.text_scaling) { + this.text_scaling = 1; + } + + this.actor.set_width(this.width * this.scale_factor * this.text_scaling); + this.actor.set_height(this.height * this.scale_factor * this.text_scaling); + this.actor.connect('repaint', this._draw.bind(this)); + } + create_menu_item() { + this.menu_item = new PopupMenu.PopupBaseMenuItem({reactive: false}); + if (shell_Version < '3.36') { + this.menu_item.actor.add(this.actor, {span: -1, expand: true}); + } else { + this.menu_item.actor.add_child(this.actor); + } + // tray.menu.addMenuItem(this.menu_item); + } + show(visible) { + this.menu_item.actor.visible = visible; + } + set_scale(themeContext) { + this.scale_factor = themeContext.scale_factor; + this.actor.set_width(this.width * this.scale_factor * this.text_scaling); + this.actor.set_height(this.height * this.scale_factor * this.text_scaling); + } + set_text_scaling(interfaceSettings, key) { + // FIXME: for some reason we only get this signal once, not on later + // changes to the setting + //log('[System monitor] got text scaling signal'); + this.text_scaling = interfaceSettings.get_double(key); + this.actor.set_width(this.width * this.scale_factor * this.text_scaling); + this.actor.set_height(this.height * this.scale_factor * this.text_scaling); + } +} + +const Bar = class SystemMonitor_Bar extends Graph { + constructor() { + // Height doesn't matter, it gets set on every draw. + super(Style.bar_width(), 100); + this.mounts = MountsMonitor.get_mounts(); + MountsMonitor.add_listener(this.update_mounts.bind(this)); + } + _draw() { + if (!this.actor.visible) { + return; + } + let thickness = Style.bar_thickness() * this.scale_factor * this.text_scaling; + let fontsize = Style.bar_fontsize() * this.scale_factor * this.text_scaling; + this.actor.set_height(this.mounts.length * (3 * thickness)); + let [width, height] = this.actor.get_surface_size(); + let cr = this.actor.get_context(); + + let x0 = width / 8; + let y0 = thickness / 2; + cr.setLineWidth(thickness); + cr.setFontSize(fontsize); + for (let mount in this.mounts) { + GTop.glibtop_get_fsusage(this.gtop, this.mounts[mount]); + let perc_full = (this.gtop.blocks - this.gtop.bfree) / this.gtop.blocks; + Clutter.cairo_set_source_color(cr, this.colors[mount % this.colors.length]); + + var text = this.mounts[mount]; + if (text.length > 10) { + text = text.split('/').pop(); + } + cr.moveTo(0, y0 + thickness / 3); + cr.showText(text); + cr.moveTo(width - x0, y0 + thickness / 3); + cr.showText(Math.round(perc_full * 100).toString() + '%'); + y0 += (5 * thickness) / 4; + + cr.moveTo(0, y0); + cr.relLineTo(perc_full * width, 0); + cr.stroke(); + y0 += (7 * thickness) / 4; + } + cr.$dispose(); + } + update_mounts(mounts) { + this.mounts = mounts; + this.actor.queue_repaint(); + } +} + +const Pie = class SystemMonitor_Pie extends Graph { + constructor() { + super(Style.pie_size(), Style.pie_size()); + this.mounts = MountsMonitor.get_mounts(); + MountsMonitor.add_listener(this.update_mounts.bind(this)); + } + + _draw() { + if (!this.actor.visible) { + return; + } + let [width, height] = this.actor.get_surface_size(); + let cr = this.actor.get_context(); + let xc = width / 2; + let yc = height / 2; + let pi = Math.PI; + function arc(r, value, max, angle) { + if (max === 0) { + return angle; + } + let new_angle = angle + (value * 2 * pi / max); + cr.arc(xc, yc, r, angle, new_angle); + return new_angle; + } + + // Set the ring thickness so that at least 7 rings can be displayed. If + // there are more mounts, make the rings thinner. If the rings are too + // thin to have a line height of 1.2 for the labels, shrink the labels. + let rings = Math.max(this.mounts.length, 7); + let ring_width = width / (2 * rings); + let fontsize = Style.pie_fontsize() * this.scale_factor * this.text_scaling; + if (ring_width < 1.2 * fontsize) { + fontsize = ring_width / 1.2; + } + let thickness = ring_width / 1.5; + + cr.setLineWidth(thickness); + cr.setFontSize(fontsize); + let r = (height - ring_width) / 2; + for (let mount in this.mounts) { + GTop.glibtop_get_fsusage(this.gtop, this.mounts[mount]); + Clutter.cairo_set_source_color(cr, this.colors[mount % this.colors.length]); + arc(r, this.gtop.blocks - this.gtop.bfree, this.gtop.blocks, -pi / 2); + cr.stroke(); + r -= ring_width; + } + let y = (ring_width + fontsize) / 2; + for (let mount in this.mounts) { + var text = this.mounts[mount]; + if (text.length > 10) { + text = text.split('/').pop(); + } + cr.moveTo(0, y); + cr.showText(text); + y += ring_width; + } + cr.$dispose(); + } + + update_mounts(mounts) { + this.mounts = mounts; + this.actor.queue_repaint(); + } +} + +var TipItem = null; + +if (shell_Version < '3.36') { + TipItem = class SystemMonitor_TipItem extends PopupMenu.PopupBaseMenuItem { + constructor() { + super(); + // PopupMenu.PopupBaseMenuItem.prototype._init.call(this); + this.actor.remove_style_class_name('popup-menu-item'); + this.actor.add_style_class_name('sm-tooltip-item'); + } + } +} else { + TipItem = GObject.registerClass( + { + GTypeName: 'TipItem' + }, + class SystemMonitor_TipItem extends PopupMenu.PopupBaseMenuItem { + _init() { + super._init(); + // PopupMenu.PopupBaseMenuItem.prototype._init.call(this); + this.actor.remove_style_class_name('popup-menu-item'); + this.actor.add_style_class_name('sm-tooltip-item'); + } + } + ); +} +const TipMenu = class SystemMonitor_TipMenu extends PopupMenu.PopupMenuBase { + constructor(sourceActor) { + // PopupMenu.PopupMenuBase.prototype._init.call(this, sourceActor, 'sm-tooltip-box'); + super(sourceActor, 'sm-tooltip-box'); + this.actor = new Clutter.Actor(); + // this.actor.connect('get-preferred-width', + // this._boxGetPreferredWidth).bind(this); + // this.actor.connect('get-preferred-height', + // this._boxGetPreferredHeight.bind(this)); + this.actor.add_actor(this.box); + } + // _boxGetPreferredWidth (actor, forHeight, alloc) { + // // let columnWidths = this.getColumnWidths(); + // // this.setColumnWidths(columnWidths); + // + // [alloc.min_size, alloc.natural_size] = this.box.get_preferred_width(forHeight); + // } + // _boxGetPreferredHeight (actor, forWidth, alloc) { + // [alloc.min_size, alloc.natural_size] = this.box.get_preferred_height(forWidth); + // } + // _boxAllocate (actor, box, flags) { + // this.box.allocate(box, flags); + // } + _shift() { + // Probably old but works + let node = this.sourceActor.get_theme_node(); + let contentbox = node.get_content_box(this.sourceActor.get_allocation_box()); + + let sourceTopLeftX = 0; + let sourceTopLeftY = 0; + if (typeof this.sourceActor.get_transformed_extents === 'function') { + let extents = this.sourceActor.get_transformed_extents(); + let sourceTopLeft = extents.get_top_left(); + sourceTopLeftY = sourceTopLeft.y; + sourceTopLeftX = sourceTopLeft.x; + } else { + let allocation = Shell.util_get_transformed_allocation(this.sourceActor); + sourceTopLeftY = allocation.y1; + sourceTopLeftX = allocation.x1; + } + let monitor = Main.layoutManager.findMonitorForActor(this.sourceActor); + let [x, y] = [sourceTopLeftX + contentbox.x1, + sourceTopLeftY + contentbox.y1]; + let [cx, cy] = [sourceTopLeftX + (contentbox.x1 + contentbox.x2) / 2, + sourceTopLeftY + (contentbox.y1 + contentbox.y2) / 2]; + let [xm, ym] = [sourceTopLeftX + contentbox.x2, + sourceTopLeftY + contentbox.y2]; + let [width, height] = this.actor.get_size(); + let tipx = cx - width / 2; + tipx = Math.max(tipx, monitor.x); + tipx = Math.min(tipx, monitor.x + monitor.width - width); + let tipy = Math.floor(ym); + // Hacky condition to determine if the status bar is at the top or at the bottom of the screen + if (sourceTopLeftY / monitor.height > 0.3) { + tipy = sourceTopLeftY - height; // If it is at the bottom, place the tooltip above instead of below + } + this.actor.set_position(tipx, tipy); + } + open(animate) { + if (this.isOpen) { + return; + } + + this.isOpen = true; + this.actor.show(); + this._shift(); + this.actor.raise_top(); + this.emit('open-state-changed', true); + } + close(animate) { + this.isOpen = false; + this.actor.hide(); + this.emit('open-state-changed', false); + } +} + +const TipBox = class SystemMonitor_TipBox { + constructor() { + this.actor = new St.BoxLayout({reactive: true}); + this.actor._delegate = this; + this.set_tip(new TipMenu(this.actor)); + this.in_to = this.out_to = 0; + this.actor.connect('enter-event', this.on_enter.bind(this)); + this.actor.connect('leave-event', this.on_leave.bind(this)); + } + set_tip(tipmenu) { + if (this.tipmenu) { + this.tipmenu.destroy(); + } + this.tipmenu = tipmenu; + if (this.tipmenu) { + Main.uiGroup.add_actor(this.tipmenu.actor); + this.hide_tip(); + } + } + show_tip() { + if (!this.tipmenu) { + return; + } + this.tipmenu.open(); + if (this.in_to) { + Mainloop.source_remove(this.in_to); + this.in_to = 0; + } + } + hide_tip() { + if (!this.tipmenu) { + return; + } + this.tipmenu.close(); + if (this.out_to) { + Mainloop.source_remove(this.out_to); + this.out_to = 0; + } + if (this.in_to) { + Mainloop.source_remove(this.in_to); + this.in_to = 0; + } + } + on_enter() { + let show_tooltip = Schema.get_boolean('show-tooltip'); + + if (!show_tooltip) { + return; + } + + if (this.out_to) { + Mainloop.source_remove(this.out_to); + this.out_to = 0; + } + if (!this.in_to) { + this.in_to = Mainloop.timeout_add(500, + this.show_tip.bind(this)); + } + } + on_leave() { + if (this.in_to) { + Mainloop.source_remove(this.in_to); + this.in_to = 0; + } + if (!this.out_to) { + this.out_to = Mainloop.timeout_add(500, + this.hide_tip.bind(this)); + } + } + destroy() { + if (this.in_to) { + Mainloop.source_remove(this.in_to); + this.in_to = 0; + } + + if (this.out_to) { + Mainloop.source_remove(this.out_to); + this.out_to = 0; + } + + this.actor.destroy(); + } +} + +const ElementBase = class SystemMonitor_ElementBase extends TipBox { + constructor(properties) { + super(); + this.elt = ''; + this.item_name = _(''); + this.color_name = []; + this.text_items = []; + this.menu_items = []; + this.menu_visible = true; + + Object.assign(this, properties); + + // TipBox.prototype._init.apply(this, arguments); + this.vals = []; + this.tip_labels = []; + this.tip_vals = []; + this.tip_unit_labels = []; + + this.colors = []; + for (let color in this.color_name) { + let name = this.elt + '-' + this.color_name[color] + '-color'; + let clutterColor = color_from_string(Schema.get_string(name)); + Schema.connect('changed::' + name, (schema, key) => { + this.clutterColor = color_from_string(Schema.get_string(key)); + }); + Schema.connect('changed::' + name, () => { + this.chart.actor.queue_repaint(); + }); + this.colors.push(clutterColor); + } + + let element_width = Schema.get_int(this.elt + '-graph-width'); + if (Style.get('') === '-compact') { + element_width = Math.round(element_width / 1.5); + } + this.chart = new Chart(element_width, IconSize, this); + + Schema.connect('changed::background', () => { + this.chart.actor.queue_repaint(); + }); + + this.actor.visible = Schema.get_boolean(this.elt + '-display'); + Schema.connect( + 'changed::' + this.elt + '-display', (schema, key) => { + this.actor.visible = Schema.get_boolean(key); + }); + + this.interval = l_limit(Schema.get_int(this.elt + '-refresh-time')); + this.timeout = Mainloop.timeout_add( + this.interval, + this.update.bind(this), + GLib.PRIORITY_DEFAULT_IDLE + ); + + Schema.connect( + 'changed::' + this.elt + '-refresh-time', + (schema, key) => { + Mainloop.source_remove(this.timeout); + this.timeout = null; + this.interval = l_limit(Schema.get_int(key)); + this.timeout = Mainloop.timeout_add( + this.interval, this.update.bind(this), GLib.PRIORITY_DEFAULT_IDLE); + }); + Schema.connect('changed::' + this.elt + '-graph-width', this.resize.bind(this)); + + if (this.elt === 'thermal') { + Schema.connect('changed::thermal-threshold', + () => { + Mainloop.source_remove(this.timeout); + this.timeout = null; + this.reset_style(); + this.timeout = Mainloop.timeout_add( + this.interval, this.update.bind(this), GLib.PRIORITY_DEFAULT_IDLE); + }); + } + + this.label = new St.Label({text: this.elt === 'memory' ? _('mem') : _(this.elt), + style_class: Style.get('sm-status-label')}); + change_text.call(this); + Schema.connect('changed::' + this.elt + '-show-text', change_text.bind(this)); + + this.menu_visible = Schema.get_boolean(this.elt + '-show-menu'); + Schema.connect('changed::' + this.elt + '-show-menu', change_menu.bind(this)); + + this.actor.add_actor(this.label); + this.text_box = new St.BoxLayout(); + + this.actor.add_actor(this.text_box); + this.text_items = this.create_text_items(); + for (let item in this.text_items) { + this.text_box.add_actor(this.text_items[item]); + } + this.actor.add_actor(this.chart.actor); + change_style.call(this); + Schema.connect('changed::' + this.elt + '-style', change_style.bind(this)); + this.menu_items = this.create_menu_items(); + } + tip_format(unit) { + if (typeof (unit) === 'undefined') { + unit = '%'; + } + if (typeof (unit) === 'string') { + let all_unit = unit; + unit = []; + for (let i = 0; i < this.color_name.length; i++) { + unit.push(all_unit); + } + } + for (let i = 0; i < this.color_name.length; i++) { + let tipline = new TipItem(); + this.tipmenu.addMenuItem(tipline); + tipline.actor.add(new St.Label({text: _(this.color_name[i])})); + this.tip_labels[i] = new St.Label({text: ''}); + tipline.actor.add(this.tip_labels[i]); + + this.tip_unit_labels[i] = new St.Label({text: unit[i]}); + tipline.actor.add(this.tip_unit_labels[i]); + this.tip_vals[i] = 0; + } + } + // set_tip_unit: function(unit) { + // for (let i = 0;i < this.tip_unit_labels.length;i++) { + // this.tip_unit_labels[i].text = unit[i]; + // } + // } + update() { + if (!this.menu_visible && !this.actor.visible) { + return false; + } + this.refresh(); + this._apply(); + if (this.elt === 'thermal') { + this.threshold(); + } + this.chart.update(); + for (let i = 0; i < this.tip_vals.length; i++) { + if (this.tip_labels[i]) { + this.tip_labels[i].text = this.tip_vals[i].toString(); + } + } + return true; + } + reset_style() { + this.text_items[0].set_style('color: rgba(255, 255, 255, 1)'); + } + threshold() { + if (Schema.get_int('thermal-threshold')) { + if (this.temp_over_threshold) { + this.text_items[0].set_style('color: rgba(255, 0, 0, 1)'); + } else { + this.text_items[0].set_style('color: rgba(255, 255, 255, 1)'); + } + } + } + resize(schema, key) { + let width = Schema.get_int(key); + if (Style.get('') === '-compact') { + width = Math.round(width / 1.5); + } + this.chart.resize(width); + } + destroy() { + TipBox.prototype.destroy.call(this); + if (this.timeout) { + Mainloop.source_remove(this.timeout); + this.timeout = null; + } + } +} + +const Battery = class SystemMonitor_Battery extends ElementBase { + constructor() { + super({ + elt: 'battery', + item_name: _('Battery'), + color_name: ['batt0'], + icon: '. GThemedIcon battery-good-symbolic battery-good' + }); + + this.max = 100; + this.icon_hidden = false; + this.percentage = 0; + this.timeString = '-- '; + this._proxy = StatusArea.aggregateMenu._power._proxy; + if (typeof (this._proxy) === 'undefined') { + this._proxy = StatusArea.battery._proxy; + } + this.powerSigID = this._proxy.connect('g-properties-changed', this.update_battery.bind(this)); + + // need to specify a default icon, since the contructor completes before UPower callback + this.gicon = Gio.icon_new_for_string(this.icon); + + this.tip_format('%'); + + this.update_battery(); + this.update_tips(); + // this.hide_system_icon(); + this.update(); + + // Schema.connect('changed::' + this.elt + '-hidesystem', this.hide_system_icon.bind(this)); + Schema.connect('changed::' + this.elt + '-time', this.update_tips.bind(this)); + } + refresh() { + // do nothing here? + } + update_battery() { + // callback function for when battery stats updated. + let battery_found = false; + let isBattery = false; + if (typeof (this._proxy.GetDevicesRemote) === 'undefined') { + let device_type = this._proxy.Type; + isBattery = (device_type === UPower.DeviceKind.BATTERY); + if (isBattery) { + battery_found = true; + let icon = this._proxy.IconName; + let percentage = this._proxy.Percentage; + let seconds = this._proxy.TimeToEmpty; + this.update_battery_value(seconds, percentage, icon); + } else { + // log("[System monitor] No battery found"); + this.actor.hide(); + this.menu_visible = false; + build_menu_info(); + } + } else { + this._proxy.GetDevicesRemote((devices, error) => { + if (error) { + log('[System monitor] Power proxy error: ' + error); + this.actor.hide(); + this.menu_visible = false; + build_menu_info(); + return; + } + + let [result] = devices; + for (let i = 0; i < result.length; i++) { + let [device_id, device_type, icon, percentage, state, seconds] = result[i]; + + isBattery = (device_type === UPower.DeviceKind.BATTERY); + if (isBattery) { + battery_found = true; + this.update_battery_value(seconds, percentage, icon); + break; + } + } + + if (!battery_found) { + // log("[System monitor] No battery found"); + this.actor.hide(); + this.menu_visible = false; + build_menu_info(); + } + }); + } + } + update_battery_value(seconds, percentage, icon) { + if (seconds > 60) { + let time = Math.round(seconds / 60); + let minutes = time % 60; + let hours = Math.floor(time / 60); + this.timeString = C_('battery time remaining', '%d:%02d').format(hours, minutes); + } else { + this.timeString = '-- '; + } + this.percentage = Math.ceil(percentage); + this.gicon = Gio.icon_new_for_string(icon); + + if (Schema.get_boolean(this.elt + '-display')) { + this.actor.show() + } + if (Schema.get_boolean(this.elt + '-show-menu') && !this.menu_visible) { + this.menu_visible = true; + build_menu_info(); + } + } + hide_system_icon(override) { + let value = Schema.get_boolean(this.elt + '-hidesystem'); + if (!override) { + value = false; + } + if (value && Schema.get_boolean(this.elt + '-display')) { + if (shell_Version > '3.5') { + if (StatusArea.battery.actor.visible) { + StatusArea.battery.destroy(); + this.icon_hidden = true; + } + } else { + for (let Index = 0; Index < Main.panel._rightBox.get_children().length; Index++) { + if (StatusArea.battery === Main.panel._rightBox.get_children()[Index]._delegate) { + Main.panel._rightBox.get_children()[Index].destroy(); + StatusArea.battery = null; + this.icon_hidden = true; + break; + } + } + } + } else if (this.icon_hidden) { + if (shell_Version < '3.5') { + let Indicator = new Panel.STANDARD_STATUS_AREA_SHELL_IMPLEMENTATION.battery(); + Main.panel.addToStatusArea('battery', Indicator, Panel.STANDARD_STATUS_AREA_ORDER.indexOf('battery')); + } else { + let Indicator = new Panel.PANEL_ITEM_IMPLEMENTATIONS.battery(); + Main.panel.addToStatusArea('battery', Indicator, Main.sessionMode.panel.right.indexOf('battery'), 'right'); + } + this.icon_hidden = false; + // Main.panel._updatePanel('right'); + } + } + get_battery_unit() { + let unitString; + let value = Schema.get_boolean(this.elt + '-time'); + + if (value) { + unitString = 'h'; + } else { + unitString = '%'; + } + + return unitString; + } + update_tips() { + let unitString = this.get_battery_unit(); + + if (Schema.get_boolean(this.elt + '-display')) { + this.text_items[2].text = unitString; + } + if (Schema.get_boolean(this.elt + '-show-menu')) { + this.menu_items[1].text = unitString; + } + + this.update(); + } + _apply() { + let displayString; + let value = Schema.get_boolean(this.elt + '-time'); + if (value) { + displayString = this.timeString; + } else { + displayString = this.percentage.toString() + } + if (Schema.get_boolean(this.elt + '-display')) { + this.text_items[0].gicon = this.gicon; + this.text_items[1].text = displayString; + } + if (Schema.get_boolean(this.elt + '-show-menu')) { + this.menu_items[0].text = displayString; + } + this.vals = [this.percentage]; + this.tip_vals[0] = Math.round(this.percentage); + } + create_text_items() { + return [ + new St.Icon({ + gicon: Gio.icon_new_for_string(this.icon), + style_class: Style.get('sm-status-icon')}), + new St.Label({ + text: '', + style_class: Style.get('sm-status-value'), + y_align: Clutter.ActorAlign.CENTER}), + new St.Label({ + text: this.get_battery_unit(), + style_class: Style.get('sm-perc-label'), + y_align: Clutter.ActorAlign.CENTER}) + ]; + } + create_menu_items() { + return [ + new St.Label({ + text: '', + style_class: Style.get('sm-value')}), + new St.Label({ + text: this.get_battery_unit(), + style_class: Style.get('sm-label')}) + ]; + } + destroy() { + ElementBase.prototype.destroy.call(this); + this._proxy.disconnect(this.powerSigID); + } +} + +const Cpu = class SystemMonitor_Cpu extends ElementBase { + constructor(cpuid) { + super({ + elt: 'cpu', + item_name: _('CPU'), + color_name: ['user', 'system', 'nice', 'iowait', 'other'], + cpuid: -1 // cpuid is -1 when all cores are displayed in the same graph + }); + this.max = 100; + + this.cpuid = cpuid; + this.gtop = new GTop.glibtop_cpu(); + this.last = [0, 0, 0, 0, 0]; + this.current = [0, 0, 0, 0, 0]; + try { + this.total_cores = GTop.glibtop_get_sysinfo().ncpu; + if (cpuid === -1) { + this.max *= this.total_cores; + } + } catch (e) { + this.total_cores = this.get_cores(); + global.logError(e) + } + this.last_total = 0; + this.usage = [0, 0, 0, 1, 0]; + this.item_name = _('Cpu'); + if (cpuid !== -1) { + this.item_name += ' ' + (cpuid + 1); + } // append cpu number to cpu name in popup + // ElementBase.prototype._init.call(this); + this.tip_format(); + this.update(); + } + refresh() { + GTop.glibtop_get_cpu(this.gtop); + // display global cpu usage on 1 graph + if (this.cpuid === -1) { + this.current[0] = this.gtop.user; + this.current[1] = this.gtop.sys; + this.current[2] = this.gtop.nice; + this.current[3] = this.gtop.idle; + this.current[4] = this.gtop.iowait; + let delta = (this.gtop.total - this.last_total) / (100 * this.total_cores); + + if (delta > 0) { + for (let i = 0; i < 5; i++) { + this.usage[i] = Math.round((this.current[i] - this.last[i]) / delta); + this.last[i] = this.current[i]; + } + this.last_total = this.gtop.total; + } else if (delta < 0) { + this.last = [0, 0, 0, 0, 0]; + this.current = [0, 0, 0, 0, 0]; + this.last_total = 0; + this.usage = [0, 0, 0, 1, 0]; + } + } else { + // display per cpu data + this.current[0] = this.gtop.xcpu_user[this.cpuid]; + this.current[1] = this.gtop.xcpu_sys[this.cpuid]; + this.current[2] = this.gtop.xcpu_nice[this.cpuid]; + this.current[3] = this.gtop.xcpu_idle[this.cpuid]; + this.current[4] = this.gtop.xcpu_iowait[this.cpuid]; + let delta = (this.gtop.xcpu_total[this.cpuid] - this.last_total) / 100; + + if (delta > 0) { + for (let i = 0; i < 5; i++) { + this.usage[i] = Math.round((this.current[i] - this.last[i]) / delta); + this.last[i] = this.current[i]; + } + this.last_total = this.gtop.xcpu_total[this.cpuid]; + } else if (delta < 0) { + this.last = [0, 0, 0, 0, 0]; + this.current = [0, 0, 0, 0, 0]; + this.last_total = 0; + this.usage = [0, 0, 0, 1, 0]; + } + } + + // GTop.glibtop_get_cpu(this.gtop); + // // display global cpu usage on 1 graph + // if (this.cpuid == -1) { + // this.current[0] = this.gtop.user; + // this.current[1] = this.gtop.sys; + // this.current[2] = this.gtop.nice; + // this.current[3] = this.gtop.idle; + // this.current[4] = this.gtop.iowait; + // } else { + // // display cpu usage for given core + // this.current[0] = this.gtop.xcpu_user[this.cpuid]; + // this.current[1] = this.gtop.xcpu_sys[this.cpuid]; + // this.current[2] = this.gtop.xcpu_nice[this.cpuid]; + // this.current[3] = this.gtop.xcpu_idle[this.cpuid]; + // this.current[4] = this.gtop.xcpu_iowait[this.cpuid]; + // } + // + // let delta = 0; + // if (this.cpuid == -1) + // delta = (this.gtop.total - this.last_total)/(100*this.total_cores); + // else + // delta = (this.gtop.xcpu_total[this.cpuid] - this.last_total)/100; + // + // if (delta > 0) { + // for (let i = 0;i < 5;i++) { + // this.usage[i] = Math.round((this.current[i] - this.last[i])/delta); + // this.last[i] = this.current[i]; + // } + // if (this.cpuid == -1) + // this.last_total = this.gtop.total; + // else + // this.last_total = this.gtop.xcpu_total[this.cpuid]; + // } + } + _apply() { + let percent = 0; + if (this.cpuid === -1) { + percent = Math.round(((100 * this.total_cores) - this.usage[3]) / + this.total_cores); + } else { + percent = Math.round((100 - this.usage[3])); + } + + this.text_items[0].text = this.menu_items[0].text = percent.toString(); + let other = 100; + for (let i = 0; i < this.usage.length; i++) { + other -= this.usage[i]; + } + // Not to be confusing + other = Math.max(0, other); + this.vals = [this.usage[0], this.usage[1], + this.usage[2], this.usage[4], other]; + for (let i = 0; i < 5; i++) { + this.tip_vals[i] = Math.round(this.vals[i]); + } + } + + get_cores() { + // Getting xcpu_total makes gjs 1.29.18 segfault + // let cores = 0; + // GTop.glibtop_get_cpu(this.gtop); + // let gtop_total = this.gtop.xcpu_total + // for (let i = 0; i < gtop_total.length;i++) { + // if (gtop_total[i] > 0) + // cores++; + // } + // return cores; + return 1; + } + create_text_items() { + return [ + new St.Label({ + text: '', + style_class: Style.get('sm-status-value'), + y_align: Clutter.ActorAlign.CENTER}), + new St.Label({ + text: '%', style_class: Style.get('sm-perc-label'), + y_align: Clutter.ActorAlign.CENTER}) + ]; + } + create_menu_items() { + return [ + new St.Label({ + text: '', + style_class: Style.get('sm-value')}), + new St.Label({ + text: '%', + style_class: Style.get('sm-label')}) + ]; + } +} + +// Check if one graph per core must be displayed and create the +// appropriate number of cpu items +function createCpus() { + let array = []; + let numcores = 1; + + if (Schema.get_boolean('cpu-individual-cores')) { + // get number of cores + let gtop = new GTop.glibtop_cpu(); + try { + numcores = GTop.glibtop_get_sysinfo().ncpu; + } catch (e) { + global.logError(e); + numcores = 1; + } + } + + // there are several cores to display, + // instantiate each cpu + if (numcores > 1) { + for (let i = 0; i < numcores; i++) { + array.push(new Cpu(i)); + } + } else { + // individual cores option is not set or we failed to + // get the number of cores, create a global cpu item + array.push(new Cpu(-1)); + } + + return array; +} + +const Disk = class SystemMonitor_Disk extends ElementBase { + constructor() { + super({ + elt: 'disk', + item_name: _('Disk'), + color_name: ['read', 'write'] + }); + this.mounts = MountsMonitor.get_mounts(); + MountsMonitor.add_listener(this.update_mounts.bind(this)); + this.last = [0, 0]; + this.usage = [0, 0]; + this.last_time = 0; + this.tip_format(_('MiB/s')); + this.update(); + } + update_mounts(mounts) { + this.mounts = mounts; + } + refresh() { + let accum = [0, 0]; + + let file = Gio.file_new_for_path('/proc/diskstats'); + file.load_contents_async(null, (source, result) => { + let as_r = source.load_contents_finish(result); + let lines = parse_bytearray(as_r[1]).toString().split('\n'); + + for (let i = 0; i < lines.length; i++) { + let line = lines[i]; + let entry = line.trim().split(/[\s]+/); + if (typeof (entry[1]) === 'undefined') { + break; + } + accum[0] += parseInt(entry[5]); + accum[1] += parseInt(entry[9]); + } + + let time = GLib.get_monotonic_time() / 1000; + let delta = (time - this.last_time) / 1000; + if (delta > 0) { + for (let i = 0; i < 2; i++) { + this.usage[i] = ((accum[i] - this.last[i]) / delta / 1024 / 8); + this.last[i] = accum[i]; + } + } + this.last_time = time; + }); + } + _apply() { + this.vals = this.usage.slice(); + for (let i = 0; i < 2; i++) { + if (this.usage[i] < 10) { + this.usage[i] = Math.round(10 * this.usage[i]) / 10; + } else { + this.usage[i] = Math.round(this.usage[i]); + } + } + this.tip_vals = [this.usage[0], this.usage[1]]; + this.menu_items[0].text = this.text_items[1].text = this.tip_vals[0].toLocaleString(Locale); + this.menu_items[3].text = this.text_items[4].text = this.tip_vals[1].toLocaleString(Locale); + } + create_text_items() { + return [ + new St.Label({ + text: _('R'), + style_class: Style.get('sm-status-label')}), + new St.Label({ + text: '', + style_class: Style.get('sm-disk-value'), + y_align: Clutter.ActorAlign.CENTER}), + new St.Label({ + text: Style.diskunits(), + style_class: Style.get('sm-disk-unit-label'), + y_align: Clutter.ActorAlign.CENTER}), + new St.Label({ + text: _('W'), + style_class: Style.get('sm-status-label')}), + new St.Label({ + text: '', + style_class: Style.get('sm-disk-value'), + y_align: Clutter.ActorAlign.CENTER}), + new St.Label({ + text: Style.diskunits(), + style_class: Style.get('sm-disk-unit-label'), + y_align: Clutter.ActorAlign.CENTER}) + ]; + } + create_menu_items() { + return [ + new St.Label({ + text: '', + style_class: Style.get('sm-value')}), + new St.Label({ + text: Style.diskunits(), + style_class: Style.get('sm-label')}), + new St.Label({ + text: _('R'), + style_class: Style.get('sm-label')}), + new St.Label({ + text: '', + style_class: Style.get('sm-value')}), + new St.Label({ + text: Style.diskunits(), + style_class: Style.get('sm-label')}), + new St.Label({ + text: ' ' + _('W'), + style_class: Style.get('sm-label')}) + ]; + } +} + +const Freq = class SystemMonitor_Freq extends ElementBase { + constructor() { + super({ + elt: 'freq', + item_name: _('Freq'), + color_name: ['freq'] + }); + this.freq = 0; + this.tip_format('MHz'); + this.update(); + } + refresh() { + let total_frequency = 0; + let num_cpus = GTop.glibtop_get_sysinfo().ncpu; + let i = 0; + let file = Gio.file_new_for_path(`/sys/devices/system/cpu/cpu${i}/cpufreq/scaling_cur_freq`); + var that = this; + file.load_contents_async(null, function cb(source, result) { + let as_r = source.load_contents_finish(result); + total_frequency += parseInt(parse_bytearray(as_r[1])); + + if (++i >= num_cpus) { + that.freq = Math.round(total_frequency / num_cpus / 1000); + } else { + file = Gio.file_new_for_path(`/sys/devices/system/cpu/cpu${i}/cpufreq/scaling_cur_freq`); + file.load_contents_async(null, cb.bind(that)); + } + }); + } + _apply() { + let value = this.freq.toString(); + this.text_items[0].text = value + ' '; + this.vals[0] = value; + this.tip_vals[0] = value; + if (Style.get('') !== '-compact') { + this.menu_items[0].text = value; + } else { + this.menu_items[0].text = this._pad(value, 4); + } + } + // pad a string with leading spaces + _pad(number, length) { + var str = '' + number; + while (str.length < length) { + str = ' ' + str; + } + return str; + } + create_text_items() { + return [ + new St.Label({ + text: '', + style_class: Style.get('sm-big-status-value'), + y_align: Clutter.ActorAlign.CENTER}), + new St.Label({ + text: 'MHz', style_class: Style.get('sm-perc-label'), + y_align: Clutter.ActorAlign.CENTER}) + ]; + } + create_menu_items() { + return [ + new St.Label({ + text: '', + style_class: Style.get('sm-value')}), + new St.Label({ + text: 'MHz', + style_class: Style.get('sm-label')}) + ]; + } +} + +const Mem = class SystemMonitor_Mem extends ElementBase { + constructor() { + super({ + elt: 'memory', + item_name: _('Memory'), + color_name: ['program', 'buffer', 'cache'] + }); + this.max = 1; + + this.gtop = new GTop.glibtop_mem(); + this.mem = [0, 0, 0]; + + GTop.glibtop_get_mem(this.gtop); + this.total = Math.round(this.gtop.total / 1024 / 1024); + let threshold = 4 * 1024; // In MiB + this.useGiB = false; + this._unitConversion = 1024 * 1024; + this._decimals = 100; + if (this.total > threshold) { + this.useGiB = true; + this._unitConversion *= 1024 / this._decimals; + } + + this.tip_format(); + this.update(); + } + refresh() { + GTop.glibtop_get_mem(this.gtop); + if (this.useGiB) { + this.mem[0] = Math.round(this.gtop.user / this._unitConversion); + this.mem[0] /= this._decimals; + this.mem[1] = Math.round(this.gtop.buffer / this._unitConversion); + this.mem[1] /= this._decimals; + this.mem[2] = Math.round(this.gtop.cached / this._unitConversion); + this.mem[2] /= this._decimals; + this.total = Math.round(this.gtop.total / this._unitConversion); + this.total /= this._decimals; + } else { + this.mem[0] = Math.round(this.gtop.user / this._unitConversion); + this.mem[1] = Math.round(this.gtop.buffer / this._unitConversion); + this.mem[2] = Math.round(this.gtop.cached / this._unitConversion); + this.total = Math.round(this.gtop.total / this._unitConversion); + } + } + _pad(number) { + if (this.useGiB) { + if (number < 1) { + // examples: 0.01, 0.10, 0.88 + return number.toLocaleString(Locale, {minimumFractionDigits: 2, maximumFractionDigits: 2}); + } + // examples: 5.85, 16.0, 128 + return number.toLocaleString(Locale, {minimumSignificantDigits: 3, maximumSignificantDigits: 3}); + } + + return number.toLocaleString(Locale); + } + _apply() { + if (this.total === 0) { + this.vals = this.tip_vals = [0, 0, 0]; + } else { + for (let i = 0; i < 3; i++) { + this.vals[i] = this.mem[i] / this.total; + this.tip_vals[i] = Math.round(this.vals[i] * 100); + } + } + this.text_items[0].text = this.tip_vals[0].toString(); + this.menu_items[0].text = this.tip_vals[0].toLocaleString(Locale); + if (Style.get('') !== '-compact') { + this.menu_items[3].text = this._pad(this.mem[0]) + + ' / ' + this._pad(this.total); + } else { + this.menu_items[3].text = this._pad(this.mem[0]) + + '/' + this._pad(this.total); + } + } + create_text_items() { + return [ + new St.Label({ + text: '', + style_class: Style.get('sm-status-value'), + y_align: Clutter.ActorAlign.CENTER}), + new St.Label({ + text: '%', style_class: Style.get('sm-perc-label'), + y_align: Clutter.ActorAlign.CENTER}) + ]; + } + create_menu_items() { + let unit = _('MiB'); + if (this.useGiB) { + unit = _('GiB'); + } + return [ + new St.Label({ + text: '', + style_class: Style.get('sm-value')}), + new St.Label({ + text: '%', + style_class: Style.get('sm-label')}), + new St.Label({ + text: '', + style_class: Style.get('sm-label')}), + new St.Label({ + text: '', + style_class: Style.get('sm-value')}), + new St.Label({text: unit, + style_class: Style.get('sm-label')}) + ]; + } +} + +const Net = class SystemMonitor_Net extends ElementBase { + constructor() { + super({ + elt: 'net', + item_name: _('Net'), + color_name: ['down', 'downerrors', 'up', 'uperrors', 'collisions'] + }); + this.speed_in_bits = false; + this.ifs = []; + this.client = libnm_glib ? NM.Client.new() : NM.Client.new(null); + this.update_iface_list(); + + if (!this.ifs.length) { + let net_lines = Shell.get_file_contents_utf8_sync('/proc/net/dev').split('\n'); + for (let i = 2; i < net_lines.length - 1; i++) { + let ifc = net_lines[i].replace(/^\s+/g, '').split(':')[0]; + if (Shell.get_file_contents_utf8_sync('/sys/class/net/' + ifc + '/operstate') + .replace(/\s/g, '') === 'up' && + ifc.indexOf('br') < 0 && + ifc.indexOf('lo') < 0) { + this.ifs.push(ifc); + } + } + } + this.gtop = new GTop.glibtop_netload(); + this.last = [0, 0, 0, 0, 0]; + this.usage = [0, 0, 0, 0, 0]; + this.last_time = 0; + this.tip_format([_('KiB/s'), '/s', _('KiB/s'), '/s', '/s']); + this.update_units(); + Schema.connect('changed::' + this.elt + '-speed-in-bits', this.update_units.bind(this)); + try { + let iface_list = this.client.get_devices(); + this.NMsigID = []; + for (let j = 0; j < iface_list.length; j++) { + this.NMsigID[j] = iface_list[j].connect('state-changed', this.update_iface_list.bind(this)); + } + } catch (e) { + global.logError('Please install Network Manager Gobject Introspection Bindings: ' + e); + } + this.update(); + } + update_units() { + this.speed_in_bits = Schema.get_boolean(this.elt + '-speed-in-bits'); + } + update_iface_list() { + try { + this.ifs = []; + let iface_list = this.client.get_devices(); + for (let j = 0; j < iface_list.length; j++) { + if (iface_list[j].state === NetworkManager.DeviceState.ACTIVATED) { + this.ifs.push(iface_list[j].get_ip_iface() || iface_list[j].get_iface()); + } + } + } catch (e) { + global.logError('Please install Network Manager Gobject Introspection Bindings'); + } + } + refresh() { + let accum = [0, 0, 0, 0, 0]; + + for (let ifn in this.ifs) { + GTop.glibtop_get_netload(this.gtop, this.ifs[ifn]); + accum[0] += this.gtop.bytes_in; + accum[1] += this.gtop.errors_in; + accum[2] += this.gtop.bytes_out; + accum[3] += this.gtop.errors_out; + accum[4] += this.gtop.collisions; + } + + let time = GLib.get_monotonic_time() * 0.001024; + let delta = time - this.last_time; + if (delta > 0) { + for (let i = 0; i < 5; i++) { + this.usage[i] = Math.round((accum[i] - this.last[i]) / delta); + this.last[i] = accum[i]; + this.vals[i] = this.usage[i]; + } + } + this.last_time = time; + } + + // pad a string with leading spaces + _pad(number, length) { + var str = '' + number; + while (str.length < length) { + str = ' ' + str; + } + return str; + } + + _apply() { + this.tip_vals = this.usage; + if (this.speed_in_bits) { + this.tip_vals[0] = Math.round(this.tip_vals[0] * 8.192); + this.tip_vals[2] = Math.round(this.tip_vals[2] * 8.192); + if (this.tip_vals[0] < 1000) { + this.text_items[2].text = Style.netunits_kbits(); + this.menu_items[1].text = this.tip_unit_labels[0].text = _('kbit/s'); + } else if (this.tip_vals[0] < 1000000) { + this.text_items[2].text = Style.netunits_mbits(); + this.menu_items[1].text = this.tip_unit_labels[0].text = _('Mbit/s'); + this.tip_vals[0] = (this.tip_vals[0] / 1000).toPrecision(3); + } else { + this.text_items[2].text = Style.netunits_gbits(); + this.menu_items[1].text = this.tip_unit_labels[0].text = _('Gbit/s'); + this.tip_vals[0] = (this.tip_vals[0] / 1000000).toPrecision(3); + } + if (this.tip_vals[2] < 1000) { + this.text_items[5].text = Style.netunits_kbits(); + this.menu_items[4].text = this.tip_unit_labels[2].text = _('kbit/s'); + } else if (this.tip_vals[2] < 1000000) { + this.text_items[5].text = Style.netunits_mbits(); + this.menu_items[4].text = this.tip_unit_labels[2].text = _('Mbit/s'); + this.tip_vals[2] = (this.tip_vals[2] / 1000).toPrecision(3); + } else { + this.text_items[5].text = Style.netunits_gbits(); + this.menu_items[4].text = this.tip_unit_labels[2].text = _('Gbit/s'); + this.tip_vals[2] = (this.tip_vals[2] / 1000000).toPrecision(3); + } + } else { + if (this.tip_vals[0] < 1024) { + this.text_items[2].text = Style.netunits_kbytes(); + this.menu_items[1].text = this.tip_unit_labels[0].text = _('KiB/s'); + } else if (this.tip_vals[0] < 1048576) { + this.text_items[2].text = Style.netunits_mbytes(); + this.menu_items[1].text = this.tip_unit_labels[0].text = _('MiB/s'); + this.tip_vals[0] = (this.tip_vals[0] / 1024).toPrecision(3); + } else { + this.text_items[2].text = Style.netunits_gbytes(); + this.menu_items[1].text = this.tip_unit_labels[0].text = _('GiB/s'); + this.tip_vals[0] = (this.tip_vals[0] / 1048576).toPrecision(3); + } + if (this.tip_vals[2] < 1024) { + this.text_items[5].text = Style.netunits_kbytes(); + this.menu_items[4].text = this.tip_unit_labels[2].text = _('KiB/s'); + } else if (this.tip_vals[2] < 1048576) { + this.text_items[5].text = Style.netunits_mbytes(); + this.menu_items[4].text = this.tip_unit_labels[2].text = _('MiB/s'); + this.tip_vals[2] = (this.tip_vals[2] / 1024).toPrecision(3); + } else { + this.text_items[5].text = Style.netunits_gbytes(); + this.menu_items[4].text = this.tip_unit_labels[2].text = _('GiB/s'); + this.tip_vals[2] = (this.tip_vals[2] / 1048576).toPrecision(3); + } + } + + if (Style.get('') !== '-compact') { + this.menu_items[0].text = this.text_items[1].text = this.tip_vals[0].toString(); + this.menu_items[3].text = this.text_items[4].text = this.tip_vals[2].toString(); + } else { + this.menu_items[0].text = this.text_items[1].text = this._pad(this.tip_vals[0].toString(), 4); + this.menu_items[3].text = this.text_items[4].text = this._pad(this.tip_vals[2].toString(), 4); + } + } + create_text_items() { + return [ + new St.Icon({ + icon_size: 2 * IconSize / 3 * Style.iconsize(), + icon_name: 'go-down-symbolic'}), + new St.Label({ + text: '', + style_class: Style.get('sm-net-value'), + y_align: Clutter.ActorAlign.CENTER}), + new St.Label({ + text: _('KiB/s'), + style_class: Style.get('sm-net-unit-label'), + y_align: Clutter.ActorAlign.CENTER}), + new St.Icon({ + icon_size: 2 * IconSize / 3 * Style.iconsize(), + icon_name: 'go-up-symbolic'}), + new St.Label({ + text: '', + style_class: Style.get('sm-net-value'), + y_align: Clutter.ActorAlign.CENTER}), + new St.Label({ + text: _('KiB/s'), + style_class: Style.get('sm-net-unit-label'), + y_align: Clutter.ActorAlign.CENTER}) + ]; + } + create_menu_items() { + return [ + new St.Label({ + text: '', + style_class: Style.get('sm-value')}), + new St.Label({ + text: _('KiB/s'), + style_class: Style.get('sm-label')}), + new St.Label({ + text: _(' ↓'), + style_class: Style.get('sm-label')}), + new St.Label({ + text: '', + style_class: Style.get('sm-value')}), + new St.Label({ + text: _(' KiB/s'), + style_class: Style.get('sm-label')}), + new St.Label({ + text: _(' ↑'), + style_class: Style.get('sm-label')}) + ]; + } +} + +const Swap = class SystemMonitor_Swap extends ElementBase { + constructor() { + super({ + elt: 'swap', + item_name: _('Swap'), + color_name: ['used'] + }); + this.max = 1; + this.gtop = new GTop.glibtop_swap(); + + GTop.glibtop_get_swap(this.gtop); + this.total = Math.round(this.gtop.total / 1024 / 1024); + let threshold = 4 * 1024; // In MiB + this.useGiB = false; + this._unitConversion = 1024 * 1024; + this._decimals = 100; + if (this.total > threshold) { + this.useGiB = true; + this._unitConversion *= 1024 / this._decimals; + } + + this.tip_format(); + this.update(); + } + refresh() { + GTop.glibtop_get_swap(this.gtop); + if (this.useGiB) { + this.swap = Math.round(this.gtop.used / this._unitConversion); + this.swap /= this._decimals; + this.total = Math.round(this.gtop.total / this._unitConversion); + this.total /= this._decimals; + } else { + this.swap = Math.round(this.gtop.used / this._unitConversion); + this.total = Math.round(this.gtop.total / this._unitConversion); + } + } + _pad(number) { + if (this.useGiB) { + if (number < 1) { + // examples: 0.01, 0.10, 0.88 + return number.toLocaleString(Locale, {minimumFractionDigits: 2, maximumFractionDigits: 2}); + } + // examples: 5.85, 16.0, 128 + return number.toLocaleString(Locale, {minimumSignificantDigits: 3, maximumSignificantDigits: 3}); + } + + return number.toLocaleString(Locale); + } + _apply() { + if (this.total === 0) { + this.vals = this.tip_vals = [0]; + } else { + this.vals[0] = this.swap / this.total; + this.tip_vals[0] = Math.round(this.vals[0] * 100); + } + this.text_items[0].text = this.tip_vals[0].toString(); + this.menu_items[0].text = this.tip_vals[0].toString(); + if (Style.get('') !== '-compact') { + this.menu_items[3].text = this._pad(this.swap) + + ' / ' + this._pad(this.total); + } else { + this.menu_items[3].text = this._pad(this.swap) + + '/' + this._pad(this.total); + } + } + + create_text_items() { + return [ + new St.Label({ + text: '', + style_class: Style.get('sm-status-value'), + y_align: Clutter.ActorAlign.CENTER}), + new St.Label({ + text: '%', + style_class: Style.get('sm-perc-label'), + y_align: Clutter.ActorAlign.CENTER}) + ]; + } + create_menu_items() { + let unit = 'MiB'; + if (this.useGiB) { + unit = 'GiB'; + } + return [ + new St.Label({ + text: '', + style_class: Style.get('sm-value')}), + new St.Label({ + text: '%', + style_class: Style.get('sm-label')}), + new St.Label({ + text: '', + style_class: Style.get('sm-label')}), + new St.Label({ + text: '', + style_class: Style.get('sm-value')}), + new St.Label({ + text: _(unit), + style_class: Style.get('sm-label')}) + ]; + } +} + +const Thermal = class SystemMonitor_Thermal extends ElementBase { + constructor() { + super({ + elt: 'thermal', + item_name: _('Thermal'), + color_name: ['tz0'] + }); + this.max = 100; + + this.item_name = _('Thermal'); + this.temperature = '-- '; + this.fahrenheit_unit = Schema.get_boolean(this.elt + '-fahrenheit-unit'); + this.display_error = true; + this.tip_format(this.temperature_symbol()); + Schema.connect('changed::' + this.elt + '-sensor-file', this.refresh.bind(this)); + this.update(); + } + refresh() { + let sfile = Schema.get_string(this.elt + '-sensor-file'); + if (GLib.file_test(sfile, GLib.FileTest.EXISTS)) { + let file = Gio.file_new_for_path(sfile); + file.load_contents_async(null, (source, result) => { + let as_r = source.load_contents_finish(result) + this.temperature = Math.round(parseInt(parse_bytearray(as_r[1])) / 1000); + }); + } else if (this.display_error) { + global.logError('error reading: ' + sfile); + this.display_error = false; + } + + this.fahrenheit_unit = Schema.get_boolean(this.elt + '-fahrenheit-unit'); + } + _apply() { + this.text_items[0].text = this.menu_items[0].text = this.temperature_text(); + this.temp_over_threshold = this.temperature > Schema.get_int('thermal-threshold'); + this.vals = [this.temperature]; + this.tip_vals[0] = this.temperature_text(); + this.text_items[1].text = this.menu_items[1].text = this.temperature_symbol(); + this.tip_unit_labels[0].text = _(this.temperature_symbol()); + } + create_text_items() { + return [ + new St.Label({ + text: '', + style_class: Style.get('sm-status-value'), + y_align: Clutter.ActorAlign.CENTER}), + new St.Label({ + text: this.temperature_symbol(), + style_class: Style.get('sm-temp-label'), + y_align: Clutter.ActorAlign.CENTER}) + ]; + } + create_menu_items() { + return [ + new St.Label({ + text: '', + style_class: Style.get('sm-value')}), + new St.Label({ + text: this.temperature_symbol(), + style_class: Style.get('sm-label')}) + ]; + } + temperature_text() { + let temperature = this.temperature; + if (this.fahrenheit_unit) { + temperature = Math.round(temperature * 1.8 + 32); + } + return temperature.toString(); + } + temperature_symbol() { + return this.fahrenheit_unit ? '°F' : '°C'; + } +} + +const Fan = class SystemMonitor_Fan extends ElementBase { + constructor() { + super({ + elt: 'fan', + item_name: _('Fan'), + color_name: ['fan0'] + }); + this.rpm = 0; + this.display_error = true; + this.tip_format(_('rpm')); + Schema.connect('changed::' + this.elt + '-sensor-file', this.refresh.bind(this)); + this.update(); + } + refresh() { + let sfile = Schema.get_string(this.elt + '-sensor-file'); + if (GLib.file_test(sfile, GLib.FileTest.EXISTS)) { + let file = Gio.file_new_for_path(sfile); + file.load_contents_async(null, (source, result) => { + let as_r = source.load_contents_finish(result) + this.rpm = parseInt(parse_bytearray(as_r[1])); + }); + } else if (this.display_error) { + global.logError('error reading: ' + sfile); + this.display_error = false; + } + } + _apply() { + this.text_items[0].text = this.rpm.toString(); + this.menu_items[0].text = this.rpm.toString(); + this.vals = [this.rpm / 10]; + this.tip_vals[0] = this.rpm; + } + create_text_items() { + return [ + new St.Label({ + text: '', + style_class: Style.get('sm-status-value'), + y_align: Clutter.ActorAlign.CENTER}), + new St.Label({ + text: _('rpm'), style_class: Style.get('sm-unit-label'), + y_align: Clutter.ActorAlign.CENTER}) + ]; + } + create_menu_items() { + return [ + new St.Label({ + text: '', + style_class: Style.get('sm-value')}), + new St.Label({ + text: _('rpm'), + style_class: Style.get('sm-label')}) + ]; + } +} + +const Gpu = class SystemMonitor_Gpu extends ElementBase { + constructor() { + super({ + elt: 'gpu', + item_name: _('GPU'), + color_name: ['used', 'memory'] + }); + this.max = 100; + + this.item_name = _('GPU'); + this.mem = 0; + this.total = 0; + this.tip_format(); + this.update(); + } + _unit(total) { + this.total = total; + let threshold = 4 * 1024; // In MiB + this.useGiB = false; + this._unitConversion = 1; + this._decimals = 100; + if (this.total > threshold) { + this.useGiB = true; + this._unitConversion *= 1024 / this._decimals; + } + } + refresh() { + // Run asynchronously, to avoid shell freeze + try { + let path = Me.dir.get_path(); + let script = ['/bin/bash', path + '/gpu_usage.sh']; + + // Create subprocess and capture STDOUT + let proc = new Gio.Subprocess({argv: script, flags: Gio.SubprocessFlags.STDOUT_PIPE}); + proc.init(null); + // Asynchronously call the output handler when script output is ready + proc.communicate_utf8_async(null, null, Lang.bind(this, this._handleOutput)); + } catch (err) { + global.logError(err.message); + } + } + _handleOutput(proc, result) { + let [ok, output, ] = proc.communicate_utf8_finish(result); + if (ok) { + this._readTemperature(output); + } else { + global.logError('gpu_usage.sh invocation failed'); + } + } + _sanitizeUsageValue(val) { + val = parseInt(val); + if (isNaN(val)) { + val = 0 + } + return val; + } + _readTemperature(procOutput) { + let usage = procOutput.split('\n'); + let memTotal = this._sanitizeUsageValue(usage[0]); + let memUsed = this._sanitizeUsageValue(usage[1]); + this.percentage = this._sanitizeUsageValue(usage[2]); + if (typeof this.useGiB === 'undefined') { + this._unit(memTotal); + this._update_unit(); + } + + if (this.useGiB) { + this.mem = Math.round(memUsed / this._unitConversion); + this.mem /= this._decimals; + this.total = Math.round(memTotal / this._unitConversion); + this.total /= this._decimals; + } else { + this.mem = Math.round(memUsed / this._unitConversion); + this.total = Math.round(memTotal / this._unitConversion); + } + } + _pad(number) { + if (this.useGiB) { + if (number < 1) { + // examples: 0.01, 0.10, 0.88 + return number.toFixed(2); + } + // examples: 5.85, 16.0, 128 + return number.toPrecision(3); + } + + return number; + } + _update_unit() { + let unit = _('MiB'); + if (this.useGiB) { + unit = _('GiB'); + } + this.menu_items[4].text = unit; + } + _apply() { + this.tip_unit_labels[1].text = "/ " + this.total + " " + this.menu_items[4].text; + if (this.total === 0) { + this.vals = [0, 0]; + this.tip_vals = [0, 0]; + } else { + // we subtract percentage from memory because we do not want memory to be + // "accumulated" in the chart with utilization; these two measures should be + // independent + this.vals = [this.percentage, this.mem / this.total * 100 - this.percentage]; + this.tip_vals = [Math.round(this.vals[0]), this.mem]; + } + this.text_items[0].text = this.tip_vals[0].toString(); + this.menu_items[0].text = this.tip_vals[0].toLocaleString(Locale); + + if (Style.get('') !== '-compact') { + this.menu_items[3].text = this._pad(this.mem).toLocaleString(Locale) + + ' / ' + this._pad(this.total).toLocaleString(Locale); + } else { + this.menu_items[3].text = this._pad(this.mem).toLocaleString(Locale) + + '/' + this._pad(this.total).toLocaleString(Locale); + } + } + create_text_items() { + return [ + new St.Label({ + text: '', + style_class: Style.get('sm-status-value'), + y_align: Clutter.ActorAlign.CENTER}), + new St.Label({ + text: '%', + style_class: Style.get('sm-perc-label'), + y_align: Clutter.ActorAlign.CENTER}) + ]; + } + create_menu_items() { + let unit = _('MiB'); + if (this.useGiB) { + unit = _('GiB'); + } + return [ + new St.Label({ + text: '', + style_class: Style.get('sm-value')}), + new St.Label({ + text: '%', + style_class: Style.get('sm-label')}), + new St.Label({ + text: '', + style_class: Style.get('sm-label')}), + new St.Label({ + text: '', + style_class: Style.get('sm-value')}), + new St.Label({ + text: unit, + style_class: Style.get('sm-label')}) + ]; + } +} + +const Icon = class SystemMonitor_Icon { + constructor() { + this.actor = new St.Icon({ + icon_name: 'org.gnome.SystemMonitor-symbolic', + style_class: 'system-status-icon' + }); + this.actor.visible = Schema.get_boolean('icon-display'); + Schema.connect( + 'changed::icon-display', + () => { + this.actor.visible = Schema.get_boolean('icon-display'); + } + ); + } +} + +function init() { + log('[System monitor] applet init from ' + extension.path); + + Convenience.initTranslations(); + // Get locale, needed as an argument for toLocaleString() since GNOME Shell 3.24 + // See: mozjs library bug https://bugzilla.mozilla.org/show_bug.cgi?id=999003 + Locale = GLib.get_language_names()[0]; + if (Locale.indexOf('_') !== -1) { + Locale = Locale.split('_')[0]; + } + + IconSize = Math.round(Panel.PANEL_ICON_SIZE * 4 / 5); +} + +function enable() { + log('[System monitor] applet enabling'); + Schema = Convenience.getSettings(); + + Style = new smStyleManager(); + MountsMonitor = new smMountsMonitor(); + + Background = color_from_string(Schema.get_string('background')); + + if (!(smDepsGtop && smDepsNM)) { + Main.__sm = { + smdialog: new smDialog() + }; + + let dialog_timeout = Mainloop.timeout_add_seconds( + 1, + () => { + Main.__sm.smdialog.open(); + Mainloop.source_remove(dialog_timeout); + return true; + }); + } else { + let panel = Main.panel._rightBox; + StatusArea = Main.panel._statusArea; + if (typeof (StatusArea) === 'undefined') { + StatusArea = Main.panel.statusArea; + } + if (Schema.get_boolean('center-display')) { + panel = Main.panel._centerBox; + } + + MountsMonitor.connect(); + + // Debug + Main.__sm = { + tray: new PanelMenu.Button(0.5), + icon: new Icon(), + pie: new Pie(), + bar: new Bar(), + elts: [], + }; + + // Items to Monitor + Main.__sm.elts = createCpus(); + Main.__sm.elts.push(new Freq()); + Main.__sm.elts.push(new Mem()); + Main.__sm.elts.push(new Swap()); + Main.__sm.elts.push(new Net()); + Main.__sm.elts.push(new Disk()); + Main.__sm.elts.push(new Gpu()); + Main.__sm.elts.push(new Thermal()); + Main.__sm.elts.push(new Fan()); + Main.__sm.elts.push(new Battery()); + + let tray = Main.__sm.tray; + let elts = Main.__sm.elts; + + if (Schema.get_boolean('move-clock')) { + let dateMenu = Main.panel.statusArea.dateMenu; + Main.panel._centerBox.remove_actor(dateMenu.container); + Main.panel._addToPanelBox('dateMenu', dateMenu, -1, Main.panel._rightBox); + tray.clockMoved = true; + } + + Schema.connect('changed::background', (schema, key) => { + Background = color_from_string(Schema.get_string(key)); + }); + Main.panel._addToPanelBox('system-monitor', tray, 1, panel); + + // The spacing adds a distance between the graphs/text on the top bar + let spacing = Schema.get_boolean('compact-display') ? '1' : '4'; + let box = new St.BoxLayout({style: 'spacing: ' + spacing + 'px;'}); + if (shell_Version < '3.36') { + tray.actor.add_actor(box); + } else { + tray.add_actor(box); + } + box.add_actor(Main.__sm.icon.actor); + // Add items to panel box + for (let elt in elts) { + box.add_actor(elts[elt].actor); + } + + // Build Menu Info Box Table + let menu_info = new PopupMenu.PopupBaseMenuItem({reactive: false}); + let menu_info_box = new St.BoxLayout(); + menu_info.actor.add(menu_info_box); + Main.__sm.tray.menu.addMenuItem(menu_info, 0); + + build_menu_info(); + + tray.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + + let pie_item = Main.__sm.pie; + pie_item.create_menu_item(); + tray.menu.addMenuItem(pie_item.menu_item); + + let bar_item = Main.__sm.bar; + bar_item.create_menu_item(); + tray.menu.addMenuItem(bar_item.menu_item); + + change_usage(); + Schema.connect('changed::disk-usage-style', change_usage); + + tray.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + + tray.menu.connect( + 'open-state-changed', + function (menu, isOpen) { + if (isOpen) { + Main.__sm.pie.actor.queue_repaint(); + + menu_timeout = Mainloop.timeout_add_seconds( + 5, + () => { + Main.__sm.pie.actor.queue_repaint(); + return true; + }); + } else { + Mainloop.source_remove(menu_timeout); + } + } + ); + + let _appSys = Shell.AppSystem.get_default(); + let _gsmApp = _appSys.lookup_app('gnome-system-monitor.desktop'); + let _gsmPrefs = _appSys.lookup_app('gnome-shell-extension-prefs.desktop'); + if (_gsmPrefs === null) { + _gsmPrefs = _appSys.lookup_app('org.gnome.Extensions.desktop'); + } + let item; + item = new PopupMenu.PopupMenuItem(_('System Monitor...')); + item.connect('activate', () => { + _gsmApp.activate(); + }); + tray.menu.addMenuItem(item); + + item = new PopupMenu.PopupMenuItem(_('Preferences...')); + item.connect('activate', () => { + if (typeof ExtensionUtils.openPrefs === 'function') { + ExtensionUtils.openPrefs(); + } else if (_gsmPrefs.get_state() === _gsmPrefs.SHELL_APP_STATE_RUNNING) { + _gsmPrefs.activate(); + } else { + let info = _gsmPrefs.get_app_info(); + let timestamp = global.display.get_current_time_roundtrip(); + info.launch_uris([metadata.uuid], global.create_app_launch_context(timestamp, -1)); + } + }); + tray.menu.addMenuItem(item); + Main.panel.menuManager.addMenu(tray.menu); + } + log('[System monitor] applet enabling done'); +} + +function disable() { + // restore clock + if (Main.__sm.tray.clockMoved) { + let dateMenu = Main.panel.statusArea.dateMenu; + Main.panel._rightBox.remove_actor(dateMenu.container); + Main.panel._addToPanelBox('dateMenu', dateMenu, Main.sessionMode.panel.center.indexOf('dateMenu'), Main.panel._centerBox); + } + // restore system power icon if necessary + // workaround bug introduced by multiple cpus init : + // if (Schema.get_boolean('battery-hidesystem') && Main.__sm.elts.battery.icon_hidden) { + // Main.__sm.elts.battery.hide_system_icon(false); + // } + // for (let i in Main.__sm.elts) { + // if (Main.__sm.elts[i].elt == 'battery') + // Main.__sm.elts[i].hide_system_icon(false); + // } + + if (MountsMonitor) { + MountsMonitor.disconnect(); + MountsMonitor = null; + } + + if (Style) { + Style = null; + } + + Schema.run_dispose(); + for (let eltName in Main.__sm.elts) { + Main.__sm.elts[eltName].destroy(); + } + if (shell_Version < '3.36') { + Main.__sm.tray.actor.destroy(); + } else { + Main.__sm.tray.destroy(); + } + Main.__sm = null; + + log('[System monitor] applet disable'); +} diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/gpu_usage.sh b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/gpu_usage.sh new file mode 100755 index 0000000..f2167ef --- /dev/null +++ b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/gpu_usage.sh @@ -0,0 +1,49 @@ +#!/bin/sh +################################################################################## +# This file is part of System Monitor Gnome extension. +# System Monitor Gnome extension is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# System Monitor Gnome extension is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# You should have received a copy of the GNU General Public License +# along with System Monitor. If not, see <http://www.gnu.org/licenses/>. +# Copyright 2017 Fran Glais, David King, indigohedgehog@github. +################################################################################## + +################################## +# # +# Check for GPU memory usage # +# # +################################## + +checkcommand() +{ + command -v "$1" > /dev/null 2>&1 +} + +# This will print three lines. The first one is the the total vRAM available, +# the second one is the used vRAM and the third on is the GPU usage in %. +if checkcommand nvidia-smi; then + nvidia-smi -i 0 --query-gpu=memory.total,memory.used,utilization.gpu --format=csv,noheader,nounits | while IFS=', ' read -r a b c; do echo "$a"; echo "$b"; echo "$c"; done + +elif lsmod | grep amdgpu > /dev/null; then + total=$(cat /sys/class/drm/card0/device/mem_info_vram_total) + echo $(($total / 1024 / 1024)) + + used=$(cat /sys/class/drm/card0/device/mem_info_vram_used) + echo $(($used / 1024 / 1024)) + + cat /sys/class/drm/card0/device/gpu_busy_percent + +elif checkcommand glxinfo; then + TOTALVRAM=$(glxinfo | grep -A2 -i GL_NVX_gpu_memory_info | grep -E -i "dedicated" | cut -f2- -d ':' | gawk '{print $1}') + AVAILVRAM=$(glxinfo | grep -A4 -i GL_NVX_gpu_memory_info | grep -E -i "available dedicated" | cut -f2- -d ':' | gawk '{print $1}') + FREEVRAM=$((TOTALVRAM-AVAILVRAM)) + echo "$TOTALVRAM" + echo "$FREEVRAM" + +fi diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/ar/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/ar/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..ed116bec6f2f51cc55fba4df14b8d19e576aeafb GIT binary patch literal 4303 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/ca/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/ca/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..ab48c309bd3df15426613223aeff36029f2fada8 GIT binary patch literal 4243 zcmZveYiu1y700JErNx03TKY)oV?zo~Xs?sBr40^g6FYWd5+}xwB$Na+-W}h&_1@i^ z-MzL`T2aUcKmtBgAR!Q<0wGl(Bxq18+I&EDsVbya;#Gl!A^{(O76b?+9s<hmzdP5D ziqSpu+c`6P=FFM1$G&johR++?DdYxZ!^OsY6#n57I@<3pHRdAtd-!Jf3gl=0P4{}Z z;WA@*#cYIn<q9bM)vyoV0Iz`kQ0wl78{lpzJA2_9;X!yEoPlqGDZCm!2sgt=px*l~ zl-=(^*?ZROpNAV6zX0XuFQEK*8OpD}z}LaQd;Pzl-oNM#_53C9a>g5>^w&e}caxvr z0yi<<2DNS&%Ku5I_}mAz{v_0SEY$v~A9o=?^KrVX;HTmB@KLDsPeZ-`Jk)zHK*i&y z@GbC<Q2V|LZ-JK+1nqwZ)VX#;j%4nEio*dYdsCh#p!_@q6^{=?ozp?tiJ{h?hT4At zYTZMSpE*mX^`G_nN1)b!!Sl<IpZO}C)_u$CzXN6G2`Ibgq5NHjTKAmi525V6==l?< z_kQl@FF^VEYd`<8*Z;|n{|aUQA5iOFh1%y*f}yzdLD|2`^ExQ|H^P9xY=Mf;elA0C z9)+?q1K$cyLHV0QerAD=t1xH%_%rZY#$SY5_c+vkPeA$gG}L$S47?Hk7;62Wq4M=I zl70)^1hwxTsCyrQ9K{@mEH$%G`IEu7!Lxq;D^Tb82GqH~4R!8wQ2w0v^WTT^<2gTm z3Cho3`th%zzPsN-`Sl+tJC_p_#phZm|29FLV>8tG?uD{D3i+91bUL4d{qP~E^<RfN z&oY#q7rp)^cn#yfL7i_S#%_f-L$1agg4*XelwTk6`V7kdkNEkAq5S*=)IOi``p-e_ z_a)D-LD~H#R9v6*d=@HRKY}{<&!FsIfO`KokdT;Hy#Ak_|Ml}1<DB;02z3wFK-t>_ zt9*m9x5ewXdi`ylw?pl_3u?c8Q16dHoqycxr=k2m>GdtBb7WBVAN2Z<LHYAZsJK)% z;Z|e^q8wA))ehA#?}D=3@Z&Kk+iD*~-hteUY(b`xJCPAYd854uMb(s_1IS6_0CEe` zkGu!D6}b`_MdWX<4fzMvKDE8beuT6zyAj2H3fYO=j%-8ThxFPm9u#-A6E(~S;JAO* z-?;mb_age6r*=<`Dz7&C=eyt?$Z<q@tQw$p6nQ5yjO;;lS9c?-<p&Y{jkp<6dv}d0 zpKtQdGoHdEas<)6P9U#sgI*Cp<&N6+8s;{5*gp%(5jE8$eP?QKN8XPd(_qjH*>Jv@ zXWcXkhO;EgQIw_2&zPa^+?>nJ@WhN6wqeWBr=d#>tCF~gv$Wu`-LYXA?2n61V$Yc2 zPS@}}udY&f24i8C_LRX`8pR87)U^pa=B`+om@3aCZe<jwLD_P_cpOD*w54lzTyD#5 z?gCbf+E(t;mWxZXKU-uEI+y2JUYJpvn$g^yHV1OsX$8mQsBD>oapZ!jGX-X9r^d2H z%gcx2q3wkkA2Q>vopJc_n8#X|Ex5{&Fv-GsbHtVANE|wI6z6g?kvLn(lcKaq5+v<L z;nG6ePvnm9IYL$p3=A;Lnz?OryOYjFbaR}%6->u%XQrxG%v38|BsjsiOS|=8+AWp- ziFLuGYuh-D(`KzpUAdU$^Fh&ZE>bXN<Fcry$}CIDxMQXkEnRh7k6=7Y<1)*Gk)@Jt zxyIK_$XOqtcD>Y0uZej{yo*+rM8Vb_L74?nT-e#fZ8Ot^qiqv2)8Xf@V5YE5!d+eK zOyMF!m-F!)&*RdWS(_tuvdv~(&TO4=VU=y+YMO<lRTMY5CU%=QkuF-D-(`-ACYq-W z0q(R+T;J&YkZHTNAxI|GeWh9I47=b!Cw8Xe?<3c3<ej!D6sn>Ql~(C2-3~v$OIQBW z<z}(+o(sL-g=J$bY8-R90y!8o)2!_pMav~gqc+v>F>KgQCvj!pr0ZmP*%&XHanu;< zHjBn|HW+klZli2zX=&gAmuGvMTyruIX6?R-qm4<oP<?HU{kC+2LEm9pl#S`!rbS}8 z$H8DE%(E1A+UyeT#<a8TYFRxV3_{yCJ~lqG>gCRX9eu-D%JpODOb4SSdA6MoHcod3 zVaw))EBDMyk2ZF#uG5C(b7Le8vxpQN40e%jeJ2_R9QROrGLg9`E`yx|cMaUxM`@|* zNf5atm{YZ&u(&+7`oxwW=Yf<((O!NmLa8#dG7;KtU2T5waKcoUJo99)<Q$6&3P)T% zV8eL%TtJXif$I0EHlZ2MiZW-D3PHjy)Si{GONt=vhFD|wUKuLPNKwWKo(9BvjrpDx zDpg3da@SK}cC`@Mz^yApLpENrku@V#9g@{quQ`U1IN_@EsxmE~Uw$kNIsCph!H;Ye zs%F0Y=v*9{qhU8G?aBeFxgu8K!ZM$HZiA8SQ|k&>71ed6s~ZsfoOH~}rZXpOzGUm_ zwJ*_W##FY5vFom_T;#@F-0bF6gxAzA%b%9Rq&ze0+z+CDu3&B0d_nVfK=j%!U4E(; zJX})3tsAyG1%Bd9x}qn>3yzu_m$p<8887Joigs1$dRN@9n+z3IEt~5S(&clUjpPb# zx)7VGUfAlAN80p?pmMV}yH@7|t;7ExwK<%yrLDGGn+8-w?j_JY7DOaousL;crFdF1 z(Y=3tVsyNsPQU);rwU4>E$O;8kNNj$qzLo4Q-z#zsXvxf%<3!J*Pe}*&lP6vM?}G^ zzbt(~6wuYrMkVw}mv1d6wWy*YcIqs3RhITf3uI_rOb_@xbak#d{yh4UTHQijR6}Z} rtx~D9IbWf#s@5bvPgGee4@1*S-Zc?3{`|EMe$Z6_x^)2D>hb>vJs+5- literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/cs/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/cs/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..be4ab2dc5c9d3a84bb2eb9bbdba23b756e3d6523 GIT binary patch literal 3909 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 CEomPB literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/de/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/de/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..a14e9618d5ec6d2fc168e401b16c9b404579adde GIT binary patch literal 4208 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 KpE9Q(zW)GNSDdT> literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/es_ES/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/es_ES/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..7302234a684f42a41694e32d9477e6379c0ac2a8 GIT binary patch literal 4264 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/es_MX/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/es_MX/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..7f3d4b3ea9681331e662ef3c5e908660c00bc3d2 GIT binary patch literal 1962 zcmZXTJ#1V>5XT2FUkBkM;VTfx27-wZ?+Ah-axma8EJr!Tvd@l$X!hQld$;j>Z;yTZ zY@dZhLxn;EiXcc7p#veM0D-6|645{tI+_$nNNG}lghT`4KkxQzP*!jD=bf9KnVlJ5 z-8*)UuqGe}AjwTc$H6ae#)kFrEkyT$=fN@X3-EUEB6v6WEw~4~3_b|{42s<~5FcH~ zb{F_NcpLaPcq=#tqkF&xDE{_?a_<l}@jDLQ3BCx*y(#d1a1NAvYv5s!K}q#J5FfoC z_yH(!J__egLGklh(0?BE7X!Zz`tLxo`#$J@2>KsE$@5Ck{{o8LRZ!yp7WCHwuY(f* z4<Ty$7btoE1EuKq;PM^dK2Yj+0DJ&^7L<Cw3X0tlD0N&5zrO)W9u|a4+63<f-v-6s z*`PlMK8Eurp!oj=lzW#!>C;c(Uhr2?djBVgj}F2};*Nu2{~R{)`%=(Pfnqls&L;ww z0@pyXe-jit1|`od@U8IsMmWC%;-h!5iTznn;(r+Q=Rxsz0Yn+=oPm1DT*)0-4T!uK zX7zds`_qs|AW~0RPYzM<GGn79`8@%VI*&v4Lk>YkOZqD{dNe2`ztK8~KlVXnRuyEl zq}LBa<bP_kqz}(PWR9iRM<DXH!w~7oLy*TI(odSv@p|fPRU~TGX4cW1Dc5D|XL>NA zT8wS6b6D|%jCW3^P`<~iWs*d+cC(-bGeZkb_j~HJNqmo5oLe`bQ?PTi<haX@i<rx1 zvx%ckoz7{Qb%L$yxa_I6$(dGqc2l)^%hSrB^qdE%mMx68PA(6BQ29_pZO;4bw67c^ zC-G#nL>)P*yj6)QbtmHqTJ6(n$%$-^PCMh7Iu=QGsxI=PSjRmkSg%teZ$WbX9vQo- zji<uIOfZIBKjiHs?{b)6)u&2aV{^1wyT<d*aO|VSBwAxv8e2@N#xnP<^HHlzO%lyi zX&JTcr0Q#@6T7vw)!b%hUrTdsvSw`a#?q;1nKz7Fj^?yycsQv`AGMt>%1omclWJDG z%$91psTORptux+MFL&6Zqv>GR)hoDIq?JxNYO~I3N5eDzY%R9tcawafd9*QW3(o}t z4f+V`(Te9W6{a|@;+}RT`>ECT>(Q}YJ4pydjOL5jCZ<Ry)v=E84XP(gC9Jx(NUV%b zY-vdsUT2x6mMy*0P8kfDrTNmE4E<t!7(&Bo#XBZksGJjHX}Y7&*m}^Wi?~;tXIiE1 z(uJ;x;Zi2fXpEdgpwYw`{oj$(bdu4hMZD2%=wV15`$}hKTgT?oSH(_PdAg+)BV#D} z)&nRhmP3`y;*DBb%{xwKVuyQSGPTU3v|65raArfh37U4!bTnoX6M((a4sV<<V`z<| p>AYjh8wM05)Wa%_Uj6?m#mmExYRcrLAz9MHU{xKg3c+|3>|c`Sp=AI7 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/fa/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/fa/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..b1ccb175a4aba46d6d12739d32b5283c4e930124 GIT binary patch literal 2536 zcmbW0TWl0n7{`wSid8}Jf{K@e_Y1QWe2}a~+AbiQbZOHIpouZlozvZs?woaIwzoth zfnG3u&=7nv`hbzr2HBL>;4MvjkO!ZPPcyA(;tPrK$v2G;{{OS5-5P_5o}BOe<~x_~ zcFz2`Y3*r&wi9wc<otC)WWc617|;%^72-B93*G>}46XxhkXs5Mx;TpAMz9Rt0uF(< zf}`MF;3*JYOk=niya3YP7a;9?4Wf&0Fwp+D;0EwJkam9p(Zy8^wEru36ZpHCUxP!m zw;sG5+z9ggR`7bT5#;%24R?X)Vz<#VAkQB#^X(wx>M{CDAkV*Q=$LsI<T(v;|A65L z$o+4E=;AmA=5f-@ziap&$n)=mjPs0{|JZN_L>E;ICvi8Qg7-k*17qvKBOvQ*gPXuX zGygW&0R5C%{}5zd=E1wc%OLA`1>6Y!0&@Q!AnmThKyEPH4D!4Ld2XlS9+3Xq%z7_~ zE<y~n_Xc<$I00@3KQi-Qf;@NG=sy@<1$nQ(f#~8-4BWR4lgw`m$oZ7vb0FhsHhK<Z zJr9BO|B9I}8oq8g0`lB(!*|U52O!U#HJk<M|0|<^Z+Hd7btH{(JptKLQ^n(83i1?$ zKAwc||Ds(s3&J~)kk#!`DEv40j-G}*2*GtE`D_Cpg76NWfxvz8zw&Lc-3z%h`KtY| zcbIS1o{evDJA}FL9v*<OJp$o>x)s9rx7_$9*!b2OA^exv9)>&y*+B)3cagMKJAbs` z>x$>djPBRIXsz`1DPLsld{K!^xgz$u!BGz7etUSCGMm@liqaJh_p24n^<-F7GUqxD zXZ4^bUi9s9Q66@kuqbk>r2S!WP=(^4n^&S8n|;yYD|l62UInRC3aT#nc1d(9+riN1 ztDq>m-I5YrMLj6H)lew92JN!w8V*8LGLSj#xuN!Hp|&7PwHeV<7CnJ-IQZhQ?}keB zYDCjp%Ql~6n@6^eTW~{+$k}1&2%}sSt{${8U4knQfqM)b$6>2%BJ~+*z>tDkNR_gv z&?5$43??3t)oUsrT3N?Br2N3uURpMEs<QS&D;E@8$7-z<0;^l6W!d&^M-L4RrACym zn+qk|?N8-(siC9Y>Qn<R4_kZfP{Bf*9fVf5Z+k(%jqKC1#kWzeoUR1+fGe9ywT~1` z2F1Fm7jV!kRP2JXx|Ll@%MQk5d$K7swp6p*7gP!<?HB5s)3VQQ$Ypc;>N)I4H8y0l z7b*{N43`m8hzr_|2jhCr%Y4!H0~PM>>E3Vcs_&x@T)DN+%WKE=3Te5k*9{wvSRGn9 zZfGqBw-Q%<DTC)4`on3N?QA)?O=i2=T5{Qp?A+JU)7plQ_Wkv}UAp4uRi+k0T2{h7 z1R<i?cx<sM<ArGM=eH)Ki}C2vWIPs+i|AZ56(5UcMf6GX@mV~M4P1gcCZf5eN$Lv; zaArb8)uqX3ZgE;*(`YmWWq#GCjOJ^z@d+G`&Mi*MYwW8=vofB*o=Y%EXYmQBRS{i^ zC*l)w5$lX-RbV2Tr$Iy=k0p-hl3=(~L^Ifa9>E}uXof!oGDFfZG0HSAz&3nDRVIb> zsW37IFcn=~np~X&mS>XWv3xdONUDH+Gx4ZE1(THNRI)m?c{;qtl8Da43(L7pCEJg| zWi=j0tq8F=4SkmJGKdMx(tA8k4^>wDzu(d4C@Jo#ep!Dj@k(vYZ(hIqe=YH~3p)4D JLjBLI{{lZs1OWg5 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/fi/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/fi/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..7623fac7f532cd59c699ab0129a443e0956a7dea GIT binary patch literal 4250 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/fr/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/fr/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..e567368f1ccf0e8db209d3155027427ce983904b GIT binary patch literal 6292 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/hu/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/hu/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..5ffcee0fc9000cb660e612f6ce4dde28ec103b19 GIT binary patch literal 5927 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�DlODs-_TTPXS z#flS6Nh?`WyT#8=)|O>(i`Bt=5%~3hsie-1s<Y3;Vo%P+y+)2`r<Byasx#ECC(cAP zs9SFAR-;3Q4)u0S9Br=S%%HatHG9VPID5@hei}Pl-P8=~fxdx$r|)`aU_ke680^1s zbzgs9U(YT#Nu7z<4U@p-q#Yzu5<fexhuk1L+DY`yP0CSk*$v!IvbmPjdK>Lq=oQx6 z_`#VlZWHCY<w4zO_mp;&w#_NdY1%U!g#-rka1?PM5rJ#j-Cnp+R~l}dm~_+R#E5g# zTszT+NO!h{l}OI=L46Z;@43?%HF&5{bSyGeFVzFR>v{)BBkkA?oRKJQCWAU`<;7&v zz(!p>ZdwuQWt(*W%8fnmF-hKL0uu(Nt}BV_Yqv8mVLHA}v!l(Zjv{Hw+dN%v&yJ>P zHWMeE>{zI__<p<D#)cSGjm;&@(P=hvb}Y?~GeoRP*-Sk#DtjXJvu8WmOk#YsQ*x)D z>ZYk55GADb3@MdUXuFpk4W~Q!r<R?l*H0c!=x_X<oy>b6JAqzwXwf0T&Woo~hU%uM zxl}5ZHVd4QN>NJYHoiUji$qFBl@3KJB{tvZT$(3R);XK{{@+w8c~tfgNvQaOu&3O6 z$)q-oI&`0(GT)a(x_+G;uZE8yNvGWvzkiu@muZ>~+2@Qf+Tr8AGegU$ZS`r?Xu9>t z|F3sv_SZ?89ShotyI@>h#4qMj+fN>D%09?u{J<;>V>2(~mTA1p5Y%N?lcD!<M0j?` zkaztl`BZf0vae*}*sXc805;i7V3IuiNqk{7;rz?rm>@O>@}O1!I~`}oV@94!$Xhy^ zDp#{z8zz`G`A$^q=pZ}Zacy?SNZCx8%+=juaw?p9;M;Qv`qTqYa$H1W$5ag&x(Bj( zk0z0S>VaqG<?B7cl$%h`_XXd<#Rm7ZS3P-HWe>&7?wf$;1*UiLO_-e!vyFGMXQw!v z7CKT6Ar8W9X6oeOe7wb46pQk7V#<XH7dFbn(wBLL+T|!loh187ol|Gwbi*VqBNm(W z!R$lLBc@n{_B~VQxZqSwSyI<m-PdB5g>?%f$d6(PivzBFA4sSWCSuh_)LFTpts3Ef zNFGo%d2o|Xh!@;+E^NDXsWffa2HA1GAhJhdaY>YPf~ck3`3HEDzZAYnWdW_?EU%w= zj%O#j2e^_i3s!rydBD`CRbZOE+*HE`IYB)e<a}UsgPhNdJg^u4(rB=TUi+=#g_T4x zvV<bJC3R=NH==G()uOoCX3=Z;z^JH-_(<{9nC}tQIAeDzF;lKv{$S0KO7%S3WXE#L F{0Crl<G%m^ literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/it/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/it/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..8fb45925d6eab311c751b646a8f4fb4313980bca GIT binary patch literal 4330 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 ba|q;`@FC7+bXM0C8aibpI^9U*D{=TAn(Cp- literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/ja/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/ja/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..4e96e8eb633510d025cf92000087329069045e3c GIT binary patch literal 3639 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/ko/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/ko/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..94c2ce37cab8efddeb174a16aefbf6bedcaee016 GIT binary patch literal 5643 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/nl_NL/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/nl_NL/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..472482faafc9fd1598576c08c2950751855c6563 GIT binary patch literal 4335 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/pl/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/pl/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..9537a6e9c10a2419b35a16d98cadd747b8469027 GIT binary patch literal 2228 zcmZvbO^g&p6vv-}2!kJpiXVs;_ys}_v+D;qg0ed-n+ZGYva`z?4r*<8F;mP;S6f|U z?@aUpCd9;eF&<XDs22~$7`bvG%p8oygI5zy>Vd=<PsWq}-%JgQ36*~J>#DAL?_aN8 zU)#F=6GAJ%9)o>$8&Qeh*P%n(y`E?bxCh(-7Qs8f36PgegLvpTx;w!+@GkIm@NQ6n z4}vL(hdw~J8T<%jf1i4O4zj<mK=%JNcn|m;$bPSYc<6g{?Efcl6Zngt{|#h+*TMV1 zzrfqUb#R7P+TghvWdB?IcpJ$3w)^>AAnVxe$NNCuKkj+J&mRJL-z3QE%bpeg{Upe` zmq6bCre_55J`J+I#LuUmD<B?v58Ywh#d$D?@fB?1`5(di!Jk2n`#Z?~uY(-#Pmu5H zZ*VKP5t9#q+d<x606G6Jft=q-a3grqe?JYfzqkB%1>zxx4le0k@Cood$oKFi$a=o@ z-+Lg({Q=~7*Fg6FE68#G@bmwGtp8s>UWY~OXA{WtJ3#ic6J$Lvfb8#OkafHUvi@Tr z9$G}l-`@aveFVZaX^_{y4YL0gKmHKp^&f+HXcZmn`@+wE10wx{wio>~ut$eOdK%p4 z-#G`*!ubE;dm6QEc;P(V-1vXs7<|6nupKbg$ojZF4&&P01LNA>4r7hno`dl{a^w5u zvv7N4h{2fyyz{^I5RCt2Zo6QdCjsMn8#T@?w})YQ*u6YJ+X;ILwx0)RNCQ2|FOxEC z)U0X8ktms_u{7B}b4FPz$*``dl(gxnP8-}MP1zYyltUBW7*t)SscwvPEL>fQijE?l zHCas4Rwi|^q$5|SvdN@&R8cKsJ9I)hn#FQUb5<d)ibIv=^LY$S&B_+dD;c3XV^vxg z3%aGKS~r<kP%DnAnM|nKNu6qWh>D4|GnPFpCbXEUh&xM5Ry#$@1_G9c2Mh-X3~|6n z*B~h3V@9kNag#dfq0Sb3#}J8~aFcjdmQ|%3J{^W{C)DO(=qzP}U^u~96}q4t1*eov zwTX*jZlZL2VtPuPnwqbcXHVqjRbs3QDrrqe!DPFZ1`DPr5@}^*R#sN>U1iOYT1)C? zJ~XY|+-xwf-qGwUI4Yegit)nOSWq|+jE{-&=Zi0n?=6fK3b`4Xx?sV|IBiN~qbMfg zSc)ZSyDF?paY$mA&-`%B|Et&-*V?kCf(0d8kZYP6X+JbGbEK*B#@5zWKxH#mDOaY} z4mXxB<Vq%XDn{^50(B0zv>$b&<5vU%+f=#3iwnnsgKO(pB<?+!ibE6WxK<Pgm$l2$ zaV|#T`sXB9B<gZbwsdc`cR?g3vc1)?8_QvNc5rVK4cE!y4lQ;17c;F~Q|N|?d#l8y zDYDV5jJ3RJ4RX@IXid~Wkzc*kKSx#0cf<5A_AbzDqO1(hDv_!ES?5N0s%nL=<v3Pr zC3{xgT(YxK*zGtfb$hF+GpTFpY~WIuXrQitd7xv?G-UrWa@0s2Ty<6I(DmPk+?JL& zjIV58(qdR?ZH_d3^^%ozy4}Cr3ALg{&^1uHx<RL9qEj-<lIt94OKy=iE_&R)8>(vR zu)B<6r-(Bd4HA)c8u;PJ{67aHUz84Vpg9MlKf(+To!sIa{uikYM0ZD`nIqBNf$0AL D?VaQ! literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/pt/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/pt/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..b756a3d08775652cba7f7dd68f7676ca68982e06 GIT binary patch literal 4246 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 XW-|TbZd4$gzpG1Yy6Xpa;kf??!>5qB literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/pt_BR/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/pt_BR/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d79d270f4edb89f2539ee3a7245ee18b936fea8 GIT binary patch literal 4265 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 D#7U&D literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/ro/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/ro/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..c789c33ceb20503254d3376408727892c3a65ae2 GIT binary patch literal 2186 zcmZvbO^6&t6vv+?eq=PRCK_YnN2yVxYfm--B}`V$?j#!rc9PA`tSf#XwcRy6mF@0o zs%tW{>q(a!1o09)Xz&sQ5mfNvLGU2#VGn|M3V6stM7+d{2p&B6f76wn6;sr!U%%?A zSO2Pdb#>SF4+-T6>_ONUHxbpr$!%y*Ufxc0H`oAg1z!Vi0VT*oVi1*<(cA{+;GN(A z+zDO)?*lJ_sPs9SJHW3%j`ywS_aMjH08!}&G`!AF;9cMqkmLUbqS7B|c%47N9Uu?p z{tC!($H05QJ>bpYLH~c;a{@%A=e&Om<ax(^|0$65G`)Wo<oJu8Z}@%*^1K-2@tLOu zdHi`0mEK0f`rq;W?|FUz^8AY+>-^aFf8u!=M5WKr?8DuB3G#VtU=q)}f`<QJ1$q5n zL0<QFumWBK_ke$ceC}fqaWA+ZWPbu211CWYp;-`>T4*@l>mbK_3*`S1$m=hI9DmjK zUjXli|31j~@+rvkzV`pW0r@_@19{$$AS(Ur{a?I)4diwH^85$n{cXFw)UyL*e<#TC zcZ0l-y&%s!;5iPm{v)2xgFL?G|GxyXjv4Qt_5WY>{+s@P1VREC@Atj`E{N+GmfdKd zg6%I|dJ?R{o`&&R9E9=v;`O)$Fg_~*+gkX&@LpK=Gq8tYyf*KP%OkK|uyGjYJik{y z3og&X_?>a#bLKndvaiH&?}zZsxv&@ZknevS<okRS#`$->@SErI0Bi!rIl|>J*b}hB z?4aNn=m39A%Wyfirk6#cZW3c@y4UF_OLZA`71i?|ozTTHH+dpgu2VEZlWjUJx00w$ zPiMk)m1ycH;$G9w=#-UtS1jttb*W+cQafs@)Yug|tsI@!p`tUGV`<JREUvOp71e4L zu8FNoX<o?)O~<ODE803$)ashPXsdyvR$u1SS}B}LJw(%F+8N6c7IIoBRK$&?MXQ~n zB?A>p<u1eFE<@}x(lJy;yw`{&W8J(<x)hrtb_S&xE5&rg%Dptp(hAm4lp1<D^>{@z zmiogLv5j*obU`BuUQxEtCaZ}!GpQ0qS0#xkXNt6xEo7c2%2no7ZmbKMMXaM>x)&Eg z+tfrZt&GfIFsQC7YmUdM)JZioX=UzAFt5&QjvkzlPSr$ZRu(R3TbUJ!#Cg=js!Yv5 zOb$_v&6epeVsIAsWUPX=lId`A>EpB6Xr3G?KU|%t)J^78hTtnX7IL_ULr4#u9TQ<! z+CsS(7TPnx(a|^-ftw9ZW}%667T3hlCG9Fy?<GcBC1#L-GPz;$g*4UC8y_jTy1usM zQ_Shm4bubNZJmqZD0yz{kVJ|)WLhM#D#G<O8)7Y*U0>U{gd`G3%Z?8JlaX4DQgSMB z3K?AHq|P=jDbA>=Jkg;<08Mr6G~&%EtBs7-*TlREhh2g%fTpZ;=xl43^n9A?VVe5< z6s@7lK^xhox*5E}0;#y*bfQ=J-l!E?jaNZ3U1CB1%iU1pWpM6>6m8ijQkb`Qqa2py Z*5qg}rEs6grA$2%)$yWwBT<DH^)K4Szjgos literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/ru/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/ru/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..3bca914426764397f742ecc07814aa1e6ae8075c GIT binary patch literal 4672 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/sk/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/sk/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..c362e0ee30cb4ebaca1fc0547f0fc22c6cdbe42f GIT binary patch literal 4331 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/tr/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/tr/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..1d4ff15543fef1b527fcc3facef4ce66f4cf1023 GIT binary patch literal 4412 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 G)_(vh!Uz)p literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/uk/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/uk/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..9169a1f63fbb8b546dd648f121daac4d2430472d GIT binary patch literal 4835 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/zh_CN/LC_MESSAGES/system-monitor.mo b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/locale/zh_CN/LC_MESSAGES/system-monitor.mo new file mode 100644 index 0000000000000000000000000000000000000000..09f9162115ac40d767d420797b1d310d0489f005 GIT binary patch literal 5603 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 DaK|n% literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/metadata.json b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/metadata.json new file mode 100644 index 0000000..44e737a --- /dev/null +++ b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/metadata.json @@ -0,0 +1,19 @@ +{ + "_generated": "Generated by SweetTooth, do not edit", + "description": "Display system information in GNOME Shell status bar, such as memory, CPU, disk and battery usages, network rates\u2026", + "gettext-domain": "system-monitor", + "name": "system-monitor", + "settings-schema": "org.gnome.shell.extensions.system-monitor", + "shell-version": [ + "3.26", + "3.28", + "3.30", + "3.34", + "3.32", + "3.36", + "40" + ], + "url": "https://github.com/paradoxxxzero/gnome-shell-system-monitor-applet", + "uuid": "system-monitor@paradoxxx.zero.gmail.com", + "version": 40 +} \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/prefs.js b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/prefs.js new file mode 100644 index 0000000..388f512 --- /dev/null +++ b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/prefs.js @@ -0,0 +1,553 @@ +const Gtk = imports.gi.Gtk; +const Gio = imports.gi.Gio; +const Gdk = imports.gi.Gdk; +const GLib = imports.gi.GLib; +const ByteArray = imports.byteArray; +const Config = imports.misc.config; + +const Gettext = imports.gettext.domain('system-monitor'); + +let extension = imports.misc.extensionUtils.getCurrentExtension(); +let convenience = extension.imports.convenience; + +const _ = Gettext.gettext; +const N_ = function (e) { + return e; +}; + +let Schema; + +const shellMajorVersion = parseInt(Config.PACKAGE_VERSION.split('.')[0]); + +function init() { + convenience.initTranslations(); + Schema = convenience.getSettings(); +} + +String.prototype.capitalize = function () { + return this.replace(/(^|\s)([a-z])/g, function (m, p1, p2) { + return p1 + p2.toUpperCase(); + }); +}; + +function color_to_hex(color) { + var output = N_('#%02x%02x%02x%02x').format( + 255 * color.red, + 255 * color.green, + 255 * color.blue, + 255 * color.alpha); + return output; +} + +function parse_bytearray(bytearray) { + if (!ByteArray.toString(bytearray).match(/GjsModule byteArray/)) { + return ByteArray.toString(bytearray); + } + return bytearray +} + +function check_sensors(sensor_type) { + const hwmon_path = '/sys/class/hwmon/'; + const hwmon_dir = Gio.file_new_for_path(hwmon_path); + + const sensor_files = []; + const sensor_labels = []; + + function get_label_from(file) { + if (file.query_exists(null)) { + // load_contents (and even cat) fails with "Invalid argument" for some label files + try { + let [success, contents] = file.load_contents(null); + if (success) { + return String(parse_bytearray(contents)).split('\n')[0]; + } + } catch (e) { + log('[System monitor] error loading label from file ' + file.get_path() + ': ' + e); + } + } + return null; + } + + function add_sensors_from(chip_dir, chip_label) { + const chip_children = chip_dir.enumerate_children( + 'standard::name,standard::type', Gio.FileQueryInfoFlags.NONE, null); + if (!chip_children) { + log('[System monitor] error enumerating children of chip ' + chip_dir.get_path()); + return false; + } + + const input_entry_regex = new RegExp('^' + sensor_type + '(\\d+)_input$'); + let info; + let added = false; + while ((info = chip_children.next_file(null))) { + if (info.get_file_type() !== Gio.FileType.REGULAR) { + continue; + } + const matches = info.get_name().match(input_entry_regex); + if (!matches) { + continue; + } + const input_ordinal = matches[1]; + const input = chip_children.get_child(info); + const input_label = get_label_from(chip_dir.get_child(sensor_type + input_ordinal + '_label')); + + sensor_files.push(input.get_path()); + sensor_labels.push(chip_label + ' - ' + (input_label || input_ordinal)); + added = true; + } + return added; + } + + const hwmon_children = hwmon_dir.enumerate_children( + 'standard::name,standard::type', Gio.FileQueryInfoFlags.NONE, null); + if (!hwmon_children) { + log('[System monitor] error enumerating hwmon children'); + return [[], []]; + } + + let info; + while ((info = hwmon_children.next_file(null))) { + if (info.get_file_type() !== Gio.FileType.DIRECTORY || !info.get_name().match(/^hwmon\d+$/)) { + continue; + } + const chip = hwmon_children.get_child(info); + const chip_label = get_label_from(chip.get_child('name')) || chip.get_basename(); + + if (!add_sensors_from(chip, chip_label)) { + // This is here to provide compatibility with previous code, but I can't find any + // information about sensors being stored in chip/device directory. Can we delete it? + const chip_device = chip.get_child('device'); + if (chip_device.query_exists(null)) { + add_sensors_from(chip_device, chip_label); + } + } + } + return [sensor_files, sensor_labels]; +} + +/** + * @param args.hasBorder Whether the box has a border (true) or not + * @param args.horizontal Whether the box is horizontal (true) + * or vertical (false) + * @param args.shouldPack Determines whether a horizontal box should have + * uniform spacing for its children. Only applies to horizontal boxes + * @param args.spacing The amount of spacing for a given box + * @returns a new Box with settings specified by args + */ +function box(args = {}) { + const options = { }; + + if (typeof args.spacing !== 'undefined') { + options.spacing = args.spacing; + } + + if (shellMajorVersion < 40) { + if (args.hasBorder) { + options.border_width = 10; + } + + return args.horizontal ? + new Gtk.HBox(options) : new Gtk.VBox(options); + } + + if (args.hasBorder) { + options.margin_top = 10; + options.margin_bottom = 10; + options.margin_start = 10; + options.margin_end = 10; + } + + options.orientation = args.horizontal ? + Gtk.Orientation.HORIZONTAL : Gtk.Orientation.VERTICAL; + + const aliasBox = new Gtk.Box(options); + + if (args.shouldPack) { + aliasBox.set_homogeneous(true); + } + + + aliasBox.add = aliasBox.append; + aliasBox.pack_start = aliasBox.prepend; + // normally, this would be append; it is aliased to prepend because + // that appears to yield the same behavior as version < 40 + aliasBox.pack_end = aliasBox.prepend; + + return aliasBox; +} + +const ColorSelect = class SystemMonitor_ColorSelect { + constructor(name) { + this.label = new Gtk.Label({label: name + _(':')}); + this.picker = new Gtk.ColorButton(); + this.actor = box({horizontal: true, spacing: 5}); + this.actor.add(this.label); + this.actor.add(this.picker); + this.picker.set_use_alpha(true); + } + set_value(value) { + let color = new Gdk.RGBA(); + + if (Gtk.get_major_version() >= 4) { + // GDK did not support parsing hex colours with alpha before GTK 4. + color.parse(value); + } else { + // Use the Compat only when GTK 4 is not available, + // since it depends on the deprecated Clutter library. + let Compat = extension.imports.compat; + let clutterColor = Compat.color_from_string(value); + let ctemp = [clutterColor.red, clutterColor.green, clutterColor.blue, clutterColor.alpha / 255]; + color.parse('rgba(' + ctemp.join(',') + ')'); + } + + this.picker.set_rgba(color); + } +} + +const IntSelect = class SystemMonitor_IntSelect { + constructor(name) { + this.label = new Gtk.Label({label: name + _(':')}); + this.spin = new Gtk.SpinButton(); + this.actor = box({horizontal: true, shouldPack: true, }); + this.actor.add(this.label); + this.actor.add(this.spin); + this.spin.set_numeric(true); + } + set_args(minv, maxv, incre, page) { + this.spin.set_range(minv, maxv); + this.spin.set_increments(incre, page); + } + set_value(value) { + this.spin.set_value(value); + } +} + +const Select = class SystemMonitor_Select { + constructor(name) { + this.label = new Gtk.Label({label: name + _(':')}); + // this.label.set_justify(Gtk.Justification.RIGHT); + this.selector = new Gtk.ComboBoxText(); + this.actor = box({horizontal: true, shouldPack: true, spacing: 5}); + this.actor.add(this.label); + this.actor.add(this.selector); + } + set_value(value) { + this.selector.set_active(value); + } + add(items) { + items.forEach((item) => { + this.selector.append_text(item); + }) + } +} + +function set_enum(combo, schema, name) { + Schema.set_enum(name, combo.get_active()); +} + +function set_color(color, schema, name) { + Schema.set_string(name, color_to_hex(color.get_rgba())) +} + +function set_string(combo, schema, name, _slist) { + Schema.set_string(name, _slist[combo.get_active()]); +} + +const SettingFrame = class SystemMonitor { + constructor(name, schema) { + this.schema = schema; + this.label = new Gtk.Label({label: name}); + + this.vbox = box({horizontal: false, shouldPack: true, spacing: 20}); + this.hbox0 = box({horizontal: true, shouldPack: true, spacing: 20}); + this.hbox1 = box({horizontal: true, shouldPack: true, spacing: 20}); + this.hbox2 = box({horizontal: true, shouldPack: true, spacing: 20}); + this.hbox3 = box({horizontal: true, shouldPack: true, spacing: 20}); + + if (shellMajorVersion < 40) { + this.frame = new Gtk.Frame({border_width: 10}); + this.frame.add(this.vbox); + } else { + this.frame = new Gtk.Frame({ + margin_top: 10, + margin_bottom: 10, + margin_start: 10, + margin_end: 10 + }); + this.frame.set_child(this.vbox); + } + + + if (shellMajorVersion < 40) { + this.vbox.pack_start(this.hbox0, true, false, 0); + this.vbox.pack_start(this.hbox1, true, false, 0); + this.vbox.pack_start(this.hbox2, true, false, 0); + this.vbox.pack_start(this.hbox3, true, false, 0); + } else { + this.vbox.append(this.hbox0); + this.vbox.append(this.hbox1); + this.vbox.append(this.hbox2); + this.vbox.append(this.hbox3); + } + } + + /** Enforces child ordering of first 2 boxes by label */ + _reorder() { + if (shellMajorVersion < 40) { + /** @return {string} label of/inside component */ + const labelOf = el => { + if (el.get_children) { + return labelOf(el.get_children()[0]); + } + return el && el.label || ''; + }; + [this.hbox0, this.hbox1].forEach(hbox => { + hbox.get_children() + .sort((c1, c2) => labelOf(c1).localeCompare(labelOf(c2))) + .forEach((child, index) => hbox.reorder_child(child, index)); + }); + } else { + /** @return {string} label of/inside component */ + const labelOf = el => { + if (el.get_label) { + return el.get_label(); + } + return labelOf(el.get_first_child()); + } + + [this.hbox0, this.hbox1].forEach(hbox => { + const children = []; + let next = hbox.get_first_child(); + + while (next !== null) { + children.push(next); + next = next.get_next_sibling(); + } + + const sorted = children + .sort((c1, c2) => labelOf(c1).localeCompare(labelOf(c2))); + + sorted + .forEach((child, index) => { + hbox.reorder_child_after(child, sorted[index - 1] || null); + }); + }); + } + } + + add(key) { + const configParent = key.substring(0, key.indexOf('-')); + const config = key.substring(configParent.length + 1); + + // hbox0 + if (config === 'display') { + let item = new Gtk.CheckButton({label: _('Display')}); + this.hbox0.add(item); + Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT); + } else if (config === 'show-text') { + let item = new Gtk.CheckButton({label: _('Show Text')}); + this.hbox0.add(item); + Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT); + } else if (config === 'show-menu') { + let item = new Gtk.CheckButton({label: _('Show In Menu')}); + this.hbox0.add(item); + Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT); + // hbox1 + } else if (config === 'refresh-time') { + let item = new IntSelect(_('Refresh Time')); + item.set_args(50, 100000, 1000, 5000); + this.hbox1.add(item.actor); + Schema.bind(key, item.spin, 'value', Gio.SettingsBindFlags.DEFAULT); + } else if (config === 'graph-width') { + let item = new IntSelect(_('Graph Width')); + item.set_args(1, 1000, 1, 10); + this.hbox1.add(item.actor); + Schema.bind(key, item.spin, 'value', Gio.SettingsBindFlags.DEFAULT); + } else if (config === 'style') { + let item = new Select(_('Display Style')); + item.add([_('digit'), _('graph'), _('both')]); + item.set_value(this.schema.get_enum(key)); + this.hbox1.add(item.actor); + item.selector.connect('changed', function (style) { + set_enum(style, Schema, key); + }); + // Schema.bind(key, item.selector, 'active', Gio.SettingsBindFlags.DEFAULT); + // hbox2 + } else if (config.match(/-color$/)) { + let item = new ColorSelect(_(config.split('-')[0].capitalize())); + item.set_value(this.schema.get_string(key)); + if (shellMajorVersion < 40) { + this.hbox2.pack_end(item.actor, true, false, 0); + } else { + this.hbox2.append(item.actor); + } + item.picker.connect('color-set', function (color) { + set_color(color, Schema, key); + }); + } else if (config.match(/sensor/)) { + let sensor_type = configParent === 'fan' ? 'fan' : 'temp'; + let [_slist, _strlist] = check_sensors(sensor_type); + let item = new Select(_('Sensor')); + if (_slist.length === 0) { + item.add([_('Please install lm-sensors')]); + } else if (_slist.length === 1) { + this.schema.set_string(key, _slist[0]); + } + item.add(_strlist); + try { + item.set_value(_slist.indexOf(this.schema.get_string(key))); + } catch (e) { + item.set_value(0); + } + // this.hbox3.add(item.actor); + if (configParent === 'fan') { + if (shellMajorVersion < 40) { + this.hbox2.pack_end(item.actor, true, false, 0); + } else { + this.hbox2.append(item.actor); + } + } else if (shellMajorVersion < 40) { + this.hbox2.pack_start(item.actor, true, false, 0); + } else { + this.hbox2.prepend(item.actor); + } + item.selector.connect('changed', function (combo) { + set_string(combo, Schema, key, _slist); + }); + // hbox3 + } else if (config === 'speed-in-bits') { + let item = new Gtk.CheckButton({label: _('Show network speed in bits')}); + this.hbox3.add(item); + Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT); + } else if (config === 'individual-cores') { + let item = new Gtk.CheckButton({label: _('Display Individual Cores')}); + this.hbox3.add(item); + Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT); + } else if (config === 'time') { + let item = new Gtk.CheckButton({label: _('Show Time Remaining')}); + this.hbox3.add(item); + Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT); + } else if (config === 'hidesystem') { + let item = new Gtk.CheckButton({label: _('Hide System Icon')}); + this.hbox3.add(item); + Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT); + } else if (config === 'usage-style') { + let item = new Select(_('Usage Style')); + item.add([_('pie'), _('bar'), _('none')]); + item.set_value(this.schema.get_enum(key)); + if (shellMajorVersion < 40) { + this.hbox3.pack_end(item.actor, false, false, 20); + } else { + this.hbox3.append(item.actor); + } + + item.selector.connect('changed', function (style) { + set_enum(style, Schema, key); + }); + } else if (config === 'fahrenheit-unit') { + let item = new Gtk.CheckButton({label: _('Display temperature in Fahrenheit')}); + this.hbox3.add(item); + Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT); + } else if (config === 'threshold') { + let item = new IntSelect(_('Temperature threshold (0 to disable)')); + item.set_args(0, 300, 5, 5); + this.hbox3.add(item.actor); + Schema.bind(key, item.spin, 'value', Gio.SettingsBindFlags.DEFAULT); + } + if (configParent.indexOf('gpu') !== -1 && + config === 'display') { + let item = new Gtk.Label({label: _('** Only Nvidia GPUs supported so far **')}); + this.hbox3.add(item); + } + this._reorder(); + } +} + +const App = class SystemMonitor_App { + constructor() { + let setting_items = ['cpu', 'memory', 'swap', 'net', 'disk', 'gpu', 'thermal', 'fan', 'freq', 'battery']; + let keys = Schema.list_keys(); + + this.items = []; + this.settings = []; + + setting_items.forEach((setting) => { + this.settings[setting] = new SettingFrame(_(setting.capitalize()), Schema); + }); + + this.main_vbox = box({ + hasBorder: true, horizontal: false, spacing: 10}); + this.hbox1 = box({ + hasBorder: true, horizontal: true, shouldPack: true, spacing: 20}); + if (shellMajorVersion < 40) { + this.main_vbox.pack_start(this.hbox1, false, false, 0); + } else { + this.main_vbox.prepend(this.hbox1); + } + + keys.forEach((key) => { + if (key === 'icon-display') { + let item = new Gtk.CheckButton({label: _('Display Icon')}); + this.items.push(item) + this.hbox1.add(item) + Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT); + } else if (key === 'center-display') { + let item = new Gtk.CheckButton({label: _('Display in the Middle')}) + this.items.push(item) + this.hbox1.add(item) + Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT); + } else if (key === 'compact-display') { + let item = new Gtk.CheckButton({label: _('Compact Display')}) + this.items.push(item) + this.hbox1.add(item) + Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT); + } else if (key === 'show-tooltip') { + let item = new Gtk.CheckButton({label: _('Show tooltip')}) + item.set_active(Schema.get_boolean(key)) + this.items.push(item) + this.hbox1.add(item) + Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT); + } else if (key === 'move-clock') { + let item = new Gtk.CheckButton({label: _('Move the clock')}) + this.items.push(item) + this.hbox1.add(item) + Schema.bind(key, item, 'active', Gio.SettingsBindFlags.DEFAULT); + } else if (key === 'background') { + let item = new ColorSelect(_('Background Color')) + item.set_value(Schema.get_string(key)) + this.items.push(item) + if (shellMajorVersion < 40) { + this.hbox1.pack_start(item.actor, true, false, 0) + } else { + this.hbox1.prepend(item.actor) + } + item.picker.connect('color-set', function (color) { + set_color(color, Schema, key); + }); + } else { + let sections = key.split('-'); + if (setting_items.indexOf(sections[0]) >= 0) { + this.settings[sections[0]].add(key); + } + } + }); + this.notebook = new Gtk.Notebook() + setting_items.forEach((setting) => { + this.notebook.append_page(this.settings[setting].frame, this.settings[setting].label) + if (shellMajorVersion < 40) { + this.main_vbox.show_all(); + this.main_vbox.pack_start(this.notebook, true, true, 0) + } else { + this.main_vbox.append(this.notebook); + } + }); + if (shellMajorVersion < 40) { + this.main_vbox.show_all(); + } + } +} + +function buildPrefsWidget() { + let widget = new App(); + return widget.main_vbox; +} diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/schemas/gschemas.compiled b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/schemas/gschemas.compiled new file mode 100644 index 0000000000000000000000000000000000000000..aeef9ca57543183abf31dfab0a75494bfe60f792 GIT binary patch literal 6516 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 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/schemas/org.gnome.shell.extensions.system-monitor.gschema.xml b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/schemas/org.gnome.shell.extensions.system-monitor.gschema.xml new file mode 100644 index 0000000..6247263 --- /dev/null +++ b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/schemas/org.gnome.shell.extensions.system-monitor.gschema.xml @@ -0,0 +1,463 @@ +<schemalist gettext-domain="system-monitor"> + <enum id="org.gnome.shell.extensions.system-monitor.display-style"> + <value value="0" nick="digit"/> + <value value="1" nick="graph"/> + <value value="2" nick="both"/> + </enum> + <enum id="org.gnome.shell.extensions.system-monitor.disk-usage-style"> + <value value="0" nick="pie"/> + <value value="1" nick="bar"/> + <value value="2" nick="none"/> + </enum> + <schema id="org.gnome.shell.extensions.system-monitor" path="/org/gnome/shell/extensions/system-monitor/"> + <key name="icon-display" type="b"> + <default>true</default> + <summary>Display system monitor icon</summary> + <description>Set to true to display system monitor icon in status bar.(NOTICE: The icon will be shown when none of the others is shown.)</description> + </key> + <key name="memory-display" type="b"> + <default>true</default> + <summary>Display memory</summary> + <description>Set to false to remove memory display in status bar</description> + </key> + <key name="memory-refresh-time" type="i"> + <default>5000</default> + <summary>Memory refresh time</summary> + <description>Time in ms between 2 refresh of memory</description> + </key> + <key name="memory-graph-width" type="i"> + <default>100</default> + <summary>Memory graph width</summary> + <description>Graph width in pixel</description> + </key> + <key name="memory-show-text" type="b"> + <default>true</default> + <summary>Display 'mem'</summary> + <description>Set to true to show 'mem' before memory display</description> + </key> + <key name="memory-show-menu" type="b"> + <default>true</default> + <summary>Display Memory In Menu</summary> + <description>Set to true to show memory in pop-up menu</description> + </key> + <key name="memory-style" enum="org.gnome.shell.extensions.system-monitor.display-style"> + <default>'graph'</default> + <summary>Choose the display style.</summary> + </key> + <key name="swap-display" type="b"> + <default>false</default> + <summary>Display swap</summary> + <description>Set to false to remove swap display in status bar</description> + </key> + <key name="swap-refresh-time" type="i"> + <default>5000</default> + <summary>Swap refresh time</summary> + <description>Time in ms between 2 refresh of swap</description> + </key> + <key name="swap-graph-width" type="i"> + <default>100</default> + <summary>Swap graph width</summary> + <description>Graph width in pixel</description> + </key> + <key name="swap-show-text" type="b"> + <default>true</default> + <summary>Display 'swap'</summary> + <description>Set to true to show 'swap' before swap display</description> + </key> + <key name="swap-show-menu" type="b"> + <default>true</default> + <summary>Display Swap In Menu</summary> + <description>Set to true to show swap in pop-up menu</description> + </key> + <key name="swap-style" enum="org.gnome.shell.extensions.system-monitor.display-style"> + <default>'graph'</default> + <summary>Choose the display style.</summary> + </key> + <key name="cpu-display" type="b"> + <default>true</default> + <summary>Display cpu</summary> + <description>Set to false to remove cpu display in status bar</description> + </key> + <key name="cpu-refresh-time" type="i"> + <default>1500</default> + <summary>Cpu refresh time</summary> + <description>Time in ms between 2 refresh of cpu</description> + </key> + <key name="cpu-graph-width" type="i"> + <default>100</default> + <summary>Cpu graph width</summary> + <description>Graph width in pixel</description> + </key> + <key name="cpu-show-text" type="b"> + <default>true</default> + <summary>Display 'cpu'</summary> + <description>Set to true to show 'cpu' before cpu display</description> + </key> + <key name="cpu-show-menu" type="b"> + <default>true</default> + <summary>Display Cpu In Menu</summary> + <description>Set to true to show cpu in pop-up menu</description> + </key> + <key name="cpu-style" enum="org.gnome.shell.extensions.system-monitor.display-style"> + <default>'graph'</default> + <summary>Choose the display style.</summary> + </key> + <key name="cpu-individual-cores" type="b"> + <default>false</default> + <summary>Display one graph per cpu core</summary> + <description>Set to true to display one graph per cpu core</description> + </key> + <key name="gpu-display" type="b"> + <default>false</default> + <summary>Display GPU usage</summary> + <description>Set to false to remove GPU display in status bar</description> + </key> + <key name="gpu-refresh-time" type="i"> + <default>5000</default> + <summary>Memory refresh time</summary> + <description>Time in ms between 2 refreshes of GPU usage</description> + </key> + <key name="gpu-graph-width" type="i"> + <default>100</default> + <summary>GPU usage graph width</summary> + <description>Graph width in pixel</description> + </key> + <key name="gpu-show-text" type="b"> + <default>true</default> + <summary>Display 'gpu'</summary> + <description>Set to true to show 'gpu' before GPU display</description> + </key> + <key name="gpu-show-menu" type="b"> + <default>false</default> + <summary>Display GPU In Menu</summary> + <description>Set to true to show GPU in pop-up menu</description> + </key> + <key name="gpu-style" enum="org.gnome.shell.extensions.system-monitor.display-style"> + <default>'graph'</default> + <summary>Choose the display style.</summary> + </key> + <key name="freq-display" type="b"> + <default>false</default> + <summary>Display freq</summary> + <description>Set to false to remove freq display in status bar</description> + </key> + <key name="freq-refresh-time" type="i"> + <default>1500</default> + <summary>Cpu frequency refresh time</summary> + <description>Time in ms between 2 refresh of cpu</description> + </key> + <key name="freq-graph-width" type="i"> + <default>100</default> + <summary>Cpu frequency graph width</summary> + <description>Graph width in pixel</description> + </key> + <key name="freq-show-text" type="b"> + <default>false</default> + <summary>Display freq'</summary> + <description>Set to true to show 'freq' before cpu frequency display</description> + </key> + <key name="freq-show-menu" type="b"> + <default>false</default> + <summary>Display Freq In Menu</summary> + <description>Set to true to show freq in pop-up menu</description> + </key> + <key name="freq-style" enum="org.gnome.shell.extensions.system-monitor.display-style"> + <default>'graph'</default> + <summary>Choose the display style.</summary> + </key> + <key name="net-display" type="b"> + <default>true</default> + <summary>Display net</summary> + <description>Set to false to remove net display in status bar</description> + </key> + <key name="net-refresh-time" type="i"> + <default>1000</default> + <summary>Net refresh time</summary> + <description>Time in ms between 2 refresh of net</description> + </key> + <key name="net-graph-width" type="i"> + <default>100</default> + <summary>Net graph width</summary> + <description>Graph width in pixel</description> + </key> + <key name="net-show-text" type="b"> + <default>true</default> + <summary>Display 'net'</summary> + <description>Set to true to show 'net' before net display</description> + </key> + <key name="net-show-menu" type="b"> + <default>true</default> + <summary>Display Net In Menu</summary> + <description>Set to true to show net in pop-up menu</description> + </key> + <key name="net-style" enum="org.gnome.shell.extensions.system-monitor.display-style"> + <default>'graph'</default> + <summary>Choose the display style.</summary> + </key> + <key name="net-speed-in-bits" type="b"> + <default>false</default> + <summary>Show network speed in bits/sec</summary> + </key> + <key name="disk-display" type="b"> + <default>false</default> + <summary>Display disk io speed</summary> + <description>Set to false to remove disk display in status bar</description> + </key> + <key name="disk-refresh-time" type="i"> + <default>2000</default> + <summary>Disk IO refresh time</summary> + <description>Time in ms between 2 refresh of Disk IO</description> + </key> + <key name="disk-graph-width" type="i"> + <default>100</default> + <summary>Disk IO graph width</summary> + <description>Graph width in pixel</description> + </key> + <key name="disk-show-text" type="b"> + <default>true</default> + <summary>Display 'disk'</summary> + <description>Set to true to show 'disk' before disk io display</description> + </key> + <key name="disk-show-menu" type="b"> + <default>true</default> + <summary>Display Disk In Menu</summary> + <description>Set to true to show disk in pop-up menu</description> + </key> + <key name="disk-style" enum="org.gnome.shell.extensions.system-monitor.display-style"> + <default>'graph'</default> + <summary>Choose the display style.</summary> + </key> + <key name="disk-usage-style" enum="org.gnome.shell.extensions.system-monitor.disk-usage-style"> + <default>'pie'</default> + <summary>Choose the disk usage display style.</summary> + </key> + <key name="thermal-display" type="b"> + <default>false</default> + <summary>Display thermal</summary> + <description>Set to false to remove thermal display in status bar</description> + </key> + <key name="thermal-refresh-time" type="i"> + <default>5000</default> + <summary>thermal refresh time</summary> + <description>Time in ms between 2 refresh of thermal</description> + </key> + <key name="thermal-graph-width" type="i"> + <default>100</default> + <summary>thermal graph width</summary> + <description>Graph width in pixel</description> + </key> + <key name="thermal-show-text" type="b"> + <default>true</default> + <summary>Display 'thermal'</summary> + <description>Set to true to show 'thermal' before thermal display</description> + </key> + <key name="thermal-show-menu" type="b"> + <default>true</default> + <summary>Display Temps In Menu</summary> + <description>Set to true to show thermal in pop-up menu</description> + </key> + <key name="thermal-style" enum="org.gnome.shell.extensions.system-monitor.display-style"> + <default>'graph'</default> + <summary>Choose the display style.</summary> + </key> + <key name="thermal-sensor-file" type="s"> + <default>'/sys/devices/virtual/thermal/thermal_zone0/temp'</default> + <summary>Sensor File</summary> + <description>Location of the sensor file for cpu temp</description> + </key> + <key name="thermal-threshold" type="i"> + <default>0</default> + <summary>Thermal threshold</summary> + <description>When the temprature passes the threshold, the text is set to red as an alert.</description> + </key> + <key name="fan-display" type="b"> + <default>false</default> + <summary>Display fan</summary> + <description>Set to false to remove fan display in status bar</description> + </key> + <key name="fan-refresh-time" type="i"> + <default>5000</default> + <summary>fan refresh time</summary> + <description>Time in ms between 2 refresh of fan</description> + </key> + <key name="fan-graph-width" type="i"> + <default>100</default> + <summary>fan graph width</summary> + <description>Graph width in pixel</description> + </key> + <key name="fan-show-text" type="b"> + <default>true</default> + <summary>Display 'fan'</summary> + <description>Set to true to show 'fan' before fan display</description> + </key> + <key name="fan-show-menu" type="b"> + <default>true</default> + <summary>Display Fans In Menu</summary> + <description>Set to true to show fan in pop-up menu</description> + </key> + <key name="fan-style" enum="org.gnome.shell.extensions.system-monitor.display-style"> + <default>'graph'</default> + <summary>Choose the display style.</summary> + </key> + <key name="fan-sensor-file" type="s"> + <default>'/sys/devices/virtual/thermal/cooling_device0/cur_state'</default> + <summary>Sensor File</summary> + <description>Location of the sensor file for fan</description> + </key> + <key name="fan-fan0-color" type="s"> + <default>'#f2002e'</default> + <summary>Color of fan in the chart</summary> + </key> + <key name="center-display" type="b"> + <default>false</default> + <summary>Display informations next to the clock</summary> + <description>True: Set information at the center, False: Set information in status bar (at top right)</description> + </key> + <key name="move-clock" type="b"> + <default>false</default> + <summary>Move the clock to the right when center-display is true</summary> + <description>True: Move the clock to the right, False: Keep it in center</description> + </key> + <key name="show-tooltip" type="b"> + <default>false</default> + <summary>Enable or disable the tooltip</summary> + <description>True: show tool tip on mouse hover </description> + </key> + <key name="compact-display" type="b"> + <default>false</default> + <summary>Optimize view for small displays</summary> + <description>Optimize texts sizes to fit on a small display </description> + </key> + <key name="memory-program-color" type="s"> + <default>'#00b35b'</default> + <summary>Color of program memory in the chart</summary> + </key> + <key name="memory-buffer-color" type="s"> + <default>'#00ff82'</default> + <summary>Color of buffer memory in the chart</summary> + </key> + <key name="memory-cache-color" type="s"> + <default>'#aaf5d0'</default> + <summary>Color of cache memory in the chart</summary> + </key> + <key name="net-down-color" type="s"> + <default>'#fce94f'</default> + <summary>Color of download speed in the chart</summary> + </key> + <key name="net-up-color" type="s"> + <default>'#fb74fb'</default> + <summary>Color of upload speed in the chart</summary> + </key> + <key name="net-downerrors-color" type="s"> + <default>'#ff6e00'</default> + <summary>Color of download errors</summary> + </key> + <key name="net-uperrors-color" type="s"> + <default>'#e0006e'</default> + <summary>Color of upload errors</summary> + </key> + <key name="net-collisions-color" type="s"> + <default>'#ff0000'</default> + <summary>Color of collisions</summary> + </key> + <key name="cpu-user-color" type="s"> + <default>'#0072b3'</default> + <summary>Color of user cpu in the chart</summary> + </key> + <key name="cpu-system-color" type="s"> + <default>'#0092e6'</default> + <summary>Color of system cpu in the chart</summary> + </key> + <key name="cpu-nice-color" type="s"> + <default>'#00a3ff'</default> + <summary>Color of nice cpu in the chart</summary> + </key> + <key name="cpu-iowait-color" type="s"> + <default>'#002f3d'</default> + <summary>Color of iowait in the chart</summary> + </key> + <key name="cpu-other-color" type="s"> + <default>'#001d26'</default> + <summary>Color of other cpu in the chart</summary> + </key> + <key name="freq-freq-color" type="s"> + <default>'#001d26'</default> + <summary>Color of freq in the chart</summary> + </key> + <key name="swap-used-color" type="s"> + <default>'#8b00c3'</default> + <summary>Color of used swap in the chart</summary> + </key> + <key name="disk-read-color" type="s"> + <default>'#c65000'</default> + <summary>Color of disk reading speed in the chart</summary> + </key> + <key name="disk-write-color" type="s"> + <default>'#ff6700'</default> + <summary>Color of disk writing speed in the chart</summary> + </key> + <key name="gpu-used-color" type="s"> + <default>'#00b35b'</default> + <summary>Color of program GPU usage in the chart</summary> + </key> + <key name="gpu-memory-color" type="s"> + <default>'#00ff82'</default> + <summary>Color of program GPU memory in the chart</summary> + </key> + <key name="thermal-tz0-color" type="s"> + <default>'#f2002e'</default> + <summary>Color of user thermal in the chart</summary> + </key> + <key name="thermal-fahrenheit-unit" type="b"> + <default>false</default> + <summary>Display temperature in Fahrenheit</summary> + <description>Set to true to show temperature in Fahrenheit</description> + </key> + <key name="background" type="s"> + <default>'#ffffff16'</default> + <summary>Color of background</summary> + </key> + <key name="battery-display" type="b"> + <default>false</default> + <summary>Display battery</summary> + <description>Set to false to remove battery display in status bar</description> + </key> + <key name="battery-refresh-time" type="i"> + <default>5000</default> + <summary>thermal refresh time</summary> + <description>Time in ms between 2 refresh of thermal</description> + </key> + <key name="battery-graph-width" type="i"> + <default>100</default> + <summary>thermal graph width</summary> + <description>Graph width in pixel</description> + </key> + <key name="battery-show-text" type="b"> + <default>true</default> + <summary>Display 'batt'</summary> + <description>Set to true to show 'batt' before net display</description> + </key> + <key name="battery-show-menu" type="b"> + <default>false</default> + <summary>Display 'batt'</summary> + <description>Set to true to show battery in pop-up menu</description> + </key> + <key name="battery-style" enum="org.gnome.shell.extensions.system-monitor.display-style"> + <default>'digit'</default> + <summary>Choose the display style.</summary> + </key> + <key name="battery-batt0-color" type="s"> + <default>'#f2002e'</default> + <summary>Color of battery</summary> + </key> + <key name="battery-time" type="b"> + <default>false</default> + <summary>Display battery time remaining rather than percentage</summary> + </key> + <key name="battery-hidesystem" type="b"> + <default>false</default> + <summary>Hide system battery icon</summary> + </key> + + </schema> +</schemalist> diff --git a/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/stylesheet.css b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/stylesheet.css new file mode 100644 index 0000000..5f7120e --- /dev/null +++ b/.local/share/gnome-shell/extensions/system-monitor@paradoxxx.zero.gmail.com/stylesheet.css @@ -0,0 +1,204 @@ +.sm-title { + font-size: 1em; + padding: 0px 25px 0px 0px; +} + +.sm-label { + color: #bbb; + font-size: 0.75em; + padding-top: 5px; + font-family: monospace; +} + +.sm-label-left { + color: #bbb; + font-size: 0.75em; + padding: 5px 5px 0 0; +} + +.sm-value { + text-align: right; + min-width: 40px; + padding: 2px 5px 0 0; + font-family: monospace; +} + +.sm-status-label { + color: #bbb; + font-size: 0.625em; + padding: 0 2px 0 5px; +} + +.sm-unit-label, +.sm-net-unit-label { + color: #aaa; + font-size: 0.5em; + padding: 0 5px 0 2px; +} + +.sm-disk-unit-label { + color: #aaa; + font-size: 0.625em; + padding: 0 5px 0 2px; +} + +.sm-net-unit-label { + min-width: 25px; +} + +.sm-perc-label { + color: #aaa; + font-size: 0.625em; + padding: 0 5px 0 2px; +} + +.sm-temp-label { + color: #aaa; + font-size: 0.6875em; + padding: 0 5px 0 2px; +} + +.sm-status-value { + min-width: 25px; + text-align: right; +} + +.sm-net-value { + min-width: 40px; + text-align: center; +} + +.sm-disk-value { + min-width: 25px; + text-align: right; +} + +.sm-big-status-value { + min-width: 42px; + text-align: right; +} + +.sm-chart { + padding: 0 2px; +} + +.sm-tooltip-box { + font-size: 0.85em; + font-weight: bold; + color: rgba(255, 255, 255, 0.9); + background-color: rgba(10, 10, 10, 0.7); + border-radius: 5px; + padding: 3px; + border: 1px solid #a5a5a5; +} + +.sm-tooltip-item { + padding: 0px; + spacing: 10px; +} +.sm-status-icon { + padding: 0px 2px; + icon-size: 1.14em; +} + +.sm-popup-menu-item-compact { +} + +/* style definition when 'compact-display' option is activated */ + +.sm-title-compact { + font-size: 0.75em; +} + +.sm-label-compact { + color: #bbb; + font-size: 0.6875em; + font-family: monospace; +} /* Bat, Net... units in menu items */ + +.sm-value-compact { + font-size: 0.6875em; + text-align: right; + font-family: monospace; +} /* Values in menu items */ + +.sm-status-label-compact { + color: #bbb; + font-size: 0.5em; + padding: 2px 0px 0 0px; +} /* names and Disk R and W signs in text items */ + +.sm-unit-label-compact, +.sm-net-unit-label-compact { + color: #aaa; + padding: 0 3px 0px 1px; + text-align: right; + font-size: 0.4375em; +} /* Bat, Net... units in text items */ + +.sm-disk-unit-label-compact { + color: #aaa; + padding: 0 3px 0px 0px; + text-align: right; + font-size: 0.4375em; +} /* Disk units in text items */ + +.sm-perc-label-compact { + padding: 0 3px 0 0px; + color: #aaa; + font-size: 0.5em; +} /* Cpu, Mem ... % sign in text items */ + +.sm-temp-label-compact { + font-size: 0.625em; + color: #aaa; + padding: 0 1px 0 1px; +} /* Thermal °C sign in text items */ + +.sm-status-value-compact { + padding:0 0px 0 1px; + font-size: 0.6875em; + text-align: right; + min-width:16px; +} /* Cpu, Mem... value in text items */ + +.sm-net-value-compact { + padding:0 0px 0 1px; + font-size: 0.6875em; + text-align: right; + min-width:24px; +} /* Net value in text items */ + +.sm-disk-value-compact { + padding: 0 1px 0 1px; + font-size: 0.6875em; + text-align: right; + min-width: 20px; +} /* Disk value in text items */ + +.sm-big-status-value-compact { + padding: 0 0px 0px 1px; + font-size: 0.6875em; + min-width: 38px; + text-align: right; +} /* freq in text_items */ + +.sm-chart-compact { + padding: 0 2px; +} + +.sm-tooltip-item-compact { + padding: 0px; + spacing: 10px; +} + +.sm-status-icon-compact { + padding: 1px 0 0 2px; + icon-size: 1.00em; +} /* BAT icon in text_items */ + +.sm-popup-menu-item-compact { + font-size: 0.6875em; + width: 20px; + padding: 1px 2px 1px 7px; +} diff --git a/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/AppManager.js b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/AppManager.js new file mode 100644 index 0000000..283b342 --- /dev/null +++ b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/AppManager.js @@ -0,0 +1,152 @@ +const AppSystem = imports.gi.Shell.AppSystem; +const WindowTracker = imports.gi.Shell.WindowTracker; +const getSettings = imports.misc.extensionUtils.getSettings; +const GLib = imports.gi.GLib; +const Me = imports.misc.extensionUtils.getCurrentExtension(); + +var leftClick = function (icon, event) { + let trayApp = getTrayApp(icon); + if (trayApp) { + let focusedApp = WindowTracker.get_default().focusApp; + let windows = trayApp.get_windows(); + + if (windows == "") { + return openApplication(trayApp, icon, event); + } + + if (focusedApp != null && focusedApp.id == trayApp.id) { + return minimizeWindows(focusedApp.get_windows(), icon, event); + } + + return activateWindows(windows, trayApp, event); + } + + icon.click(event); + + // On Windows double-click restore app + if (isWine(icon)) { + icon.click(event); + } +}; + +var middleClick = function (icon, event) { + // When holding SHIFT + if (event.get_state_full()[1] === 1) { + let trayApp = getTrayApp(icon); + if (trayApp) { + const pid = getPid(icon); + // Kill app + if (isUsingQt(pid)) { + return GLib.spawn_command_line_sync(`/bin/kill ${pid}`); + } + let windows = trayApp.get_windows(); + windows.forEach((window) => { + window.kill(); + }); + trayApp.request_quit(); + } + } else { + icon.click(event); + } +}; + +var getAppSetting = function (icon, setting) { + const iconApp = getTrayApp(icon); + const appsSettings = JSON.parse(getSettings().get_string("applications")); + const appSettings = appsSettings.find((app) => app.id == iconApp.get_id()); + + return appSettings?.[setting]; +}; + +function getTrayApp(icon) { + if (isWine(icon)) { + const wineApps = AppSystem.get_default() + .get_running() + .filter((app) => { + return app.get_windows()[0].wm_class.includes(".exe"); + }); + return wineApps[0]; + } + + const searchedApps = AppSystem.search(getWmClass(icon.wm_class)); + if (searchedApps[0] && searchedApps[0][0]) { + var i = 1; + for (let lookup of searchedApps[0]) { + let app = AppSystem.get_default().lookup_app(lookup); + if (app.get_windows() != "" || i == searchedApps[0].length) { + return app; + } + i++; + } + } + + return false; +} + +function openApplication(trayApp, icon, event) { + const isFlatpak = trayApp.app_info.has_key("X-Flatpak"); + const onBlacklist = Me.metadata["open-blacklist"].includes(icon.wm_class); // Caprine + if (isUsingQt(getPid(icon)) || isFlatpak || onBlacklist) { + return icon.click(event); + } + + return trayApp.open_new_window(0); +} + +function minimizeWindows(windows, icon, event) { + if (isUsingQt(getPid(icon))) { + return icon.click(event); + } + + windows.forEach((window) => { + window.minimize(); + }); +} + +function activateWindows(windows, trayApp, event) { + windows.forEach((window) => { + if (getSettings().get_boolean("invoke-to-workspace")) { + window.change_workspace(global.workspace_manager.get_active_workspace()); + } + trayApp.activate_window(window, event.get_time()); + window.unminimize(); + }); +} + +function isWine(icon) { + if ( + (icon.wm_class == "Wine" || icon.wm_class == "explorer.exe") && + getSettings().get_boolean("wine-behavior") + ) { + return true; + } +} + +function isUsingQt(pid) { + let [ok, out, err, exit] = GLib.spawn_command_line_sync( + `/bin/bash -c 'pmap -p ${pid} | grep Qt'` + ); + if (out.length) { + return true; + } +} + +function getWmClass(wmclass) { + wmclass = wmclass.replace(/[0-9]/g, ""); // skype discord + wmclass = wmclass.replace("Desktop", ""); // telegram + return wmclass; +} + +function getPid(icon) { + const wmclass = getWmClass(icon.wm_class); + if (icon.title != "snixembed") { + return icon.pid; + } + + let [ok, out, err, exit] = GLib.spawn_command_line_sync( + `/bin/bash -c "pidof -s ${wmclass}"` + ); + if (out.length) { + return Number(out); + } +} diff --git a/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/TrayIndicator.js b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/TrayIndicator.js new file mode 100644 index 0000000..d0ee40f --- /dev/null +++ b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/TrayIndicator.js @@ -0,0 +1,192 @@ +const { GObject, Clutter, St, GLib } = imports.gi; +const { panelMenu, popupMenu } = imports.ui; +const { getCurrentExtension, getSettings } = imports.misc.extensionUtils; +const AppManager = getCurrentExtension().imports.AppManager; + +var TrayIndicator = GObject.registerClass( + class TrayIndicator extends panelMenu.Button { + _init() { + this._icons = []; + + super._init(0.0, null, false); + this._overflow = false; + + this._indicators = new St.BoxLayout(); + this.add_child(this._indicators); + + this._icon = new St.Icon({ + icon_name: "view-more-horizontal", + style_class: "system-status-icon", + reactive: true, + track_hover: true, + }); + this._indicators.add_child(this._icon); + + this._menuItem = new popupMenu.PopupBaseMenuItem({ + reactive: false, + can_focus: true, + }); + this.menu.addMenuItem(this._menuItem); + this.menu.actor.add_style_class_name("TrayIndicatorPopup"); + this.hide(); + } + + get size() { + const context = St.ThemeContext.get_for_stage(global.stage); + return this._size * context.scale_factor; + } + + setSize(size, margin, padding) { + this._size = size; + this._margin = margin; + this._padding = padding; + + this._icons.forEach((icon) => { + icon.get_parent().style = this._getButtonStyle(); + icon.set_size(this._size, this._size); + }); + } + + addIcon(icon) { + const isHidden = AppManager.getAppSetting(icon, "hidden"); + if (isHidden) return; + + const button = new St.Button({ + child: icon, + button_mask: + St.ButtonMask.ONE | St.ButtonMask.TWO | St.ButtonMask.THREE, + style: this._getButtonStyle(), + style_class: "panel-button", + }); + icon.opacity = 0; + icon.set_x_align(Clutter.ActorAlign.CENTER); + icon.set_y_align(Clutter.ActorAlign.CENTER); + icon.inOverflow = this._overflow; + GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, () => { + icon.set_size(this.size, this.size); + icon.ease({ + opacity: 255, + duration: 400, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + this._addEffectIcon(icon); + return GLib.SOURCE_REMOVE; + }); + icon.connect("destroy", () => { + button.destroy(); + }); + + button.connect("button-release-event", (actor, event) => { + switch (event.get_button()) { + case 1: + AppManager.leftClick(icon, event); + break; + case 2: + AppManager.middleClick(icon, event); + break; + case 3: + icon.click(event); + break; + } + if (AppManager.isWine(icon)) { + GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1, () => { + this.menu.close(); + return GLib.SOURCE_REMOVE; + }); + } else { + this.menu.close(); + } + }); + + this._icons.push(icon); + + if (this._overflow) { + this._menuItem.actor.add(button); + } else { + this._indicators.insert_child_at_index(button, 0); + } + + this.checkOverflow(); + } + + removeIcon(icon, ignoreCheckOverflow) { + const index = this._icons.indexOf(icon); + this._icons.splice(index, 1); + + const actor = icon.get_parent(); + actor.remove_actor(icon); + actor.destroy(); + + if (!ignoreCheckOverflow) { + this.checkOverflow(); + } + } + + checkOverflow() { + if (this._icons.length >= getSettings().get_int("icons-limit")) { + this._overflow = true; + this._icon.visible = true; + this.reactive = true; + this.style_class = "panel-button TrayIndicator"; + } else { + this._overflow = false; + this._icon.visible = false; + this.reactive = false; + this.style_class = "TrayIndicator"; + } + + if (this._icons.length) { + this.show(); + } else { + this.hide(); + } + + this._refreshIcons(this._overflow); + } + + _refreshIcons(overflow) { + this._icons.forEach((icon) => { + if (icon.inOverflow != overflow) { + this.removeIcon(icon, true); + this.addIcon(icon); + } + }); + } + + _getButtonStyle() { + let style; + if (!this._overflow) { + style = `margin: ${this._margin.vertical}px ${this._margin.horizontal}px; padding: ${this._padding.vertical}px ${this._padding.horizontal}px`; + } + return `width: ${this.size}px; height: ${this.size}px;${style}`; + } + + _addEffectIcon(icon) { + let brightnessContrast = new Clutter.BrightnessContrastEffect({}); + brightnessContrast.set_contrast( + getSettings().get_int("icon-contrast") / 100 + ); + brightnessContrast.set_brightness( + getSettings().get_int("icon-brightness") / 100 + ); + icon.add_effect_with_name("brightnessContrast", brightnessContrast); + icon.add_effect_with_name( + "desaturate", + new Clutter.DesaturateEffect({ + factor: getSettings().get_int("icon-saturation") / 100, + }) + ); + } + + setEffect(contrast, saturation, brightness) { + this._icons.forEach((icon) => { + let brightnessContrast = icon.get_effect("brightnessContrast"); + brightnessContrast.set_contrast(contrast / 100); + brightnessContrast.set_brightness(brightness / 100); + + let desaturate = icon.get_effect("desaturate"); + desaturate.set_factor(saturation / 100); + }); + } + } +); diff --git a/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/extension.js b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/extension.js new file mode 100644 index 0000000..178041f --- /dev/null +++ b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/extension.js @@ -0,0 +1,97 @@ +const { GObject, Shell } = imports.gi; +const { getCurrentExtension, getSettings } = imports.misc.extensionUtils; +const System = imports.system; +const Main = imports.ui.main; +const TrayIndicator = getCurrentExtension().imports.TrayIndicator; + +var TrayIconsClass = GObject.registerClass( +class TrayIconsClass extends GObject.Object { + _init() { + this.tray = new Shell.TrayManager(); + this.indicators = new TrayIndicator.TrayIndicator(); + + this.tray.connect('tray-icon-added', this._onIconAdded.bind(this)); + this.tray.connect('tray-icon-removed', this._onIconRemoved.bind(this)); + + this.tray.manage_screen(Main.panel); + } + + _onIconAdded(trayManager, icon) { this.indicators.addIcon(icon); } + _onIconRemoved(trayManager, icon) { this.indicators.removeIcon(icon); } + + _destroy() { + this.tray = null; + + this.indicators.destroy(); + System.gc(); + } +}); + +let TrayIcons; + +class Extension { + _setIconSize() { + const margin = { vertical: this._settings.get_int('icon-margin-vertical'), horizontal: this._settings.get_int('icon-margin-horizontal') } + const padding = { vertical: this._settings.get_int('icon-padding-vertical'), horizontal: this._settings.get_int('icon-padding-horizontal') } + TrayIcons.indicators.setSize(this._settings.get_int('icon-size'), margin, padding); + } + + _setTrayMargin() { + TrayIcons.indicators.set_style('margin-left: ' + this._settings.get_int('tray-margin-left') + 'px; margin-right: ' + this._settings.get_int('tray-margin-right') + 'px'); + } + + _setTrayArea() { + Main.panel.statusArea['TrayIconsReloaded'] = null; + Main.panel.addToStatusArea('TrayIconsReloaded', TrayIcons.indicators, this._settings.get_int('position-weight'), this._settings.get_string('tray-position')); + } + + _setIconsLimit() { + TrayIcons.indicators.checkOverflow(); + } + + _setIconEffect() { + TrayIcons.indicators.setEffect(this._settings.get_int('icon-contrast'), this._settings.get_int('icon-saturation'), this._settings.get_int('icon-brightness')); + } + + _onChange() { + this._settings.connect('changed::tray-position', this._setTrayArea.bind(this)); + this._settings.connect('changed::position-weight', this._setTrayArea.bind(this)); + this._settings.connect('changed::tray-margin-left', this._setTrayMargin.bind(this)); + this._settings.connect('changed::tray-margin-right', this._setTrayMargin.bind(this)); + this._settings.connect('changed::icon-size', this._setIconSize.bind(this)); + this._settings.connect('changed::icon-margin-horizontal', this._setIconSize.bind(this)); + this._settings.connect('changed::icon-margin-vertical', this._setIconSize.bind(this)); + this._settings.connect('changed::icon-padding-vertical', this._setIconSize.bind(this)); + this._settings.connect('changed::icon-padding-horizontal', this._setIconSize.bind(this)); + this._settings.connect('changed::icons-limit', this._setIconsLimit.bind()); + this._settings.connect('changed::icon-saturation', this._setIconEffect.bind(this)); + this._settings.connect('changed::icon-contrast', this._setIconEffect.bind(this)); + this._settings.connect('changed::icon-brightness', this._setIconEffect.bind(this)); + } + + enable() { + TrayIcons = new TrayIconsClass(); + this._settings = getSettings(); + this._setTrayMargin(); + this._setIconSize(); + this._onChange(); + + if (Main.layoutManager._startingUp) { + this._startupComplete = Main.layoutManager.connect('startup-complete', () => { + this._setTrayArea(); + Main.layoutManager.disconnect(this._startupComplete); + }); + } else { + this._setTrayArea(); + } + } + + disable() { + TrayIcons._destroy(); + this._settings.run_dispose(); + } +} + +function init() { + return new Extension(); +} \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/metadata.json b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/metadata.json new file mode 100644 index 0000000..ec8068a --- /dev/null +++ b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/metadata.json @@ -0,0 +1,16 @@ +{ + "_generated": "Generated by SweetTooth, do not edit", + "description": "Tray Icons Reloaded is a GNOME Shell extension which bring back Tray Icons to top panel, with additional features.\n\n>>> Read compatibility note on GitHub there is also bug reporting <<<", + "name": "Tray Icons: Reloaded", + "open-blacklist": [ + "Electron", + "Yad" + ], + "settings-schema": "org.gnome.shell.extensions.trayIconsReloaded", + "shell-version": [ + "41" + ], + "url": "https://github.com/MartinPL/Tray-Icons-Reloaded", + "uuid": "trayIconsReloaded@selfmade.pl", + "version": 19 +} \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/AppChooser.js b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/AppChooser.js new file mode 100644 index 0000000..f81423e --- /dev/null +++ b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/AppChooser.js @@ -0,0 +1,56 @@ +const { GObject, Gtk } = imports.gi; +const ExtensionUtils = imports.misc.extensionUtils; +const getSettings = ExtensionUtils.getSettings; + +var AppChooser = GObject.registerClass( + class AppChooser extends Gtk.AppChooserDialog { + _init(parent) { + super._init({ + transient_for: parent, + modal: true, + }); + + this._widget = this.get_widget(); + this._widget.set({ + show_all: true, + show_other: true, + }); + this._widget.connect( + "application-selected", + this._updateSensitivity.bind(this) + ); + + this.connect("response", this._onResponse.bind(this)); + this._updateSensitivity(); + } + + _updateSensitivity() { + const apps = JSON.parse(getSettings().get_string("applications")); + const appInfo = this._widget.get_app_info(); + + this.set_response_sensitive( + Gtk.ResponseType.OK, + appInfo && !apps.some((app) => app.id.startsWith(appInfo.get_id())) + ); + } + + _onResponse(dlg, id) { + const appInfo = + id === Gtk.ResponseType.OK ? this._widget.get_app_info() : null; + + if (appInfo) { + let apps = JSON.parse(getSettings().get_string("applications")); + apps = [ + ...apps, + { + id: appInfo.get_id(), + }, + ]; + + getSettings().set_string("applications", JSON.stringify(apps)); + } + + this.destroy(); + } + } +); diff --git a/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/AppRow.js b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/AppRow.js new file mode 100644 index 0000000..2b2b538 --- /dev/null +++ b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/AppRow.js @@ -0,0 +1,54 @@ +const { GObject, Gtk, Gio } = imports.gi; +const ExtensionUtils = imports.misc.extensionUtils; +const getSettings = ExtensionUtils.getSettings; +const Me = ExtensionUtils.getCurrentExtension(); + +var AppRow = GObject.registerClass( + { + GTypeName: "AppRow", + Template: Me.dir.get_child("preferences/AppRow.xml").get_uri(), + InternalChildren: ["icon", "label", "revealButton", "revealer", "hidden"], + }, + class AppRow extends Gtk.ListBoxRow { + _init(app) { + super._init(); + this._appInfo = Gio.DesktopAppInfo.new(app.id); + this._settings = getSettings(); + this._icon.gicon = this._appInfo.get_icon(); + this._label.label = this._appInfo.get_display_name(); + this._hidden.set_active(app.hidden); + this._hidden.connect("state-set", () => { + this._updateApp(); + }); + this.appId = this._appInfo.get_id(); + } + + toggleSettingsVisibility() { + this._revealer.reveal_child = !this._revealer.reveal_child; + + if (this._revealer.reveal_child) { + this._revealButton.get_style_context().add_class("expanded"); + } else { + this._revealButton.get_style_context().remove_class("expanded"); + } + } + + removeRow() { + const current = JSON.parse(this._settings.get_string("applications")); + const updated = current.filter((app) => app.id !== this.appId); + + this._settings.set_string("applications", JSON.stringify(updated)); + } + + _updateApp() { + let apps = JSON.parse(this._settings.get_string("applications")); + const index = apps.findIndex((app) => app.id == this.appId); + apps[index] = { + id: this.appId, + hidden: this._hidden.get_active(), + }; + + this._settings.set_string("applications", JSON.stringify(apps)); + } + } +); diff --git a/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/AppRow.xml b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/AppRow.xml new file mode 100644 index 0000000..d171f7b --- /dev/null +++ b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/AppRow.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="AppRow" parent="GtkListBoxRow"> + <property name="selectable">false</property> + <property name="activatable">false</property> + <child> + <object class="GtkGrid"> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <property name="column-spacing">12</property> + <child> + <object class="GtkImage" id="icon"> + <property name="pixel-size">32</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label"> + <property name="halign">start</property> + <property name="hexpand">true</property> + </object> + </child> + <child> + <object class="GtkButton" id="remove-button"> + <property name="tooltip-text" translatable="yes">Remove</property> + <property name="halign">center</property> + <property name="valign">center</property> + <signal name="clicked" handler="removeRow" swapped="no" /> + <child> + <object class="GtkImage"> + <property name="icon-name">user-trash-symbolic</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkSeparator"/> + </child> + <child> + <object class="GtkButton" id="revealButton"> + <property name="valign">center</property> + <property name="has-frame">false</property> + <property name="icon-name">pan-end-symbolic</property> + <signal name="clicked" handler="toggleSettingsVisibility" swapped="no" /> + <style> + <class name="reveal-button"/> + </style> + </object> + </child> + <child> + <object class="GtkRevealer" id="revealer"> + <child> + <object class="GtkListBox"> + <property name="margin-top">8</property> + <child> + <object class="GtkListBoxRow"> + <property name="selectable">false</property> + <property name="activatable">false</property> + <property name="child"> + <object class="GtkBox"> + <property name="margin-top">8</property> + <property name="margin-bottom">8</property> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Hidden</property> + <property name="halign">start</property> + <property name="hexpand">true</property> + </object> + </child> + <child> + <object class="GtkSwitch" id="hidden"> + <property name="halign">end</property> + </object> + </child> + </object> + </property> + </object> + </child> + </object> + </child> + <layout> + <property name="column">0</property> + <property name="row">1</property> + <property name="column-span">5</property> + </layout> + </object> + </child> + </object> + </child> + </template> +</interface> \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/Prefs.css b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/Prefs.css new file mode 100644 index 0000000..6f12772 --- /dev/null +++ b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/Prefs.css @@ -0,0 +1,6 @@ +.reveal-button image { + transition: 250ms; +} +.reveal-button--expanded image { + -gtk-icon-transform: rotate(0.25turn); +} diff --git a/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/Prefs.js b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/Prefs.js new file mode 100644 index 0000000..07c2a9f --- /dev/null +++ b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/Prefs.js @@ -0,0 +1,124 @@ +const { GObject, Gtk, Gio, Gdk } = imports.gi; +const ExtensionUtils = imports.misc.extensionUtils; +const getSettings = ExtensionUtils.getSettings; +const Me = ExtensionUtils.getCurrentExtension(); +const AppRow = Me.imports.preferences.AppRow.AppRow; +const AppChooser = Me.imports.preferences.AppChooser.AppChooser; + +const schemaNames = [ + "tray-position", + "position-weight", + "tray-margin-left", + "tray-margin-right", + "icons-limit", + "icon-size", + "icon-margin-vertical", + "icon-margin-horizontal", + "icon-padding-vertical", + "icon-padding-horizontal", + "icon-saturation", + "icon-contrast", + "icon-brightness", + "invoke-to-workspace", + "wine-behavior", +]; + +const settingIds = schemaNames.map(function (name) { + return name.replaceAll("-", "_"); +}); + +var Prefs = GObject.registerClass( + { + GTypeName: "Prefs", + Template: Me.dir.get_child("preferences/Prefs.xml").get_uri(), + InternalChildren: ["headerBar", "appList", ...settingIds], + }, + class Prefs extends Gtk.Box { + _init(params = {}) { + super._init(params); + + this._bindSettings(schemaNames); + + this.connect("realize", () => { + const window = this.get_root(); + window.set_titlebar(this._headerBar); + }); + + let provider = new Gtk.CssProvider(); + provider.load_from_file( + Gio.File.new_for_uri( + Me.dir.get_child("preferences/Prefs.css").get_uri() + ) + ); + Gtk.StyleContext.add_provider_for_display( + Gdk.Display.get_default(), + provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ); + + this._settings = getSettings(); + + this._changeId = this._settings.connect( + "changed::applications", + this._syncAppsRows.bind(this) + ); + + this._syncAppsRows(); + } + + showAppChooser() { + const dialog = new AppChooser(this.get_root()); + dialog.show(); + } + + _syncAppsRows() { + this._settings.block_signal_handler(this._changeId); + + const oldApps = [...this._appList].filter((row) => !!row.appId); + const newApps = JSON.parse( + this._settings.get_string("applications") + ).filter((app) => !!app); + + newApps.forEach((appInfo, index) => { + if (!oldApps.some((row) => row.appId == appInfo.id)) { + const appRow = new AppRow(appInfo); + this._appList.insert(appRow, index); + + if (this._notFirstSync) { + appRow.toggleSettingsVisibility(); + } + } + }); + + oldApps.forEach((row, index) => { + if (!newApps.some((app) => row.appId == app.id)) { + this._appList.remove(row); + } + }); + + this._notFirstSync = true; + + this._settings.unblock_signal_handler(this._changeId); + } + + _bindSettings(settings) { + settings.forEach((name) => { + let obj = eval("this._" + name.replaceAll("-", "_")); + let valueType; + + switch (obj.css_name) { + case "combobox": + valueType = "active-id"; + break; + case "switch": + valueType = "active"; + break; + default: + valueType = "value"; + } + + getSettings().bind(name, obj, valueType, Gio.SettingsBindFlags.DEFAULT); + }); + } + } +); diff --git a/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/Prefs.xml b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/Prefs.xml new file mode 100644 index 0000000..28db0fa --- /dev/null +++ b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/preferences/Prefs.xml @@ -0,0 +1,566 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <object class="GtkHeaderBar" id="headerBar"> + <property name="title-widget"> + <object class="GtkStackSwitcher"> + <property name="stack">stack</property> + </object> + </property> + </object> + <template class="Prefs" parent="GtkBox"> + <property name="orientation">vertical</property> + <child> + <object class="GtkStack" id="stack"> + <property name="transition-type">slide-left-right</property> + <child> + <object class="GtkStackPage"> + <property name="title">General</property> + <property name="child"> + <object class="GtkScrolledWindow"> + <property name="vscrollbar-policy">never</property> + <property name="hscrollbar-policy">never</property> + <child> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <property name="valign">start</property> + <property name="halign">center</property> + <property name="width-request">630</property> + <property name="margin-top">24</property> + <property name="margin-bottom">24</property> + <property name="margin-start">24</property> + <property name="margin-end">24</property> + <child> + <object class="GtkFrame"> + <property name="child"> + <object class="GtkListBox"> + <child> + <object class="GtkListBoxRow"> + <property name="selectable">0</property> + <property name="child"> + <object class="GtkBox"> + <property name="margin-top">8</property> + <property name="margin-bottom">8</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Tray position</property> + <property name="halign">start</property> + <property name="hexpand">1</property> + </object> + </child> + <child> + <object class="GtkComboBoxText" id="tray_position"> + <property name="halign">end</property> + <items> + <item translatable="yes" id="left">Left</item> + <item translatable="yes" id="center">Center</item> + <item translatable="yes" id="right">Right</item> + </items> + </object> + </child> + <child> + <object class="GtkAdjustment" id="adjustment-position-weight"> + <property name="step-increment">1</property> + <property name="lower">-99</property> + <property name="upper">99</property> + </object> + <object class="GtkSpinButton" id="position_weight"> + <property name="tooltip-text" translatable="yes">Position weight</property> + <property name="halign">end</property> + <property name="adjustment">adjustment-position-weight</property> + </object> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="selectable">0</property> + <property name="child"> + <object class="GtkBox"> + <property name="margin-top">8</property> + <property name="margin-bottom">8</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Tray icons limit</property> + <property name="halign">start</property> + <property name="hexpand">1</property> + </object> + </child> + <child> + <object class="GtkAdjustment" id="adjustment-icons-limit"> + <property name="step-increment">1</property> + <property name="lower">1</property> + <property name="upper">16</property> + </object> + <object class="GtkSpinButton" id="icons_limit"> + <property name="halign">end</property> + <property name="adjustment">adjustment-icons-limit</property> + </object> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="selectable">0</property> + <property name="child"> + <object class="GtkBox"> + <property name="margin-top">8</property> + <property name="margin-bottom">8</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Icon size</property> + <property name="halign">start</property> + <property name="hexpand">1</property> + </object> + </child> + <child> + <object class="GtkAdjustment" id="adjustment-icon-size"> + <property name="step-increment">1</property> + <property name="lower">16</property> + <property name="upper">32</property> + </object> + <object class="GtkSpinButton" id="icon_size"> + <property name="halign">end</property> + <property name="adjustment">adjustment-icon-size</property> + </object> + </child> + </object> + </property> + </object> + </child> + + </object> + </property> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Colors</property> + <property name="halign">start</property> + <property name="margin-top">24</property> + <property name="margin-bottom">8</property> + <attributes> + <attribute name="weight" value="bold"></attribute> + </attributes> + </object> + </child> + <child> + <object class="GtkFrame"> + <property name="child"> + <object class="GtkListBox"> + <child> + <object class="GtkListBoxRow"> + <property name="selectable">0</property> + <property name="child"> + <object class="GtkBox"> + <property name="margin-top">8</property> + <property name="margin-bottom">8</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Icon saturation</property> + <property name="halign">start</property> + <property name="hexpand">1</property> + </object> + </child> + <child> + <object class="GtkAdjustment" id="adjustment-icon-saturation"> + <property name="step-increment">10</property> + <property name="page-size">20</property> + <property name="upper">100</property> + </object> + <object class="GtkSpinButton" id="icon_saturation"> + <property name="halign">end</property> + <property name="adjustment">adjustment-icon-saturation</property> + </object> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="selectable">0</property> + <property name="child"> + <object class="GtkBox"> + <property name="margin-top">8</property> + <property name="margin-bottom">8</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Icon contrast</property> + <property name="halign">start</property> + <property name="hexpand">1</property> + </object> + </child> + <child> + <object class="GtkAdjustment" id="adjustment-icon-contrast"> + <property name="step-increment">10</property> + <property name="page-size">20</property> + <property name="lower">-100</property> + <property name="upper">100</property> + </object> + <object class="GtkSpinButton" id="icon_contrast"> + <property name="halign">end</property> + <property name="adjustment">adjustment-icon-contrast</property> + </object> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="selectable">0</property> + <property name="child"> + <object class="GtkBox"> + <property name="margin-top">8</property> + <property name="margin-bottom">8</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Icon brightness</property> + <property name="halign">start</property> + <property name="hexpand">1</property> + </object> + </child> + <child> + <object class="GtkAdjustment" id="adjustment-icon-brightness"> + <property name="step-increment">10</property> + <property name="page-size">20</property> + <property name="lower">-100</property> + <property name="upper">100</property> + </object> + <object class="GtkSpinButton" id="icon_brightness"> + <property name="halign">end</property> + <property name="adjustment">adjustment-icon-brightness</property> + </object> + </child> + </object> + </property> + </object> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Spacing</property> + <property name="halign">start</property> + <property name="margin-top">24</property> + <property name="margin-bottom">8</property> + <attributes> + <attribute name="weight" value="bold"></attribute> + </attributes> + </object> + </child> + <child> + <object class="GtkFrame"> + <property name="child"> + <object class="GtkListBox"> + <child> + <object class="GtkListBoxRow"> + <property name="selectable">0</property> + <property name="child"> + <object class="GtkBox"> + <property name="margin-top">8</property> + <property name="margin-bottom">8</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Tray margin</property> + <property name="halign">start</property> + <property name="hexpand">1</property> + </object> + </child> + <child> + <object class="GtkAdjustment" id="adjustment-tray-margin-left"> + <property name="step-increment">1</property> + <property name="upper">24</property> + </object> + <object class="GtkSpinButton" id="tray_margin_left"> + <property name="halign">center</property> + <property name="adjustment">adjustment-tray-margin-left</property> + <property name="tooltip-text" translatable="yes">Left margin</property> + </object> + </child> + <child> + <object class="GtkAdjustment" id="adjustment-tray-margin-right"> + <property name="step-increment">1</property> + <property name="upper">24</property> + </object> + <object class="GtkSpinButton" id="tray_margin_right"> + <property name="halign">end</property> + <property name="adjustment">adjustment-tray-margin-right</property> + <property name="tooltip-text" translatable="yes">Right margin</property> + </object> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="selectable">0</property> + <property name="child"> + <object class="GtkBox"> + <property name="margin-top">8</property> + <property name="margin-bottom">8</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Icon margin</property> + <property name="halign">start</property> + <property name="hexpand">1</property> + </object> + </child> + <child> + <object class="GtkAdjustment" id="adjustment-icon-margin-vertical"> + <property name="step-increment">1</property> + <property name="upper">24</property> + </object> + <object class="GtkSpinButton" id="icon_margin_vertical"> + <property name="halign">center</property> + <property name="adjustment">adjustment-icon-margin-vertical</property> + <property name="tooltip-text" translatable="yes">Vertical icon margin. NOTE: May not update in real time</property> + </object> + </child> + <child> + <object class="GtkAdjustment" id="adjustment-icon-margin-horizontal"> + <property name="step-increment">1</property> + <property name="upper">24</property> + </object> + <object class="GtkSpinButton" id="icon_margin_horizontal"> + <property name="halign">end</property> + <property name="adjustment">adjustment-icon-margin-horizontal</property> + <property name="tooltip-text" translatable="yes">Horizontal icon margin</property> + </object> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="selectable">0</property> + <property name="child"> + <object class="GtkBox"> + <property name="margin-top">8</property> + <property name="margin-bottom">8</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Icon padding</property> + <property name="halign">start</property> + <property name="hexpand">1</property> + </object> + </child> + <child> + <object class="GtkAdjustment" id="adjustment-icon-padding-vertical"> + <property name="step-increment">1</property> + <property name="upper">24</property> + </object> + <object class="GtkSpinButton" id="icon_padding_vertical"> + <property name="valign">center</property> + <property name="halign">center</property> + <property name="adjustment">adjustment-icon-padding-vertical</property> + <property name="tooltip-text" translatable="yes">Vertical icon padding</property> + </object> + </child> + <child> + <object class="GtkAdjustment" id="adjustment-icon-padding-horizontal"> + <property name="step-increment">1</property> + <property name="upper">24</property> + </object> + <object class="GtkSpinButton" id="icon_padding_horizontal"> + <property name="tooltip-text" translatable="yes">Horizontal icon padding</property> + <property name="valign">center</property> + <property name="halign">end</property> + <property name="adjustment">adjustment-icon-padding-horizontal</property> + </object> + </child> + </object> + </property> + </object> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Behavior</property> + <property name="halign">start</property> + <property name="margin-top">24</property> + <property name="margin-bottom">8</property> + <attributes> + <attribute name="weight" value="bold"></attribute> + </attributes> + </object> + </child> + <child> + <object class="GtkFrame"> + <property name="child"> + <object class="GtkListBox"> + <child> + <object class="GtkListBoxRow"> + <property name="selectable">0</property> + <property name="child"> + <object class="GtkBox"> + <property name="margin-top">8</property> + <property name="margin-bottom">8</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Invoke windows to current workspace</property> + <property name="halign">start</property> + <property name="hexpand">1</property> + </object> + </child> + <child> + <object class="GtkSwitch" id="invoke_to_workspace"> + <property name="halign">end</property> + </object> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkListBoxRow"> + <property name="selectable">0</property> + <property name="child"> + <object class="GtkBox"> + <property name="margin-top">8</property> + <property name="margin-bottom">8</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Modify Wine apps behavior</property> + <property name="halign">start</property> + <property name="hexpand">1</property> + </object> + </child> + <child> + <object class="GtkSwitch" id="wine_behavior"> + <property name="halign">end</property> + </object> + </child> + </object> + </property> + </object> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkGrid"> + <property name="margin-top">16</property> + <property name="valign">start</property> + <property name="halign">center</property> + <child> + <object class="GtkLinkButton"> + <property name="label">GitHub</property> + <property name="uri">https://github.com/MartinPL/Tray-Icons-Reloaded</property> + <property name="halign">start</property> + <layout> + <property name="row">0</property> + <property name="column">1</property> + </layout> + </object> + </child> + <child> + <object class="GtkLinkButton"> + <property name="label">Donate</property> + <property name="uri">https://revolut.me/martinpl</property> + <property name="halign">end</property> + <layout> + <property name="row">0</property> + <property name="column">2</property> + </layout> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkStackPage"> + <property name="title">Applications</property> + <property name="child"> + <object class="GtkScrolledWindow"> + <property name="propagate-natural-height">1</property> + <property name="hscrollbar-policy">never</property> + <child> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <property name="valign">start</property> + <property name="halign">center</property> + <property name="width-request">630</property> + <property name="margin-top">24</property> + <property name="margin-bottom">24</property> + <property name="margin-start">24</property> + <property name="margin-end">24</property> + <child> + <object class="GtkFrame"> + <property name="child"> + <object class="GtkListBox" id="appList"> + <child> + <object class="GtkListBoxRow"> + <property name="height-request">50</property> + <property name="selectable">false</property> + <property name="activatable">false</property> + <child> + <object class="GtkButton"> + <property name="receives_default">True</property> + <signal name="clicked" handler="showAppChooser" swapped="no" /> + <child> + <object class="GtkImage" id="add-button"> + <property name="icon-name">list-add-symbolic</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </property> + </object> + </child> + </object> + </child> + </object> + </property> + </object> + </child> + </object> + </child> + </template> +</interface> \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/prefs.js b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/prefs.js new file mode 100644 index 0000000..f50b12d --- /dev/null +++ b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/prefs.js @@ -0,0 +1,8 @@ +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const Prefs = Me.imports.preferences.Prefs.Prefs; + +function buildPrefsWidget() { + return new Prefs(); +} + +function init() {} diff --git a/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/schemas/gschemas.compiled b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/schemas/gschemas.compiled new file mode 100644 index 0000000000000000000000000000000000000000..76d175ddab5fd81605d4fc161d0cf8b68b5a3887 GIT binary patch literal 1204 zcmZWpO=uHA7~NW<t!+&FsRlfF5HSTe35Xs%c{757h=?K{Cdo7zy4ejoYf@WUFF8~Q zSV7Q>5QK`y9Q;MpdaJh_#gjLC5Un6$5z+T%H*JaokN5WN_kHuT-|Xp2!dJo#tZ1ab zmqVT2(lfy~pWj;@h~6Ew@>UbNiGbLcW&OqIY-R9Si~I!e{Box3dR3Xx73nw`ITc7( zE6>%Lz!x{p6rnCi#}g%4LSlM1)1$$82LYBOVtv>KO>75-BJ2R~1QHQ$-RZncLUVm_ zt8U!~w}EqC2T#(c9)aHhzX0;_w*~stN%+g)F7W=rkGu4#_rqTSuL6f3&a~-MC*X6j zLqKc7dQG2t6n+YP5LlX>8{rvI$Ka>ISAfs0M_(9E&2?75KCl)XYtg43gFg>m0G@W| zZquh`d>gz7oI3EPNuPQz{3Y-*aB*dRkv=uo`2qY1=zf3qkv{c4_`kqB=%2YmGxVv4 z;U~c<;Lo!oIr`Ll;OD@{fp1g!6ZENBKMy_+^m>AtJwaXoe$OUeF`jxC;va)MfI>6a z02)KDHzHHhYMk%1?Wn2>-yq!hY$CfV{IYUwM@|Gi-_)v!Qc1aGyW;t38g+$(@48XX z)*`s>ivZn7*VGfWnxl%w)3mN$gRRFiH4#+!oJB)tO+#l*LuWS{I{RNkXDv&c1=c*R z!fAX}u3!f!xoL%U=?9qB;S^>a4dqI^AS<G-JRjD8l`e!S(PlSin6%zG*#$F;E4A5z zsb^-3Qu~g>+^h9Gt)^vI3H^h?b9d|Bq_hLiZg~Eru8Cq8Vb;^zmfeuywxVR@@ve?U okE3ATO|}>BD!)-2;D4ldUvY)d3*mhp2KRs0=*-|o1NEu?0l=~QuK)l5 literal 0 HcmV?d00001 diff --git a/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/schemas/org.gnome.shell.extensions.trayIconsReloaded.gschema.xml b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/schemas/org.gnome.shell.extensions.trayIconsReloaded.gschema.xml new file mode 100644 index 0000000..30af6a7 --- /dev/null +++ b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/schemas/org.gnome.shell.extensions.trayIconsReloaded.gschema.xml @@ -0,0 +1,68 @@ +<schemalist> + <schema id="org.gnome.shell.extensions.trayIconsReloaded" path="/org/gnome/shell/extensions/trayIconsReloaded/"> + <key name="tray-position" type="s"> + <default>"right"</default> + <summary>Tray position</summary> + </key> + <key name="position-weight" type="i"> + <default>0</default> + <summary>Position weight</summary> + </key> + <key name="tray-margin-left" type="i"> + <default>4</default> + <summary>Tray icons left margin</summary> + </key> + <key name="tray-margin-right" type="i"> + <default>0</default> + <summary>Tray icons left margin</summary> + </key> + <key name="icons-limit" type="i"> + <default>4</default> + <summary>Icons limit</summary> + </key> + <key name="icon-size" type="i"> + <default>20</default> + <summary>Icon size</summary> + </key> + <key name="icon-margin-vertical" type="i"> + <default>0</default> + <summary>Icon margin vertical</summary> + </key> + <key name="icon-margin-horizontal" type="i"> + <default>4</default> + <summary>Icon margin horizontal</summary> + </key> + <key name="icon-padding-vertical" type="i"> + <default>0</default> + <summary>Icon padding vertical</summary> + </key> + <key name="icon-padding-horizontal" type="i"> + <default>16</default> + <summary>Icon padding horizontal</summary> + </key> + <key name="icon-saturation" type="i"> + <default>0</default> + <summary>Icon contrast</summary> + </key> + <key name="icon-contrast" type="i"> + <default>0</default> + <summary>Icon contrast</summary> + </key> + <key name="icon-brightness" type="i"> + <default>0</default> + <summary>Icon contrast</summary> + </key> + <key name="invoke-to-workspace" type="b"> + <default>true</default> + <summary>Whether to invoke window to current workspace</summary> + </key> + <key name="wine-behavior" type="b"> + <default>true</default> + <summary>Wine behavior</summary> + </key> + <key name="applications" type="s"> + <default>"[]"</default> + <summary>Applications list</summary> + </key> + </schema> +</schemalist> \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/stylesheet.css b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/stylesheet.css new file mode 100644 index 0000000..40a92b3 --- /dev/null +++ b/.local/share/gnome-shell/extensions/trayIconsReloaded@selfmade.pl/stylesheet.css @@ -0,0 +1,11 @@ +.TrayIndicatorPopup StButton { + padding: 8px; + border-radius: 6px; + transition-property: background-color; + transition-duration: 0.1s; + transition-timing-function: linear; +} + +.TrayIndicatorPopup StButton:hover { + background-color: rgba(255, 255, 255, 0.1); +} diff --git a/.local/share/gnome-shell/extensions/workspaces-bar@fthx/README.md b/.local/share/gnome-shell/extensions/workspaces-bar@fthx/README.md new file mode 100644 index 0000000..6589088 --- /dev/null +++ b/.local/share/gnome-shell/extensions/workspaces-bar@fthx/README.md @@ -0,0 +1,4 @@ +# workspaces-bar +GNOME Shell extension that shows workspaces buttons in top panel + +https://extensions.gnome.org/extension/3851/workspaces-bar/ diff --git a/.local/share/gnome-shell/extensions/workspaces-bar@fthx/extension.js b/.local/share/gnome-shell/extensions/workspaces-bar@fthx/extension.js new file mode 100644 index 0000000..1cb31c7 --- /dev/null +++ b/.local/share/gnome-shell/extensions/workspaces-bar@fthx/extension.js @@ -0,0 +1,146 @@ +/* + Workspaces Bar + Copyright Francois Thirioux 2021 + GitHub contributors: @fthx + License GPL v3 +*/ + + +const { Clutter, Gio, GObject, Shell, St } = imports.gi; + +const Main = imports.ui.main; +const PanelMenu = imports.ui.panelMenu; + +var WORKSPACES_SCHEMA = "org.gnome.desktop.wm.preferences"; +var WORKSPACES_KEY = "workspace-names"; + + +var WorkspacesBar = GObject.registerClass( +class WorkspacesBar extends PanelMenu.Button { + _init() { + super._init(0.0, 'Workspaces bar'); + + // define gsettings schema for workspaces names, get workspaces names, signal for settings key changed + this.workspaces_settings = new Gio.Settings({ schema: WORKSPACES_SCHEMA }); + this.workspaces_names_changed = this.workspaces_settings.connect(`changed::${WORKSPACES_KEY}`, this._update_workspaces_names.bind(this)); + + // hide Activities button + this._show_activities(false); + + // bar creation + this.ws_bar = new St.BoxLayout({}); + this._update_workspaces_names(); + this.add_child(this.ws_bar); + + // signals for workspaces state: active workspace, number of workspaces + this._ws_active_changed = global.workspace_manager.connect('active-workspace-changed', this._update_ws.bind(this)); + this._ws_number_changed = global.workspace_manager.connect('notify::n-workspaces', this._update_ws.bind(this)); + this._restacked = global.display.connect('restacked', this._update_ws.bind(this)); + this._windows_changed = Shell.WindowTracker.get_default().connect('tracked-windows-changed', this._update_ws.bind(this)); + } + + // remove signals, restore Activities button, destroy workspaces bar + _destroy() { + this._show_activities(true); + if (this._ws_active_changed) { + global.workspace_manager.disconnect(this._ws_active_changed); + } + if (this._ws_number_changed) { + global.workspace_manager.disconnect(this._ws_number_changed); + } + if (this._restacked) { + global.display.disconnect(this._restacked); + } + if (this._windows_changed) { + Shell.WindowTracker.get_default().disconnect(this._windows_changed); + } + if (this.workspaces_names_changed) { + this.workspaces_settings.disconnect(this.workspaces_names_changed); + } + this.ws_bar.destroy(); + super.destroy(); + } + + // hide Activities button + _show_activities(show) { + this.activities_button = Main.panel.statusArea['activities']; + if (this.activities_button) { + if (show && !Main.sessionMode.isLocked) { + this.activities_button.container.show(); + } else { + this.activities_button.container.hide(); + } + } + } + + // update workspaces names + _update_workspaces_names() { + this.workspaces_names = this.workspaces_settings.get_strv(WORKSPACES_KEY); + this._update_ws(); + } + + // update the workspaces bar + _update_ws() { + // destroy old workspaces bar buttons + this.ws_bar.destroy_all_children(); + + // get number of workspaces + this.ws_count = global.workspace_manager.get_n_workspaces(); + this.active_ws_index = global.workspace_manager.get_active_workspace_index(); + + // display all current workspaces buttons + for (let ws_index = 0; ws_index < this.ws_count; ++ws_index) { + this.ws_box = new St.Bin({visible: true, reactive: true, can_focus: true, track_hover: true}); + this.ws_box.label = new St.Label({y_align: Clutter.ActorAlign.CENTER}); + if (ws_index == this.active_ws_index) { + if (global.workspace_manager.get_workspace_by_index(ws_index).n_windows > 0) { + this.ws_box.label.style_class = 'desktop-label-nonempty-active'; + } else { + this.ws_box.label.style_class = 'desktop-label-empty-active'; + } + } else { + if (global.workspace_manager.get_workspace_by_index(ws_index).n_windows > 0) { + this.ws_box.label.style_class = 'desktop-label-nonempty-inactive'; + } else { + this.ws_box.label.style_class = 'desktop-label-empty-inactive'; + } + } + if (this.workspaces_names[ws_index]) { + this.ws_box.label.set_text(" " + this.workspaces_names[ws_index] + " "); + } else { + this.ws_box.label.set_text(" " + (ws_index + 1) + " "); + } + this.ws_box.set_child(this.ws_box.label); + this.ws_box.connect('button-release-event', () => this._toggle_ws(ws_index) ); + this.ws_bar.add_actor(this.ws_box); + } + } + + // activate workspace or show overview + _toggle_ws(ws_index) { + if (global.workspace_manager.get_active_workspace_index() == ws_index) { + Main.overview.toggle(); + } else { + global.workspace_manager.get_workspace_by_index(ws_index).activate(global.get_current_time()); + } + } +}); + +class Extension { + constructor() { + } + + enable() { + this.workspaces_bar = new WorkspacesBar(); + Main.panel.addToStatusArea('workspaces-bar', this.workspaces_bar, 0, 'left'); + } + + disable() { + this.workspaces_bar._destroy(); + } +} + +function init() { + return new Extension(); +} + diff --git a/.local/share/gnome-shell/extensions/workspaces-bar@fthx/metadata.json b/.local/share/gnome-shell/extensions/workspaces-bar@fthx/metadata.json new file mode 100644 index 0000000..44e6a8c --- /dev/null +++ b/.local/share/gnome-shell/extensions/workspaces-bar@fthx/metadata.json @@ -0,0 +1,14 @@ +{ + "_generated": "Generated by SweetTooth, do not edit", + "description": "Replace 'Activities' button by all current workspaces buttons. Switch workspace or toggle overview by clicking on these buttons.\n\n You can use names for workspaces: there are two ways for that. 1) Edit the string array 'org.gnome.desktop.wm.preferences.workspace-names' gsettings key (through dconf editor, e.g.). 2) Use official GNOME extension Workspaces Indicator's settings. You don't have to write a long enough list: numbers are displayed if no workspace name is defined.", + "name": "Workspaces Bar", + "shell-version": [ + "3.36", + "3.38", + "40", + "41" + ], + "url": "https://github.com/fthx/workspaces-bar", + "uuid": "workspaces-bar@fthx", + "version": 12 +} \ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/workspaces-bar@fthx/stylesheet.css b/.local/share/gnome-shell/extensions/workspaces-bar@fthx/stylesheet.css new file mode 100644 index 0000000..1e0a55a --- /dev/null +++ b/.local/share/gnome-shell/extensions/workspaces-bar@fthx/stylesheet.css @@ -0,0 +1,67 @@ +/* framed version */ + +/*.desktop-label-nonempty-active { + margin-left: 0px; + margin-right: 8px; + background-color: rgba(128, 128, 128, 0.7); + color: rgba(207, 207, 207, 1); + border: 1px solid rgba(207, 207, 207, 0.7); + border-radius: 4px; +} + +.desktop-label-nonempty-inactive { + margin-left: 0px; + margin-right: 8px; + background-color: rgba(76, 76, 76, 0.7); + color: rgba(207, 207, 207, 0.7); + border: 1px solid rgba(207, 207, 207, 0.7); + border-radius: 4px; +} + +.desktop-label-empty-active { + margin-left: 0px; + margin-right: 8px; + background-color: rgba(128, 128, 128, 0.7); + color: rgba(207, 207, 207, 1); + border: 1px solid rgba(207, 207, 207, 0); + border-radius: 4px; +} + +.desktop-label-empty-inactive { + margin-left: 0px; + margin-right: 8px; + background-color: rgba(76, 76, 76, 0.7); + color: rgba(207, 207, 207, 0.7); + border: 1px solid rgba(207, 207, 207, 0); + border-radius: 4px; +}*/ + +/* non-framed version */ + +.desktop-label-nonempty-active { + margin-left: 0px; + margin-right: 8px; + background-color: rgba(154, 154, 154, 0.7); + border-radius: 4px; +} + +.desktop-label-nonempty-inactive { + margin-left: 0px; + margin-right: 8px; + background-color: rgba(96, 96, 96, 0.7); + border-radius: 4px; +} + +.desktop-label-empty-active { + margin-left: 0px; + margin-right: 8px; + background-color: rgba(154, 154, 154, 0.7); + border-radius: 4px; +} + +.desktop-label-empty-inactive { + margin-left: 0px; + margin-right: 8px; + background-color: rgba(0, 0, 0, 0.0); + border-radius: 4px; +} diff --git a/.local/share/gnome-shell/gnome-overrides-migrated b/.local/share/gnome-shell/gnome-overrides-migrated new file mode 100644 index 0000000..e69de29