14 Commits
68.5 ... master

Author SHA1 Message Date
69343573af Add basic app menu 2026-04-04 06:01:59 -07:00
de270cf9f5 update to upstream 2026-04-03 15:18:24 -07:00
0ff76b8c88 Fix permission problems, and blank default ui 2026-03-29 17:00:31 -07:00
b63f158459 Combine Zorin-Taskbar and Gtk4 Desktop Icons NG into 1 plugin 2026-03-29 03:42:51 -07:00
Artyom Zorin
8b34d5f144 Bump to version 70.1.1 2025-09-29 18:09:52 +01:00
Artyom Zorin
a40d94c14b Bump to version 70 2025-09-28 00:36:00 +01:00
Artyom Zorin
541f5f20da Bump version to 69.2 2025-09-16 16:06:38 +01:00
Artyom Zorin
250d988473 Bump to version 69.1 2025-09-15 16:45:47 +01:00
Artyom Zorin
41f92619bf Bump to version 69 2025-09-12 14:16:30 +01:00
Artyom Zorin
388febf2fd Bump to version 68.9 2025-09-08 21:42:14 +01:00
Artyom Zorin
dcec9442c2 Bump to version 68.8 2025-09-05 17:47:05 +01:00
Artyom Zorin
89fe8b9f4b Bump to version 68.7 2025-09-04 14:25:37 +01:00
Artyom Zorin
a4f86d41f8 Bump to version 68.5.3 2025-08-07 00:11:11 +01:00
Artyom Zorin
582b8b280b Bump to version 68.5.1 2025-08-05 22:50:54 +01:00
182 changed files with 153430 additions and 1977 deletions

4
.gitignore vendored
View File

@@ -1,8 +1,8 @@
.~
*~
gschemas.compiled
zorin-taskbar@zorinos.com*.zip
vesperos-taskbar@oxmc.me*.zip
*.mo
po/zorin-taskbar.pot
po/vesperos-taskbar.pot
ui/*.ui.h
node_modules/

View File

@@ -1,7 +1,8 @@
# Basic Makefile
UUID = zorin-taskbar@zorinos.com
UUID = vesperos-taskbar@oxmc.me
MODULES = src/*.js src/stylesheet.css metadata.json COPYING README.md
DING_MODULES = ding/dingManager.js ding/gnomeShellOverride.js ding/emulateX11WindowType.js ding/visibleArea.js
UI_MODULES = ui/*.ui
IMAGES = ./*
@@ -15,13 +16,13 @@ else
INSTALLBASE = $(DESTDIR)/usr/share/gnome-shell/extensions
SHARE_PREFIX = $(DESTDIR)/usr/share
endif
INSTALLNAME = zorin-taskbar@zorinos.com
INSTALLNAME = vesperos-taskbar@oxmc.me
# The command line passed variable VERSION is used to set the version string
# in the metadata and in the generated zip-file.
ifdef VERSION
else
VERSION = 65
VERSION = 71
endif
ifdef TARGET
@@ -37,27 +38,27 @@ clean:
extension: ./schemas/gschemas.compiled $(MSGSRC:.po=.mo)
./schemas/gschemas.compiled: ./schemas/org.gnome.shell.extensions.zorin-taskbar.gschema.xml
./schemas/gschemas.compiled: ./schemas/org.gnome.shell.extensions.vesperos-taskbar.gschema.xml
glib-compile-schemas ./schemas/
potfile: ./po/zorin-taskbar.pot
potfile: ./po/vesperos-taskbar.pot
mergepo: potfile
for l in $(MSGSRC); do \
msgmerge -U $$l ./po/zorin-taskbar.pot; \
msgmerge -U $$l ./po/vesperos-taskbar.pot; \
done;
./po/zorin-taskbar.pot: $(TOLOCALIZE)
./po/vesperos-taskbar.pot: $(TOLOCALIZE)
mkdir -p po
xgettext -k_ -kN_ -o po/zorin-taskbar.pot --package-name "Zorin Taskbar" $(TOLOCALIZE) --from-code=UTF-8
xgettext -k_ -kN_ -o po/vesperos-taskbar.pot --package-name "Vesperos Taskbar" $(TOLOCALIZE) --from-code=UTF-8
for l in $(UI_MODULES) ; do \
intltool-extract --type=gettext/glade $$l; \
xgettext -k_ -kN_ -o po/zorin-taskbar.pot $$l.h --join-existing --from-code=UTF-8; \
xgettext -k_ -kN_ -o po/vesperos-taskbar.pot $$l.h --join-existing --from-code=UTF-8; \
rm -rf $$l.h; \
done;
sed -i -e 's/&\#10;/\\n/g' po/zorin-taskbar.pot
sed -i -e 's/&\#10;/\\n/g' po/vesperos-taskbar.pot
./po/%.mo: ./po/%.po
msgfmt -c $< -o $@
@@ -100,7 +101,7 @@ _build: all
lf=_build/locale/`basename $$l .mo`; \
mkdir -p $$lf; \
mkdir -p $$lf/LC_MESSAGES; \
cp $$l $$lf/LC_MESSAGES/zorin-taskbar.mo; \
cp $$l $$lf/LC_MESSAGES/vesperos-taskbar.mo; \
done;
ifneq ($(and $(COMMIT),$(VERSION)),)
sed -i 's/"version": [[:digit:]][[:digit:]]*/"version": $(VERSION),\n"commit": "$(COMMIT)"/' _build/metadata.json;

18
NOTICE.txt Normal file
View File

@@ -0,0 +1,18 @@
NOTICE — Third-Party Source Acknowledgement
===========================================
This package (vesperos-taskbar) is a fork of
gnome-shell-extension-zorin-taskbar, originally created and maintained by
Artyom Zorin <azorin@zoringroup.com> for Zorin OS.
Original source: https://github.com/ZorinOS/gnome-shell-extension-zorin-taskbar
Original licence: GPL-2.0, Copyright Zorin Group Ltd.
The Zorin Taskbar extension is itself based on Dash to Panel, developed by
the Dash to Panel contributors and licensed under GPL-2.0.
The VesperOS modifications, branding, and additions are
Copyright 2025-2026 VesperOS Desktop Team <contact@oxmc.me>
and are distributed under the same GPL-2.0 license.
For full copyright and licence details see COPYING.

View File

@@ -1,4 +1,6 @@
# Zorin Taskbar
The official taskbar for Zorin OS.
# VesperOS Taskbar
The official taskbar for VesperOS.
Bundles the taskbar panel and [Gtk4 Desktop Icons NG (DING)](https://gitlab.com/smedius/desktop-icons-ng) into a single package.
Re-based on the [Dash to Panel](https://github.com/home-sweet-gnome/dash-to-panel) GNOME Shell extension. Dash to Panel was initially based on the original version of Zorin Taskbar from 2016, with some code derived from the [Dash to Dock](https://github.com/micheleg/dash-to-dock) extension by micheleg.

587
debian/changelog vendored
View File

@@ -1,529 +1,58 @@
gnome-shell-extension-zorin-taskbar (68.5) noble; urgency=medium
* Rebased on upstream commit 16e16c11ce08abc3c9f0bf922bbc08e17b2c1f08
-- Artyom Zorin <azorin@zoringroup.com> Mon, 04 Aug 2025 13:46:11 +0100
gnome-shell-extension-zorin-taskbar (68.4) noble; urgency=medium
* Applied monitor selection and reset geometry fixes
-- Artyom Zorin <azorin@zoringroup.com> Thu, 31 Jul 2025 19:40:58 +0100
gnome-shell-extension-zorin-taskbar (68.3) noble; urgency=medium
* Removed code to handle overview startup animation
-- Artyom Zorin <azorin@zoringroup.com> Mon, 07 Jul 2025 12:56:41 +0100
gnome-shell-extension-zorin-taskbar (68.2.3) noble; urgency=medium
* Fixed logic error when adjusting panel menu buttons
-- Artyom Zorin <azorin@zoringroup.com> Fri, 04 Jul 2025 20:36:32 +0100
gnome-shell-extension-zorin-taskbar (68.2.2) noble; urgency=medium
* Added settings schema to metadata file
-- Artyom Zorin <azorin@zoringroup.com> Fri, 06 Jun 2025 23:03:34 +0100
gnome-shell-extension-zorin-taskbar (68.2.1) noble; urgency=medium
* Updated debian control file
-- Artyom Zorin <azorin@zoringroup.com> Fri, 23 May 2025 20:05:49 +0100
gnome-shell-extension-zorin-taskbar (68.2) noble; urgency=medium
* Corrected code to detect Tiling Shell gap offset
-- Artyom Zorin <azorin@zoringroup.com> Sat, 10 May 2025 23:52:12 +0100
gnome-shell-extension-zorin-taskbar (68.1) noble; urgency=medium
* Adjusted app icon margins and padding
-- Artyom Zorin <azorin@zoringroup.com> Fri, 02 May 2025 14:19:18 +0100
gnome-shell-extension-zorin-taskbar (68) noble; urgency=medium
* Re-based on upstream version 68
-- Artyom Zorin <azorin@zoringroup.com> Tue, 29 Apr 2025 20:36:02 +0100
gnome-shell-extension-zorin-taskbar (65.3) noble; urgency=medium
* Changed activities button default position
-- Artyom Zorin <azorin@zoringroup.com> Sun, 02 Mar 2025 19:39:09 +0000
gnome-shell-extension-zorin-taskbar (65.2) noble; urgency=medium
* Separated floating rounded theme from intellihide as an independent
styling option
-- Artyom Zorin <azorin@zoringroup.com> Thu, 27 Feb 2025 13:57:03 +0000
gnome-shell-extension-zorin-taskbar (65.1) noble; urgency=medium
* Fixed various bugs
-- Artyom Zorin <azorin@zoringroup.com> Wed, 26 Feb 2025 12:20:34 +0000
gnome-shell-extension-zorin-taskbar (65) noble; urgency=medium
* Re-based on upstream version 65
-- Artyom Zorin <azorin@zoringroup.com> Tue, 25 Feb 2025 22:29:43 +0000
gnome-shell-extension-zorin-taskbar (56.11) jammy; urgency=medium
* Updated French translations
-- Artyom Zorin <azorin@zoringroup.com> Sun, 20 Oct 2024 17:58:19 +0100
gnome-shell-extension-zorin-taskbar (56.10) jammy; urgency=medium
* Bug fix for window previews
-- Artyom Zorin <azorin@zoringroup.com> Mon, 09 Sep 2024 17:44:12 +0100
gnome-shell-extension-zorin-taskbar (56.9) jammy; urgency=medium
* Increased window preview leave timeout to 250ms
-- Artyom Zorin <azorin@zoringroup.com> Tue, 03 Sep 2024 14:04:19 +0100
gnome-shell-extension-zorin-taskbar (56.8) jammy; urgency=medium
* Fixed barrier code
-- Artyom Zorin <azorin@zoringroup.com> Mon, 12 Aug 2024 23:38:18 +0100
gnome-shell-extension-zorin-taskbar (56.7) jammy; urgency=medium
* Added link to Application Switching settings to set workspace and
monitor isolation behaviour
-- Artyom Zorin <azorin@zoringroup.com> Wed, 15 May 2024 19:21:08 +0100
gnome-shell-extension-zorin-taskbar (56.6) jammy; urgency=medium
* Correctly handle desktop icons when calculating proximity
-- Artyom Zorin <azorin@zoringroup.com> Tue, 27 Feb 2024 20:11:37 +0000
gnome-shell-extension-zorin-taskbar (56.5) jammy; urgency=medium
* Fixed show desktop functionality
-- Artyom Zorin <azorin@zoringroup.com> Wed, 13 Dec 2023 17:22:08 +0000
gnome-shell-extension-zorin-taskbar (56.4) jammy; urgency=medium
* Fixed regression with shortcuts overlay
-- Artyom Zorin <azorin@zoringroup.com> Wed, 13 Dec 2023 16:21:07 +0000
gnome-shell-extension-zorin-taskbar (56.3) jammy; urgency=medium
* Adjusted shortcut-num-keys default setting
-- Artyom Zorin <azorin@zoringroup.com> Tue, 12 Dec 2023 22:21:55 +0000
gnome-shell-extension-zorin-taskbar (56.2) jammy; urgency=medium
* Fixed floating theme centering bug
-- Artyom Zorin <azorin@zoringroup.com> Sun, 10 Dec 2023 18:41:12 +0000
gnome-shell-extension-zorin-taskbar (56.1) jammy; urgency=medium
* Moved isolate settings to GNOME Control Center
-- Artyom Zorin <azorin@zoringroup.com> Sat, 18 Nov 2023 19:23:50 +0000
gnome-shell-extension-zorin-taskbar (56.0.2) jammy; urgency=medium
* Removed blue background on favorite apps when dragging app icons
-- Artyom Zorin <azorin@zoringroup.com> Wed, 01 Nov 2023 12:39:55 +0000
gnome-shell-extension-zorin-taskbar (56.0.1) jammy; urgency=medium
* Corrected Makefile
-- Artyom Zorin <azorin@zoringroup.com> Wed, 31 May 2023 01:23:02 +0100
gnome-shell-extension-zorin-taskbar (56) jammy; urgency=medium
* Re-based on upstream version 56 as at commit
9274982189f2d5306afaf29f274d007f0cd12d48
-- Artyom Zorin <azorin@zoringroup.com> Wed, 31 May 2023 00:14:18 +0100
gnome-shell-extension-zorin-taskbar (40.23) focal; urgency=medium
* Changed default window preview size to 200px
-- Artyom Zorin <azorin@zoringroup.com> Sat, 16 Oct 2021 12:44:27 +0100
gnome-shell-extension-zorin-taskbar (40.22) focal; urgency=medium
* Fixed Spanish translations
-- Artyom Zorin <azorin@zoringroup.com> Sun, 22 Aug 2021 16:45:29 +0100
gnome-shell-extension-zorin-taskbar (40.21) focal; urgency=medium
* Updated Spanish translations
-- Artyom Zorin <azorin@zoringroup.com> Sun, 22 Aug 2021 15:49:06 +0100
gnome-shell-extension-zorin-taskbar (40.20) focal; urgency=medium
* Corrected translation string
-- Artyom Zorin <azorin@zoringroup.com> Tue, 27 Jul 2021 22:37:26 +0100
gnome-shell-extension-zorin-taskbar (40.19) focal; urgency=medium
* Corrected translation strings
-- Artyom Zorin <azorin@zoringroup.com> Tue, 27 Jul 2021 22:35:25 +0100
gnome-shell-extension-zorin-taskbar (40.18) focal; urgency=medium
* Updated translations
-- Artyom Zorin <azorin@zoringroup.com> Tue, 27 Jul 2021 22:29:20 +0100
gnome-shell-extension-zorin-taskbar (40.17) focal; urgency=medium
* Fixed notification badge sizing on 200% scaled displays
-- Artyom Zorin <azorin@zoringroup.com> Wed, 21 Jul 2021 13:00:25 +0100
gnome-shell-extension-zorin-taskbar (40.16) focal; urgency=medium
* Fixed bug that caused the panel to disappear after locking the
screen while fullscreen content is playing
-- Artyom Zorin <azorin@zoringroup.com> Wed, 02 Jun 2021 20:24:27 +0100
gnome-shell-extension-zorin-taskbar (40.15) focal; urgency=medium
* Updated translations
-- Artyom Zorin <azorin@zoringroup.com> Sun, 23 May 2021 20:32:41 +0100
gnome-shell-extension-zorin-taskbar (40.14) focal; urgency=medium
* Fixed new translations
-- Artyom Zorin <azorin@zoringroup.com> Sun, 23 May 2021 20:26:18 +0100
gnome-shell-extension-zorin-taskbar (40.13) focal; urgency=medium
* Added new translations and made the main panel always appear on the
primary display
-- Artyom Zorin <azorin@zoringroup.com> Sun, 23 May 2021 20:03:46 +0100
gnome-shell-extension-zorin-taskbar (40.12) focal; urgency=medium
* Updated Russian and Japanese translations
-- Artyom Zorin <azorin@zoringroup.com> Wed, 05 May 2021 20:10:48 +0100
gnome-shell-extension-zorin-taskbar (40.11) focal; urgency=medium
* No longer animates disposed icons
-- Artyom Zorin <azorin@zoringroup.com> Tue, 06 Apr 2021 13:13:47 +0100
gnome-shell-extension-zorin-taskbar (40.10) focal; urgency=medium
* Added Zorin Appearance link to taskbar right-click menu
-- Artyom Zorin <azorin@zoringroup.com> Tue, 23 Mar 2021 19:36:07 +0000
gnome-shell-extension-zorin-taskbar (40.9) focal; urgency=medium
* Removed Terminal from right-click menu
-- Artyom Zorin <azorin@zoringroup.com> Mon, 22 Mar 2021 15:38:20 +0000
gnome-shell-extension-zorin-taskbar (40.8) focal; urgency=medium
* Removed dot style settings and moved Intellihide to Style tab in
prefs
-- Artyom Zorin <azorin@zoringroup.com> Sun, 21 Mar 2021 15:08:03 +0000
gnome-shell-extension-zorin-taskbar (40.7) focal; urgency=medium
* Removed style override for app-well-app items
-- Artyom Zorin <azorin@zoringroup.com> Sun, 21 Feb 2021 20:00:12 +0000
gnome-shell-extension-zorin-taskbar (40.6) focal; urgency=medium
* Improved styling of progress bars
-- Artyom Zorin <azorin@zoringroup.com> Sun, 21 Feb 2021 15:54:07 +0000
gnome-shell-extension-zorin-taskbar (40.5) focal; urgency=medium
* Re-based on upstream as at commit
e4a71fa014b565171c93d15f436be9c3599b11fb
-- Artyom Zorin <azorin@zoringroup.com> Sun, 21 Feb 2021 15:22:52 +0000
gnome-shell-extension-zorin-taskbar (40.4) focal; urgency=medium
* Updated notification badge overlay and limited minimum panel size to
24px
-- Artyom Zorin <azorin@zoringroup.com> Sun, 21 Feb 2021 15:02:09 +0000
gnome-shell-extension-zorin-taskbar (40.3) focal; urgency=medium
* Increased border radius of floating panel and preview container
-- Artyom Zorin <azorin@zoringroup.com> Sun, 21 Feb 2021 00:50:25 +0000
gnome-shell-extension-zorin-taskbar (40.2) focal; urgency=medium
* Imporved visibility of window previews by styling them with the dash-
label class
-- Artyom Zorin <azorin@zoringroup.com> Thu, 31 Dec 2020 18:34:46 +0000
gnome-shell-extension-zorin-taskbar (40.1) focal; urgency=medium
* Added floating rounded theme when using Intellihide
-- Artyom Zorin <azorin@zoringroup.com> Wed, 30 Dec 2020 00:44:32 +0000
gnome-shell-extension-zorin-taskbar (40) focal; urgency=medium
* Re-based on upstream version 40 as at commit
48a69e529614d1da456802b818e7d7f0d4d1d642
-- Artyom Zorin <azorin@zoringroup.com> Mon, 28 Dec 2020 22:08:11 +0000
gnome-shell-extension-zorin-taskbar (2.0.11) bionic; urgency=medium
* Set variables to 0 on destroy in taskbar.js
-- Artyom Zorin <azorin@zoringroup.com> Tue, 19 Feb 2019 18:35:40 +0000
gnome-shell-extension-zorin-taskbar (2.0.10) bionic; urgency=medium
* Fixed bugs with windowPreview peek mode
-- Artyom Zorin <azorin@zoringroup.com> Fri, 15 Feb 2019 00:13:19 +0000
gnome-shell-extension-zorin-taskbar (2.0.9) bionic; urgency=medium
* Fixed touch support in Gnome Shell 3.30 and made touching an app
icon show its window preview if more than one window is opened
-- Artyom Zorin <azorin@zoringroup.com> Thu, 14 Feb 2019 13:27:40 +0000
gnome-shell-extension-zorin-taskbar (2.0.8) bionic; urgency=medium
* Added definition check when getting taskbar icons
-- Artyom Zorin <azorin@zoringroup.com> Tue, 05 Feb 2019 18:17:32 +0000
gnome-shell-extension-zorin-taskbar (2.0.7) bionic; urgency=medium
* Fixed _dragInfo definition check
-- Artyom Zorin <azorin@zoringroup.com> Tue, 05 Feb 2019 14:45:45 +0000
gnome-shell-extension-zorin-taskbar (2.0.6) bionic; urgency=medium
* Fixed name of Taskbar Actor
-- Artyom Zorin <azorin@zoringroup.com> Sat, 12 Jan 2019 18:35:19 +0000
gnome-shell-extension-zorin-taskbar (2.0.5) bionic; urgency=medium
* Re-based on Dash to Panel as at commit
b6094fdaec89349cc6f3e0da887d19fdf3db1c60
-- Artyom Zorin <azorin@zoringroup.com> Sat, 12 Jan 2019 16:02:44 +0000
gnome-shell-extension-zorin-taskbar (2.0.4) bionic; urgency=medium
* Re-based on Dash to Panel as at commit
6e53889082eef4eed9cdc1c496e90a6f8450d1fd
-- Artyom Zorin <azorin@zoringroup.com> Fri, 11 Jan 2019 16:41:36 +0000
gnome-shell-extension-zorin-taskbar (2.0.3) bionic; urgency=medium
* Re-based on Dash to Panel as at commit
8e715c7b07d30bfe0858a1eb93638c653b8bd268
-- Artyom Zorin <azorin@zoringroup.com> Tue, 08 Jan 2019 18:46:52 +0000
gnome-shell-extension-zorin-taskbar (2.0.2) bionic; urgency=medium
* Re-based on Dash to Panel as at commit
dcd8a017e2a9ae66518ade2ae7a74d9836dd3633
-- Artyom Zorin <azorin@zoringroup.com> Thu, 03 Jan 2019 14:27:47 +0000
gnome-shell-extension-zorin-taskbar (2.0.1) bionic; urgency=medium
* Updated URL in metadata.json
-- Artyom Zorin <azorin@zoringroup.com> Mon, 31 Dec 2018 14:27:13 +0000
gnome-shell-extension-zorin-taskbar (2.0) bionic; urgency=medium
* Re-based on Dash to Panel commit
e2eeb0290152bdf9ea3a9643ce6d36d8ba12813d
-- Artyom Zorin <azorin@zoringroup.com> Sun, 30 Dec 2018 18:44:22 +0000
gnome-shell-extension-zorin-taskbar (1.4.4) xenial; urgency=medium
* Re-based on Dash to Panel version 13
-- Artyom Zorin <azorin@zoringroup.com> Wed, 07 Mar 2018 15:16:36 +0000
gnome-shell-extension-zorin-taskbar (1.4.3) xenial; urgency=medium
* Fixed window preview issue with Remmina
-- Artyom Zorin <azorin@zoringroup.com> Tue, 06 Mar 2018 22:37:07 +0000
gnome-shell-extension-zorin-taskbar (1.4.2) xenial; urgency=medium
* Various bug fixes
-- Artyom Zorin <azorin@zoringroup.com> Tue, 06 Mar 2018 20:44:12 +0000
gnome-shell-extension-zorin-taskbar (1.4.1) xenial; urgency=medium
* Added more required imports
-- Artyom Zorin <azorin@zoringroup.com> Tue, 06 Mar 2018 11:22:48 +0000
gnome-shell-extension-zorin-taskbar (1.4) xenial; urgency=medium
* Re-based on Dash to Panel version 12
-- Artyom Zorin <azorin@zoringroup.com> Tue, 06 Mar 2018 01:25:06 +0000
gnome-shell-extension-zorin-taskbar (1.3) xenial; urgency=medium
* Added opacify peek on window preview hover
-- Artyom Zorin <azorin@zoringroup.com> Fri, 28 Jul 2017 00:40:19 +0100
gnome-shell-extension-zorin-taskbar (1.2.1) xenial; urgency=medium
* Removed window preview opening animation
-- Artyom Zorin <azorin@zoringroup.com> Fri, 14 Apr 2017 20:05:44 +0100
gnome-shell-extension-zorin-taskbar (1.2) xenial; urgency=medium
* Re-based on Dash to Panel commit
1415cbdf5cadff94f4d9483b4b77676a3a2ea8d1
-- Artyom Zorin <azorin@zoringroup.com> Tue, 11 Apr 2017 22:16:41 +0100
gnome-shell-extension-zorin-taskbar (1.1.5) xenial; urgency=medium
* Enabled opening animations for window previews
-- Artyom Zorin <azorin@zoringroup.com> Thu, 12 Jan 2017 11:54:50 +0000
gnome-shell-extension-zorin-taskbar (1.1.4) xenial; urgency=medium
* App running indicators now appear on top when the panel is on top
and bug fixes
-- Artyom Zorin <azorin@zoringroup.com> Wed, 11 Jan 2017 12:46:23 +0000
gnome-shell-extension-zorin-taskbar (1.1.3) xenial; urgency=medium
* Fixed another high CPU usage issue credit to jderose9
-- Artyom Zorin <azorin@zoringroup.com> Tue, 10 Jan 2017 22:01:51 +0000
gnome-shell-extension-zorin-taskbar (1.1.2) xenial; urgency=medium
* Reduced CPU usage credit to jderose9  and improved the
responsiveness of DPI changes
-- Artyom Zorin <azorin@zoringroup.com> Tue, 10 Jan 2017 12:40:58 +0000
gnome-shell-extension-zorin-taskbar (1.1.1) xenial; urgency=medium
* Removed window preview menu enter timeout to fix keygrab focus
lockup bug and make the taskbar experience faster
-- Artyom Zorin <azorin@zoringroup.com> Sun, 08 Jan 2017 20:02:26 +0000
gnome-shell-extension-zorin-taskbar (1.1) xenial; urgency=medium
* Added full support for HiDPI displays
-- Artyom Zorin <azorin@zoringroup.com> Mon, 02 Jan 2017 00:28:46 +0000
gnome-shell-extension-zorin-taskbar (1.0.6) xenial; urgency=medium
* Fixed a number of memory leaks in the Window Preview code
-- Artyom Zorin <azorin@zoringroup.com> Tue, 27 Dec 2016 15:53:08 +0000
gnome-shell-extension-zorin-taskbar (1.0.5) xenial; urgency=medium
* Updated copyright notices
-- Artyom Zorin <azorin@zoringroup.com> Fri, 04 Nov 2016 14:34:48 +0000
gnome-shell-extension-zorin-taskbar (1.0.4) xenial; urgency=medium
* Disconnected signals
-- Artyom Zorin <azorin@zoringroup.com> Sun, 23 Oct 2016 13:43:35 +0100
gnome-shell-extension-zorin-taskbar (1.0.3) xenial; urgency=medium
* Fixed Work ID issue
-- Artyom Zorin <azorin@zoringroup.com> Thu, 20 Oct 2016 22:59:07 +0100
gnome-shell-extension-zorin-taskbar (1.0.2) xenial; urgency=medium
* Ready for public use in Zorin OS 12
-- Artyom Zorin <azorin@zoringroup.com> Sun, 18 Sep 2016 20:24:18 +0100
gnome-shell-extension-zorin-taskbar (1.0.1) xenial; urgency=medium
* Updated copyright notice
-- Zorin OS <os@zoringroup.com> Mon, 12 Sep 2016 19:23:04 +0100
gnome-shell-extension-zorin-taskbar (1.0) xenial; urgency=medium
* Initial stable release
-- Zorin OS <os@zoringroup.com> Sat, 03 Sep 2016 23:19:10 +0100
gnome-shell-extension-zorin-taskbar (0.9) xenial; urgency=low
* Pre-release
-- Zorin OS <os@zoringroup.com> Fri, 02 Sep 2016 10:47:51 -0400
vesperos-taskbar (26.3) vesperos; urgency=medium
* Add Windows 10-style Start Menu replacing the GNOME app grid popup.
- Clicking the Show Apps button now opens a floating Win10-style menu
instead of the GNOME overview app grid.
- Includes a search bar that filters installed applications in real time.
- "Pinned" view shows favourites + running apps in a 4-column icon grid.
- "All apps" view shows an alphabetically sorted, scrollable app list
with letter dividers.
- Footer row shows the current user name and a power button with a
submenu for Lock, Sign out, Sleep, Restart, and Shut down.
- Menu is positioned above/beside the Show Apps button depending on
panel side; clamped to monitor bounds on multi-monitor setups.
- Closes on Escape key, outside click, or when the GNOME overview opens.
- Right-click context menu on the Show Apps button is unchanged.
-- VesperOS Desktop Team <contact@oxmc.me> Sat, 04 Apr 2026 12:00:00 +0000
vesperos-taskbar (26.2) vesperos; urgency=medium
* Re-based on upstream version 73.1 (additional bug fixes).
* Fix crash in activateFirstWindow when window list is empty.
* Fix memory leaks: properly destroy menus in TaskbarAppIcon and
ShowAppsIconWrapper on extension disable.
* Fix extension enable stalling when Zorin Dash is already disabled.
* Fix intellihide panel visibility logic to avoid unintended hide.
* Fix overview disable to end hotkey preview cycle before teardown.
* Fix null-dereference on intellihide reference in panel toggle handler.
* Fix show-desktop button removal to clean up pending timeout and
restore hidden workspace state before destroying the button.
* Fix panelManager cleanup of boxPointer signal ID on disable.
* Fix Workspace prototype not restored when extension is disabled
while the overview spread is active.
* Fix panel.outerSize reference to panel.geom.outerSize.
* Guard _newLookingGlassResize against missing primary monitor panel.
* Fix taskbar destroy to clean up _onStageKeyPress override when the
overview is open at disable time.
* Fix workspace signal disconnect to tolerate removed dynamic workspaces.
* Fix menu-state-changed signal connected before item container exists.
* Fix animateWindowOpacity reading initialOpacity before window
actor reassignment.
* Fix ColorUtils RGB-to-HSV conversion variable shadowing bug.
* Fix windowPreview workspace.activate to always restore _shouldAnimate
via try/finally.
* Fix windowPreview set_child_at_index with null parent guard and
index clamping.
-- VesperOS Desktop Team <contact@oxmc.me> Fri, 03 Apr 2026 15:30:00 +0000
vesperos-taskbar (26.1) vesperos; urgency=medium
* VesperOS fork of Zorin OS's gnome-shell-extension-zorin-taskbar package.
* Re-based on upstream version 73.
* Replaced Zorin OS branding with VesperOS identity.
* Bundled Gtk4 Desktop Icons NG (DING) into the same package.
* Consolidated GSettings schemas for combined install.
-- VesperOS Desktop Team <contact@oxmc.me> Fri, 03 Apr 2026 14:55:31 +0000

21
debian/control vendored
View File

@@ -1,13 +1,20 @@
Source: gnome-shell-extension-zorin-taskbar
Source: vesperos-taskbar
Section: gnome
Priority: optional
Maintainer: Artyom Zorin <azorin@zoringroup.com>
Build-Depends: debhelper-compat (= 13), libglib2.0-bin, zip
Maintainer: VesperOS Desktop Team <contact@oxmc.me>
Build-Depends: debhelper-compat (= 13), libglib2.0-bin, zip, gettext
Standards-Version: 4.6.0
Rules-Requires-Root: no
Package: gnome-shell-extension-zorin-taskbar
Package: vesperos-taskbar
Architecture: all
Depends: ${misc:Depends}, gnome-shell (>= 46), gnome-shell (<< 49~)
Description: Zorin Taskbar extension
A taskbar extension for the Zorin OS desktop.
Depends: ${misc:Depends}, gnome-shell (>= 46), gnome-shell (<< 51~), gir1.2-webkit-6.0
Conflicts: gnome-shell-extension-zorin-taskbar, gnome-shell-extension-desktop-icons-ng, gnome-shell-extension-gtk4-desktop-icons-ng
Replaces: gnome-shell-extension-zorin-taskbar, gnome-shell-extension-desktop-icons-ng, gnome-shell-extension-gtk4-desktop-icons-ng
Description: VesperOS system taskbar with desktop icons (all-in-one)
Bundles the Zorin Taskbar panel extension and the Gtk4 Desktop Icons NG
(DING) extension into a single package for the VesperOS desktop.
.
Installs two GNOME Shell extensions:
- vesperos-taskbar@oxmc.me (taskbar panel)
- gtk4-ding@smedius.gitlab.com (desktop icon management)

2
debian/copyright vendored
View File

@@ -1,5 +1,5 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: gnome-shell-extension-zorin-taskbar
Upstream-Name: vesperos-taskbar
Files: *
Copyright: 2016-2025, Jason DeRose (https://github.com/jderose9)

3
debian/install vendored
View File

@@ -1 +1,2 @@
schemas/org.gnome.shell.extensions.zorin-taskbar.gschema.xml usr/share/glib-2.0/schemas
schemas/org.gnome.shell.extensions.vesperos-taskbar.gschema.xml usr/share/glib-2.0/schemas
schemas/org.gnome.shell.extensions.vesperos-taskbar.desktop-icons.gschema.xml usr/share/glib-2.0/schemas

62
debian/rules vendored
View File

@@ -1,9 +1,67 @@
#!/usr/bin/make -f
PKG_DIR = $(CURDIR)/debian/vesperos-taskbar
EXT_DIR = $(PKG_DIR)/usr/share/gnome-shell/extensions/vesperos-taskbar@oxmc.me
%:
dh $@
override_dh_auto_build:
# Compile vesperos-taskbar gschema + locale files
$(MAKE)
# Compile DING GResource bundle (output goes into ding/app/ for install)
glib-compile-resources \
--sourcedir=$(CURDIR)/ding/data \
--target=$(CURDIR)/ding/app/com.desktop.ding.data.gresource \
$(CURDIR)/ding/data/com.desktop.ding.data.gresource.xml
# Compile DING .po locale files to .mo
for po in $(CURDIR)/ding/po/*.po; do \
lang=$$(basename $$po .po); \
mkdir -p $(CURDIR)/ding/po/mo/$$lang/LC_MESSAGES; \
msgfmt -o $(CURDIR)/ding/po/mo/$$lang/LC_MESSAGES/gtk4-ding.mo $$po; \
done
# Process AppArmor profile template
sed 's|@PREFIX@|/usr|g' $(CURDIR)/ding/apparmor/gtk4-desktop-icons.in \
> $(CURDIR)/ding/apparmor/gtk4-desktop-icons
override_dh_install:
dh_install
rm -f debian/gnome-shell-extension-zorin-taskbar/usr/share/gnome-shell/extensions/zorin-taskbar@zorinos.com/COPYING
rm -f debian/gnome-shell-extension-zorin-taskbar/usr/share/gnome-shell/extensions/zorin-taskbar@zorinos.com/README.md
rm -f $(EXT_DIR)/COPYING
rm -f $(EXT_DIR)/README.md
# --- DING shell-side JS (in ding/ subdir so relative imports resolve) ---
mkdir -p $(EXT_DIR)/ding
install -m 644 $(CURDIR)/ding/dingManager.js $(EXT_DIR)/ding/
install -m 644 $(CURDIR)/ding/gnomeShellOverride.js $(EXT_DIR)/ding/
install -m 644 $(CURDIR)/ding/emulateX11WindowType.js $(EXT_DIR)/ding/
install -m 644 $(CURDIR)/ding/visibleArea.js $(EXT_DIR)/ding/
cp -r $(CURDIR)/ding/utils $(EXT_DIR)/ding/
cp -r $(CURDIR)/ding/dependencies $(EXT_DIR)/ding/
# --- DING GTK4 subprocess app/ (inside ding/ so all relative imports resolve) ---
cp -r $(CURDIR)/ding/app $(EXT_DIR)/ding/
chmod -R a+rX $(EXT_DIR)/ding/app
chmod +x $(EXT_DIR)/ding/app/adw-ding.js
# --- DING desktop entry ---
mkdir -p $(PKG_DIR)/usr/share/applications
install -m 644 $(CURDIR)/ding/data/com.desktop.ding.desktop \
$(PKG_DIR)/usr/share/applications/
# --- DING AppArmor profile ---
mkdir -p $(PKG_DIR)/etc/apparmor.d
install -m 644 $(CURDIR)/ding/apparmor/gtk4-desktop-icons \
$(PKG_DIR)/etc/apparmor.d/
# --- DING app icon ---
mkdir -p $(PKG_DIR)/usr/share/icons/hicolor/scalable/apps
install -m 644 $(CURDIR)/ding/data/icons/com.desktop.ding.svg \
$(PKG_DIR)/usr/share/icons/hicolor/scalable/apps/
# --- DING compiled locales ---
for lang_dir in $(CURDIR)/ding/po/mo/*/; do \
lang=$$(basename $$lang_dir); \
install -d $(PKG_DIR)/usr/share/locale/$$lang/LC_MESSAGES; \
install -m 644 $$lang_dir/LC_MESSAGES/gtk4-ding.mo \
$(PKG_DIR)/usr/share/locale/$$lang/LC_MESSAGES/; \
done
# --- Start Menu themes ---
cp -r $(CURDIR)/src/themes $(EXT_DIR)/themes
override_dh_fixperms:
dh_fixperms
chmod +x $(EXT_DIR)/ding/app/adw-ding.js

13
debian/vesperos-taskbar.postinst vendored Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
set -e
if [ "$1" = "configure" ]; then
if command -v glib-compile-schemas >/dev/null 2>&1; then
glib-compile-schemas /usr/share/glib-2.0/schemas/ || true
fi
if command -v apparmor_parser >/dev/null 2>&1 && [ -f /etc/apparmor.d/gtk4-desktop-icons ]; then
apparmor_parser -r /etc/apparmor.d/gtk4-desktop-icons || true
fi
fi
#DEBHELPER#

13
debian/vesperos-taskbar.postrm vendored Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
set -e
if [ "$1" = "remove" ] || [ "$1" = "purge" ]; then
if command -v glib-compile-schemas >/dev/null 2>&1; then
glib-compile-schemas /usr/share/glib-2.0/schemas/ || true
fi
if command -v apparmor_parser >/dev/null 2>&1 && [ -f /etc/apparmor.d/gtk4-desktop-icons ]; then
apparmor_parser -R /etc/apparmor.d/gtk4-desktop-icons || true
fi
fi
#DEBHELPER#

598
ding/app/adw-ding.js Executable file
View File

@@ -0,0 +1,598 @@
#!/usr/bin/env -S gjs -m
/* ADW-DING: Desktop Icons New Generation for GNOME Shell
*
* Copyright (C) 2025 Sundeep Mediratta
* Based on code original (C) Carlos Soriano (C) Sergio Costas
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {Gio, GLib, Adw, GObject} from '../dependencies/gi.js';
import * as Gettext from 'gettext';
import {
Preferences,
AdwPreferencesWindow,
Enums,
DBusUtils,
DesktopIconsUtil,
DesktopManager,
Thumbnails
} from '../dependencies/localFiles.js';
import * as FileUtils from '../utils/fileUtils.js';
import * as System from 'system';
Gio._promisify(Gio.AppInfo, 'launch_default_for_uri_async');
Gio._promisify(Gio.FileEnumerator.prototype, 'close_async');
Gio._promisify(Gio.FileEnumerator.prototype, 'next_files_async');
Gio._promisify(Gio.Subprocess.prototype, 'wait_check_async');
Gio._promisify(Gio.DataInputStream.prototype, 'read_line_async', 'read_line_finish');
const fileProto = imports.system.version >= 17200
? Gio.File.prototype : Gio._LocalFilePrototype;
Gio._promisify(fileProto, 'delete_async');
Gio._promisify(fileProto, 'enumerate_children_async');
Gio._promisify(fileProto, 'load_bytes_async');
Gio._promisify(fileProto, 'make_directory_async');
Gio._promisify(fileProto, 'query_info_async');
Gio._promisify(fileProto, 'set_attributes_async');
Gio._promisify(fileProto, 'replace_contents_async');
Gio._promisify(fileProto, 'load_contents_async');
const getTextDomain = 'gtk4-ding';
const appID = 'com.desktop.ding';
const testAppID = `${appID}test`;
const adWDingApp = GObject.registerClass(
class adwDingApp extends Adw.Application {
constructor(asDesktop = false) {
super({
application_id: asDesktop ? appID : testAppID,
resource_base_path: `/${appID.split('.').join('/')}`,
flags:
Gio.ApplicationFlags.HANDLES_COMMAND_LINE |
Gio.ApplicationFlags.REPLACE,
});
this.asDesktop = asDesktop;
// Connect application signals
this.connect('startup', this._onStartup.bind(this));
this.connect('command-line', this._onCommandLine.bind(this));
this.connect('activate', this._onActivate.bind(this));
this.connect('shutdown', this._onShutdown.bind(this));
}
_onStartup() {
this.codePath =
GLib.path_get_dirname(System.programPath);
this.systemInstall = this.codePath.startsWith('/usr');
this.extensionDir = GLib.path_get_dirname(this.codePath);
const localePath = GLib.build_filenamev(
[this.extensionDir, 'locale']
);
if (Gio.File.new_for_path(localePath).query_exists(null))
Gettext.bindtextdomain(getTextDomain, localePath);
const resourcePath = GLib.build_filenamev(
[this.codePath, `${appID}.data.gresource`]);
const resource = Gio.Resource.load(resourcePath);
resource._register();
this._initializeOptions();
if (!this.systemInstall) {
console.log('Local install detected, updating icon cache...');
this._updateIconCache().catch(e => logError(e));
this._updateAppInfoCache().catch(e => logError(e));
}
}
_onShutdown() {
if (this.systemInstall)
return;
if (this.appIcon)
this._removeFile(this.appIcon);
if (this.appDesktopFile)
this._removeFile(this.appDesktopFile);
}
// eslint-disable-next-line consistent-return
_onCommandLine(app, commandLine) {
let argv = [];
argv = commandLine.get_arguments();
try {
// Parse options from the main arguments
this._parseOptions(argv);
this._initializeDesktopOptions();
} catch (e) {
console.log(`Error parsing options: ${e.message}`);
this.errorFound = true;
}
if (!this.errorFound && !this.showHelp) {
if (commandLine.get_is_remote()) {
this.desktops = this.newdesktops;
const windowManager = this.desktopManager.windowManager;
windowManager.updateGridWindows(this.desktops);
// If testing Dbus activations, comment the above
// and uncomment the following -
// or get remote actions from the app and activate
// this.desktopVariants = this.newDesktopsVariants;
// this.remoteDingActions.activate_action('updateGridWindows',
// new GLib.Variant('av', this.desktopVariants));
// OR smiply activate the app action directly
// app.activate_action(
// 'updateGridWindows',
// new GLib.Variant('av', this.desktopVariants)
// );
} else {
this._finishStartUp(app);
app.activate();
}
commandLine.set_exit_status(0);
return 0;
}
if (this.showHelp) {
this._printUsage();
commandLine.set_exit_status(0);
return 0;
}
if (this.errorFound) {
this._printUsage();
commandLine.set_exit_status(1);
return 1;
}
}
_finishStartUp(app) {
this.Data = {
'codePath': this.codePath,
'extensionPath': this.extensionDir,
Enums,
'gnomeversion': this.gnomeversion,
'programversion': this.programversion,
'uuid': this.uuid,
'mainApp': app,
};
this.Utils = {FileUtils};
this.Utils.DBusUtils =
new DBusUtils.DBusUtils(app);
this.Utils.ThumbnailLoader =
new Thumbnails.ThumbnailLoader(this.Utils.FileUtils);
this.Utils.Preferences =
new Preferences.Preferences(this.Data, AdwPreferencesWindow);
this.Utils.DesktopIconsUtil =
new DesktopIconsUtil.DesktopIconsUtil(this.Data, this.Utils);
}
_onActivate() {
if (!this.desktopManager) {
this.desktops = this.newdesktops;
this.desktopManager = new DesktopManager.DesktopManager(
this.Data,
this.Utils,
this.desktops,
this.codePath,
this.asDesktop,
this.primaryIndex
);
}
}
_parseOptions(args) {
this.newdesktops = [];
this.newDesktopsVariants = [];
// modified for GJS to work like passing optioncontext
args.forEach((arg, index, array) => {
this.options.some(entry => {
const longname = arg === `--${entry.long_name}`;
const shortname = arg === `-${entry.short_name}`;
if (longname || shortname) {
const assignFunction = entry.arg_data;
if (entry.arg === GLib.OptionArg.NONE) {
assignFunction();
return true;
}
let value;
if (longname && entry.long_name.includes('='))
value = entry.split('=')[1];
else
value = array[index += 1] ?? null;
assignFunction(value);
return true;
}
return false;
});
});
}
_printUsage() {
// OptionContext does not work in GJS, modifed version
// const helptext = this.optionsContext.get_help(false, null);
let helpMessage =
'Usage: gjs -m adw-ding.js [OPTIONS]\n\nOptions:\n';
this.options.forEach(entry => {
const shortOption = entry.short_name
? `-${entry.short_name}` : '';
const argDescription = entry.arg_description
? ` ${entry.arg_description}` : '';
helpMessage += ` ${shortOption}, --${entry.long_name}` +
` ${argDescription}\n\n`;
if (this.showHelp)
helpMessage += ` ${entry.description}\n\n`;
});
print(helpMessage);
}
_initializeObjects() {
this.errorFound = false;
this.showHelp = false;
this.gnomeversion = 40;
this.primaryIndex = 0;
this.programversion = 'Testing';
this.uuid = 'testing@gtk4-ding';
this.desktops = [];
this.desktopVariants = [];
this.Data = {};
// Code for checking Dbus actions and remote controlling the app
// via DBus - see commented code in commanline invocation.
//
// const dbusID = this.asDesktop ? appID : testAppID;
// const dbusPath = `${dbusID}.actions`.split('.').join('/');
// this.remoteDingActions = Gio.DBusActionGroup.get(
// Gio.DBus.session,
// dbusID,
// dbusPath
// );
}
_initializeOptions() {
this._initializeObjects();
// Define options, similar to GLib.optionEntry for GJS
this.options = [
{
long_name: 'asdesktop',
short_name: 'E',
flags: 0,
arg: GLib.OptionArg.NONE,
arg_data: () => (this.asDesktop = true),
description: 'run as desktop (with transparent window, ' +
'reacting to data from the extension...',
arg_description: 'as desktop flag',
},
{
long_name: 'help',
short_name: 'h',
flags: 0,
arg: GLib.OptionArg.NONE,
arg_data: () => (this.showHelp = true),
description: 'show this help',
arg_description: 'help flag',
},
{
long_name: 'shellversion',
short_name: 'V',
flags: 0,
arg: GLib.OptionArg.STRING,
arg_data: value => (this.gnomeversion = value),
description:
'pass the gnome version to the DING application',
arg_description: 'gnome shell version',
},
{
long_name: 'extensionversion',
short_name: 'v',
flags: 0,
arg: GLib.OptionArg.STRING,
arg_data: value => (this.programversion = value),
description: 'pass the version-name of the program to ' +
'display in extension/DING preferences',
arg_description: 'application/extension version',
},
{
long_name: 'monitor',
short_name: 'M',
flags: 0,
arg: GLib.OptionArg.CALLBACK,
arg_data: value => (this.primaryIndex = parseInt(value)),
description: 'index of the primary monitor',
arg_description: 'primary monitor index',
},
{
long_name: 'uuid',
short_name: 'U',
flags: 0,
arg: GLib.OptionArg.STRING,
arg_data: value => (this.uuid = value),
description: 'pass the uuid of the extension to use in ' +
'the DING application',
arg_description: 'extension uuid',
},
{
long_name: 'desktop',
short_name: 'D',
flags: 0,
arg: GLib.OptionArg.CALLBACK,
arg_data: data => this._parseDesktopData(data),
description:
`monitor and desktop data-
x: X coordinate
y: Y coordinate
w: width in pixels
h: height in pixels
z: zoom value (must be greater than or equal to one)
t: top margin in pixels
b: bottom margin in pixels
l: left margin in pixels
r: right margin in pixels
i: monitor index (0, 1...)
multiple "-D" options can be set for multi monitor setup`,
arg_description:
'x:y:w:h:z:t:b:l:r:i -string with monitor dimensions',
},
];
// This does not work in GJS - constructor cannot be called -
// therefore alternative implementation for the following
//
// this.optionsContext =
// new GLib.OptionContext('Adw Desktop Icons Application');
//
// this.optionsContext.add_main_entries(options, getTextDomain);
}
_parseDesktopData(data) {
data = data.split(':');
if (data.length !== 10)
throw new Error('Incorrect number of parameters for -D\n');
if (parseFloat(data[4]) < 1.0)
throw new Error('Error: ZOOM value can not be less than one\n');
const dataObject = {
x: parseInt(data[0]),
y: parseInt(data[1]),
width: parseInt(data[2]),
height: parseInt(data[3]),
zoom: parseFloat(data[4]),
marginTop: parseInt(data[5]),
marginBottom: parseInt(data[6]),
marginLeft: parseInt(data[7]),
marginRight: parseInt(data[8]),
monitorIndex: parseInt(data[9]),
};
if (Object.values(dataObject).some(x => isNaN(x)))
throw new Error('Incorrect non numeric value in -D data \n');
this.newdesktops.push(dataObject);
}
_initializeDesktopOptions() {
if (!this.newdesktops.length && !this.asDesktop) {
/* if no desktop list is provided,
* like when launching the program in stand-alone mode,
* configure a 1280x720 desktop
*/
const data = '0:0:1280:720:1:0:0:0:0:0';
this._parseDesktopData(data);
}
this.newdesktops.forEach(d =>
(d.primaryMonitor = this.primaryIndex));
this.newDesktopsVariants = this.newdesktops.map(d => {
return new GLib.Variant('a{sd}', {
x: d.x,
y: d.y,
width: d.width,
height: d.height,
zoom: d.zoom,
marginTop: d.marginTop,
marginBottom: d.marginBottom,
marginLeft: d.marginLeft,
marginRight: d.marginRight,
monitorIndex: d.monitorIndex,
primaryMonitor: d.primaryMonitor,
});
});
}
async _installFile(resourcePath, destinationPath) {
const resourceFile = Gio.File.new_for_uri(resourcePath);
const destinationFile = Gio.File.new_for_path(destinationPath);
const [contents] =
await resourceFile.load_contents_async(null);
if (!contents)
return false;
if (destinationFile.query_exists(null)) {
const [existingContents] =
await destinationFile.load_contents_async(null);
const fileName = GLib.path_get_basename(destinationPath);
if (this._memcmp(contents, existingContents))
console.log(`Already up-to-date: ${fileName}`);
else
console.log(`User installed file ${fileName} exists`);
return false;
}
try {
await destinationFile.replace_contents_async(
contents,
null,
false,
Gio.FileCreateFlags.REPLACE_DESTINATION,
null
);
console.log(
`Updated: ${GLib.path_get_basename(destinationPath)}`
);
} catch (e) {
if (e.matches(
Gio.IOErrorEnum,
Gio.IOErrorEnum.NOT_FOUND
)) {
GLib.mkdir_with_parents(
GLib.path_get_dirname(destinationPath),
0o700
);
console.log(
'Created missing parent directories: ' +
`${GLib.path_get_dirname(destinationPath)}`
);
const retval = await this._installFile(
resourcePath,
destinationPath
);
return retval;
}
return false;
}
return true;
}
_removeFile(destinationPath) {
const destinationFile = Gio.File.new_for_path(destinationPath);
try {
if (destinationFile.query_exists(null))
destinationFile.delete(null);
console.log(
'Cleaning up, removed: ' +
`${GLib.path_get_basename(destinationPath)}`
);
} catch (e) {
logError(e);
}
}
async _updateIconCache() {
const appPath = `/${appID.split('.').join('/')}`;
const iconPath = '/icons/hicolor/scalable/apps';
const iconResrc = `resource://${appPath}${iconPath}/${appID}.svg`;
const appIcon = GLib.build_filenamev([
GLib.get_user_data_dir(),
`${iconPath}`,
`${appID}.svg`,
]);
const written = await this._installFile(iconResrc, appIcon);
if (written) {
this.appIcon = appIcon;
const iconCachePath = GLib.build_filenamev([
GLib.get_user_data_dir(),
'icons',
'hicolor',
]);
const updated = await GLib.spawn_command_line_async(
'gtk-update-icon-cache ' +
'-q -t -f ' +
`${iconCachePath}`
);
if (updated)
console.log('Updated icon cache');
}
}
async _updateAppInfoCache() {
const appPath = `/${appID.split('.').join('/')}`;
const appResource = `resource://${appPath}/${appID}.desktop`;
const appDesktopFile = GLib.build_filenamev([
GLib.get_user_data_dir(),
'applications',
`${appID}.desktop`,
]);
const written =
await this._installFile(appResource, appDesktopFile);
if (written) {
this.appDesktopFile = appDesktopFile;
// Gnome will update the app info cache automatically
// However it takes a long time to update the cache
// and we need to do it manually for the app to be
// available sooner
const updated = await GLib.spawn_command_line_async(
'update-desktop-database -q ' +
`${GLib.path_get_dirname(appDesktopFile)}`
);
if (updated)
console.log('Updated desktop database');
}
}
_memcmp(a, b) {
if (a.length !== b.length)
return false;
if (a.some((x, i) => x !== b[i]))
return false;
return true;
}
}
);
const asDesktop = ARGV.includes('-E');
const app = new adWDingApp(asDesktop);
System.exit(await app.runAsync(ARGV));

View File

@@ -0,0 +1,900 @@
/* Desktop Icons GNOME Shell extension
*
* Copyright (C) 2023 Sundeep Mediratta (smedius@gmail.com)
*
* 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/>.
*/
import {Gtk, Gdk, Gio, GLib, GObject, Adw} from '../dependencies/gi.js';
import {DesktopWidgetCapability} from '../dependencies/gi.js';
import {_} from '../dependencies/gettext.js';
import {DesktopFolderUtils} from '../dependencies/localFiles.js';
export {AdwPreferencesWindow};
const appID = 'com.desktop.ding';
const appPath = GLib.build_filenamev(['/', ...appID.split('.')]);
const ListObject = GObject.registerClass({
GTypeName: 'peferences-list',
Properties: {
'indexkey': GObject.ParamSpec.string(
'indexkey',
'Indexkey',
'A read-write string property',
GObject.ParamFlags.READWRITE,
''
),
'description': GObject.ParamSpec.string(
'description',
'Description',
'A read-write string property',
GObject.ParamFlags.READWRITE,
''
),
},
}, class listObject extends GObject.Object {
constructor(constructProperties = {}) {
super(constructProperties);
}
get indexkey() {
if (this._indexkey === undefined)
this._indexkey = '';
return this._indexkey;
}
set indexkey(value) {
if (this.indexkey === value)
return;
this._indexkey = value;
this.notify('indexkey');
}
get description() {
if (this._description === undefined)
this._description = '';
return this._description;
}
set description(value) {
if (this.description === value)
return;
this._description = value;
this.notify('description');
}
});
const ComboRowWithKey = GObject.registerClass({
GTypeName: 'ComboRowWithKey',
Properties: {
'indexkey': GObject.ParamSpec.string(
'indexkey',
'Indexkey',
'A read-write string property',
GObject.ParamFlags.READWRITE,
''
),
},
}, class ComboRowWithKey extends Adw.ComboRow {
constructor(constructProperties = {}) {
super(constructProperties);
this._indexKey = '';
this.connect('notify::selected-item', () => {
let item = this.get_selected_item();
this.indexkey = item.indexkey;
});
}
makeEnumn(enumexpression) {
const listStore = new Gio.ListStore(ListObject._$gtype);
this.enumExpression = {};
let i = 0;
for (let key in enumexpression) {
this.enumExpression[key] = parseInt(i);
let listObject = new ListObject();
listObject.indexkey = key;
listObject.description = enumexpression[key];
listStore.append(listObject);
i += 1;
}
this.set_model(listStore);
const listFactory = new Gtk.SignalListItemFactory();
listFactory.connect('setup', (_actor, listitem) => {
let label = new Gtk.Label();
listitem.set_child(label);
});
listFactory.connect('bind', (_actor, listitem) => {
let label = listitem.get_child();
let item = listitem.get_item();
label.set_text(item.description);
});
this.set_factory(listFactory);
const expression = new Gtk.PropertyExpression(ListObject,
null,
'description'
);
this.set_expression(expression);
}
get indexkey() {
if (this._indexkey === undefined)
this._indexkey = '';
return this._indexkey;
}
set indexkey(value) {
if (this.indexkey === value)
return;
this._indexkey = value;
if (this.get_selected !== this.enumExpression[value])
this.set_selected(this.enumExpression[value]);
this.notify('indexkey');
}
});
const CssOverrideGroup = GObject.registerClass(
class CssOverrideGroup extends Adw.PreferencesGroup {
constructor(params = {}) {
super({});
this.set_title(_('CSS Override'));
this.set_description(_('Customise the appearance of desktop icons with CSS'));
const warningLabel = new Gtk.Label();
warningLabel.set_markup(
`<span style="italic" foreground="red">${
_('Warning: This can break the extension if done incorrectly')
}</span>`
);
this.add(warningLabel);
const icon = Gtk.Image.new_from_icon_name('window-pop-out-symbolic');
this.cssOverrideButton = new Adw.ActionRow({
title: _('Edit CSS Override File...'),
});
this.cssOverrideButton.add_suffix(icon);
this.cssOverrideButton.set_activatable_widget(icon);
this.cssOverrideButton.connect('activated', this.openUserCssOverrideFile.bind(this));
this.add(this.cssOverrideButton);
this.reloadButtonRow = new Adw.ActionRow({
title: _('Apply CSS Changes Now'),
subtitle: _('Reload the CSS to apply changes immediately'),
});
const button = Gtk.Button.new_with_label('Reload');
button.set_size_request(120, -1);
button.set_halign(Gtk.Align.END);
button.set_valign(Gtk.Align.CENTER);
button.set_hexpand(true);
button.set_vexpand(false);
button.connect('clicked', () => {
this.reloadCSS();
});
this.reloadButtonRow.add_suffix(button);
this.reloadButtonRow.set_activatable_widget(button);
this.add(this.reloadButtonRow);
this.update(params.remoteActions);
}
reloadCSS() {
try {
this.remoteActions.activate_action('reloadCSS', null);
console.info('CSS reload requested');
} catch (e) {
console.error(`Failed to reload CSS: ${e}`);
}
}
update(remoteActions) {
this.remoteActions = remoteActions;
if (this.remoteActions?.list_actions())
this.reloadButtonRow.set_sensitive(true);
else
this.reloadButtonRow.set_sensitive(false);
}
openUserCssOverrideFile() {
const configDir = GLib.get_user_config_dir();
const cssFile = Gio.File.new_for_path(
GLib.build_filenamev([configDir, appID, 'stylesheet-override.css'])
);
// Create directory if it doesn't exist
const cssDir = cssFile.get_parent();
try {
cssDir.make_directory_with_parents(null);
} catch (e) {
// Directory already exists
}
// Create file if it doesn't exist
if (!cssFile.query_exists(null))
cssFile.create(Gio.FileCreateFlags.NONE, null);
// Open with default text editor
const context = Gdk.Display.get_default().get_app_launch_context();
context.set_timestamp(Gdk.CURRENT_TIME);
Gio.AppInfo.launch_default_for_uri(cssFile.get_uri(), context);
}
});
const ShortcutGroup = GObject.registerClass(
class ShortcutGroup extends Adw.PreferencesGroup {
constructor(params = {}) {
super({});
this.set_title(_('Shortcuts'));
this.shortcutButton = new Adw.ActionRow({
title: _('Edit Shortcuts...'),
});
const icon = Gtk.Image.new_from_icon_name('window-pop-out-symbolic');
this.shortcutButton.add_suffix(icon);
this.shortcutButton.set_activatable_widget(icon);
this.shortcutButton.connect('activated', this.showShortcuts.bind(this));
this.add(this.shortcutButton);
this.update(params.remoteActions);
}
update(remoteActions) {
this.remoteActions = remoteActions;
if (this.remoteActions?.list_actions()) {
this.shortcutButton.set_sensitive(true);
this.set_description(_('Edit Application Shortcuts'));
} else {
this.shortcutButton.set_sensitive(false);
this.set_description(
_('Shortcuts Editable only when Extension Enabled...')
);
}
}
showShortcuts() {
this.remoteActions.activate_action('showShortcutViewer', null);
}
});
const aboutApp = class AboutDialog {
constructor(params = {}) {
this.version = params.version;
this.appID = appID;
const aboutDialog = Adw.AboutDialog.new();
this.init(aboutDialog);
return aboutDialog;
}
init(aboutDialog) {
aboutDialog.modal = true;
aboutDialog.set_application_icon(this.appID);
aboutDialog.set_application_name('Adw. Desktop Icons');
aboutDialog.set_comments(
'An application to show Icons on the Gnome Desktop'
);
aboutDialog.set_copyright('© 2025 Sundeep Mediratta');
aboutDialog.set_developer_name('Sundeep Mediratta');
aboutDialog.set_comments(
'Adw. Desktop Icons is an extension and a program together for ' +
'the GNOME Shell that renders icons on the desktop. It is a fork ' +
'from Desktop Icons NG (DING), by Sergio Costas, which itself ' +
'is a fork/rewrite of the official "Desktop Icons" extension, ' +
'originally by Carlos Soriano.' +
'\n\n' +
'All these came into existence when Nautilus and Gnome decided ' +
'to drop showing a "Desktop" with Icons!' +
'\n\n' +
'Many thanks to the original developers of Desktop Icons NG, ' +
'specially Sergio Costas for his work on ' +
'Meta.WaylandClient that makes this privileged window possible in' +
'the first place and to Florian Müllner for implementing ' +
'Meta.Windotype.DESKTOP through Meta.WaylandClient, which makes ' +
'this so much easier!'
);
aboutDialog.add_credit_section(
'Originally developed by',
[
'Sergio Costas',
'Carlos Soriano',
]
);
aboutDialog.add_acknowledgement_section(
'For coding Meta.WaylandClient in mutter',
['Sergio Costas']
);
aboutDialog.add_acknowledgement_section(
'Enabling Meta.Windowtype.DESKTOP\nthrough Meta.Waylandclient',
['Florian Müllner']
);
aboutDialog.add_acknowledgement_section(
'Async code contribution',
['Marco Trevisan']
);
aboutDialog.add_acknowledgement_section(
'Gnome Extensions Matrix Channel support',
[
'Andy Holmes',
'Just Perfection',
'And Others..',
]
);
aboutDialog.add_acknowledgement_section(
'GJS Maintainers for GJS\n@ptomato for answering',
['@ptomato']
);
aboutDialog.set_license_type(Gtk.License.GPL_3_0);
aboutDialog.set_issue_url(
'https://gitlab.com/smedius/desktop-icons-ng/-/issues'
);
aboutDialog.set_support_url(
'https://gitlab.com/smedius/desktop-icons-ng/-/blob/main/ISSUES.md?ref_type=heads'
);
aboutDialog.set_translator_credits(
`Weblate Translators, See History.MD on website. Translated using machine translation with LibreTranslate. Unverified strings, translation may contain errors. Corrections, verification and additional translation can be done on Weblate.
Translations available in- ar,az,be,bg,bn,ca,cs,da,de,el,eo,es,et,eu,fa,fi,fr,fur,ga,gl,he,hi,hr,hu,id,it,ja,ka,ko,ky,lv,lt,ms,nb,nb_NO,nl,oc,pl,pt_BR,pt,ro,ru,sk,sl,sq,sv,ta,tl,tr,th,uk,ur,zh-Hans,zh-Hant,zh_CN,zh_TW.`
);
aboutDialog.set_version(this.version);
aboutDialog.set_website('https://gitlab.com/smedius/desktop-icons-ng');
aboutDialog.add_link(
_('Help translate in your web browser'),
'https://hosted.weblate.org/engage/gtk4-desktop-icons-ng'
);
aboutDialog.set_release_notes(
`<p>* Adw version 100.17 for Gnome 45, 46, 47, 48, 49</p>
<ul><li>Widget polish: grid controls, media widgets, webview reattach, redisplay, and animation.</li></ul>
<p>* Adw version 100.16 for Gnome 45, 46, 47, 48, 49</p>
<ul><li>Fixes -Empty window not mapping, icon placement with monitor hotplug</li></ul>
<p>* Adw version 100.15 for Gnome 45, 46, 47, 48, 49</p>
<ul><li>Uses GSK instead of Cairo to draw, optimizing GPU, minimizing CPU</li></ul>
<ul><li>Fixes widget positioning and loadstate race</li></ul>
<ul><li>Fixes widget visibility on mapping visibility changes</li></ul>
<ul><li>Fixes icon selection with shift/ctrl and keyboard arrow navigation</li></ul>
<ul><li>Fixes overrview animation</li></ul>
<p>* Adw version 100.14 for Gnome 45, 46, 47, 48, 49</p>
<ul><li>Adds html widgets that can launch and communicate with a local backend</li></ul>
<ul><li>Reverts multiple selection with arrows as it breaks mouse drag and drop</li></ul>
<ul><li>Added a Today(Calendar) widget and system Metrics widget for desktop</li></ul>
<ul><li>Improves preferences for widgets</li></ul>
<p>* Adw version 100.13 for Gnome 45, 46, 47, 48, 49</p>
<ul><li>Adds widgets that can be displayed on the desktop under the icon layer</li></ul>
<ul><li>Users can apply their own CSS</li></ul>
<p>* Adw version 100.11 for Gnome 45, 46, 47, 48, 49</p>
<ul><li>Fix margins in RTL layout under dock</li></ul>
<ul><li>Adapt to X11 removal in mutter</li></ul>
<p>* Adw version 100.9 for Gnome 45, 46, 47, 48, 49</p>
<ul><li>Machine Translation with LibreTranslate to all supported languages</li></ul>
<p>* Adw version 100.8-2 for Gnome 45, 46, 47, 48 49</p>
<ul><li>Bug fix for older gnome versions with no GioUnix namespace</li></ul>
<p>* Adw version 100.8 for Gnome 45, 46, 47, 48, 49</p>
<ul><li>Animate margin changes. Respects global Gtk4/Gnome animation settings</li></ul>
<ul><li>Right long-click brings up gnome shell background menu directly</li></ul>
<ul><li>Improve search UI, unselected items are now properly dimmed to highlight the selected</li></ul>
<p>* Adw version 100.7 for Gnome 45, 46, 47, 48, 49</p>
<ul><li>Fix Gnome 49 compatibility issues</li></ul>
<ul><li>Fix xdg-terminal-exec directory detection in system data dirs</li></ul>
<p>* Adw version 100.6 for Gnome 45, 46, 47, 48, 49</p>
<ul><li>Adapt to new Gnome 49 Meta.Window and Meta.WaylandClient API</li></ul>
<ul><li>Fix missing app icon if no parent icon folder</li></ul>
<p>* Adw version 100.5 for Gnome 45, 46, 47, 48</p>
<ul><li>Set localized default desktop name</li></ul>
<ul><li>Resizable open with dialog</li></ul>
<ul><li>Fix custom icons size</li></ul>
<ul><li>Update to more direct error message</li></ul>
<p>* Adw version 100.3 for Gnome 45, 46, 47, 48</p>
<ul><li>Draw proper selection rectangle at small sizes</li></ul>
<p>* Adw version 100.2 for Gnome 45, 46, 47, 48</p>
<ul><li>Remove dependency on xdg-user-dirs</li></ul>
<p>* Adw version 100.1 for Gnome 45, 46, 47, 48</p>
<p>Minor bug fixes to run on older Adw 1.5, errors on connecting second monitor, fix open terminal shortcut</p>
<p>Yay! Version 100!
Actually version 1.0, started with 0.01, but got tired of writing a 0 before every version.
I believe mostly feature complete, except DBus Activation and packaging as a GJS app.
Change Name to Adw. Desktop Icons :)
</p>
<ul><li> Add a complete shortcut manager with editable local and global shortcuts.</li>
<li> Add Adw.AboutDialog for the application with proper credits and acknowledgements.</li>
<li> Add more actions, to arrange icons directly, that can then have proper keybindings in the shortcut manager.</li>
<li> Redesign the preferences to open the shortcut manger. When opened through gnome extensions settings, shortcut manager is activated over DBus, and only works if the extension/app is enabled. </li>
<li> Add an easily editable boolean constant to gnome shell override so Users wanting to show icons on window picker overview or on thumbnails can choose to do so.</li>
<li> .desktop files on the desktop now show their actions in the right click context menus. All these actions are shown and can be activated if the file is trusted.</li></ul>`
);
}
};
const DingPreferencesWindow = class extends DesktopFolderUtils {
constructor(params) {
super(params);
this.iconTheme =
Gtk.IconTheme.get_for_display(Gdk.Display.get_default());
this.iconTheme.add_resource_path(`${appPath}/icons`);
}
addActionRowSwitch(settings, key, labelText, bindFlags = null) {
const actionRow = Adw.ActionRow.new();
const switcher = new Gtk.Switch({active: settings.get_boolean(key)});
switcher.set_halign(Gtk.Align.END);
switcher.set_valign(Gtk.Align.CENTER);
switcher.set_hexpand(false);
switcher.set_vexpand(false);
actionRow.set_title(labelText);
actionRow.add_suffix(switcher);
if (!bindFlags)
bindFlags = Gio.SettingsBindFlags.DEFAULT;
settings.bind(key, switcher, 'active', bindFlags);
actionRow.set_activatable_widget(switcher);
return actionRow;
}
addActionRowSelector(settings, key, labelText, elements) {
const actionRow = new ComboRowWithKey();
actionRow.set_title(labelText);
actionRow.set_use_subtitle(false);
actionRow.makeEnumn(elements);
actionRow.set_selected(settings.get_enum(key));
settings.bind(key, actionRow, 'indexkey',
Gio.SettingsBindFlags.DEFAULT);
return actionRow;
}
addActionRowButton(title, subtitle, buttonLabel, action, key = null) {
const actionRow = Adw.ActionRow.new();
actionRow.set_title(title);
if (subtitle) {
actionRow.use_markup = false;
actionRow.set_subtitle(subtitle);
if (Adw.get_minor_version() > 2)
actionRow.set_subtitle_selectable(true);
}
if (buttonLabel && action) {
const button = Gtk.Button.new_with_label(buttonLabel);
button.set_size_request(120, -1);
button.set_halign(Gtk.Align.END);
button.set_valign(Gtk.Align.CENTER);
button.set_hexpand(true);
button.set_vexpand(false);
button.connect('clicked', action.bind(this, actionRow, button, key));
actionRow.add_suffix(button);
actionRow.set_activatable_widget(button);
}
return actionRow;
}
launchUri(uri) {
const context = Gdk.Display.get_default().get_app_launch_context();
context.set_timestamp(Gdk.CURRENT_TIME);
Gio.AppInfo.launch_default_for_uri(uri, context);
}
};
const AdwPreferencesWindow = class extends DingPreferencesWindow {
constructor(
desktopSettings,
nautilusSettings,
gtkSettings,
version,
actiongroup = null
) {
super();
this.desktopSettings = desktopSettings;
this.nautilusSettings = nautilusSettings;
this.gtkSettings = gtkSettings;
if (!actiongroup)
this.getRemoteActions();
else
this.remoteActions = actiongroup;
this.version = version;
}
destroy() {
if (this.watchNameID)
Gio.DBus.unwatch_name(this.watchNameID);
this.watchNameID = 0;
}
getRemoteActions() {
this.watchNameID = Gio.DBus.watch_name(
Gio.BusType.SESSION,
appID,
Gio.BusNameWatcherFlags.NONE,
(_conn, _name, _nameOwner) => {
try {
this.remoteActions = Gio.DBusActionGroup.get(
Gio.DBus.session,
appID,
appPath
);
this.shortcutGroup?.update(this.remoteActions);
this.cssOverrideGroup?.update(this.remoteActions);
} catch (e) {
logError(e, 'Error getting action group');
}
},
(_conn, _name) => {
this.remoteActions = null;
this.shortcutGroup?.update(this.remoteActions);
this.cssOverrideGroup?.update(this.remoteActions);
}
);
}
getAdwPreferencesWindow(window = null) {
var prefsWindow;
if (window) {
prefsWindow = window;
} else {
prefsWindow = new Adw.PreferencesWindow();
const app = Gtk.Application.get_default();
if (app)
prefsWindow.set_application(app);
}
prefsWindow.set_can_navigate_back(true);
prefsWindow.set_search_enabled(true);
this.prefsWindow = prefsWindow;
this.activeWindow = prefsWindow;
const prefsFrame = new Adw.PreferencesPage();
prefsFrame.set_name(_('Desktop'));
prefsFrame.set_title(_('Desktop'));
prefsFrame.set_icon_name('prefs-desktop-symbolic');
const filesPrefsFrame = new Adw.PreferencesPage();
filesPrefsFrame.set_name(_('Files'));
filesPrefsFrame.set_title(_('Files'));
filesPrefsFrame.set_icon_name('prefs-files-symbolic');
const tweaksFrame = new Adw.PreferencesPage();
tweaksFrame.set_name(_('Tweaks'));
tweaksFrame.set_title(_('Tweaks'));
tweaksFrame.set_icon_name('prefs-tweaks-symbolic');
const aboutFrame = new Adw.PreferencesPage();
aboutFrame.set_name(_('More'));
aboutFrame.set_title(_('More'));
aboutFrame.set_icon_name('prefs-more-symbolic');
prefsWindow.add(prefsFrame);
prefsWindow.add(filesPrefsFrame);
prefsWindow.add(tweaksFrame);
prefsWindow.add(aboutFrame);
prefsWindow.set_visible(prefsFrame);
const desktopGroup = new Adw.PreferencesGroup();
desktopGroup.set_title(_('Desktop Settings'));
desktopGroup.set_description(_('Settings for the Desktop Program'));
prefsFrame.add(desktopGroup);
this.desktopFolderGroup = new Adw.PreferencesGroup();
this.desktopFolderGroup.set_title(_('Desktop Folder'));
this.FolderGroupDescription = _('Current Desktop: ');
const desktoPath = this.getDesktopDir().get_path();
this.desktopFolderGroup.set_description(
`${this.FolderGroupDescription} ${desktoPath}`
);
prefsFrame.add(this.desktopFolderGroup);
const filesGroup = new Adw.PreferencesGroup();
filesGroup.set_title(_('Files Settings'));
filesGroup.set_description(_('Settings shared with Gnome Files'));
filesPrefsFrame.add(filesGroup);
const tweaksGroup = new Adw.PreferencesGroup();
tweaksGroup.set_title(_('Tweaks'));
tweaksGroup.set_description(_('Miscellaneous Tweaks'));
tweaksFrame.add(tweaksGroup);
this.shortcutGroup =
new ShortcutGroup({remoteActions: this.remoteActions});
aboutFrame.add(this.shortcutGroup);
this.cssOverrideGroup = new CssOverrideGroup({
remoteActions: this.remoteActions,
});
this.cssOverrideGroup.set_visible(false); // Initially hidden
aboutFrame.add(this.cssOverrideGroup);
const aboutGroup = new Adw.PreferencesGroup();
aboutGroup.set_title('About Adw. Desktop Icons');
let versiontitle = _(`Version ${this.version}`);
aboutGroup.set_description(versiontitle);
aboutFrame.add(aboutGroup);
desktopGroup.add(this.addActionRowSelector(this.desktopSettings,
'icon-size',
_('Size for the desktop icons'),
{
'tiny': _('Tiny'),
'small': _('Small'),
'standard': _('Standard'),
'large': _('Large'),
}
));
desktopGroup.add(this.addActionRowSelector(this.desktopSettings,
'start-corner',
_('New icons alignment'),
{
'top-left': _('Top left corner'),
'top-right': _('Top right corner'),
'bottom-left': _('Bottom left corner'),
'bottom-right': _('Bottom right corner'),
}
));
desktopGroup.add(this.addActionRowSwitch(this.desktopSettings,
'show-second-monitor',
_('Add new icons to Secondary Monitors first, if available')));
desktopGroup.add(this.addActionRowSwitch(this.desktopSettings,
'free-position-icons',
_('Snap icons to grid'),
Gio.SettingsBindFlags.INVERT_BOOLEAN
));
const showWidgets = this.addActionRowSwitch(this.desktopSettings,
'show-desktop-widgets',
_('Show desktop widgets'));
showWidgets.set_sensitive(DesktopWidgetCapability);
desktopGroup.add(showWidgets);
this.desktopFolderGroup
.add(this.addActionRowButton(_('New Desktop Folder'),
_('Set a new folder for the desktop'),
_('Choose'),
this.changeDesktop.bind(this)
));
const defaultDesktopPath = this.getSystemLocalizedDesktopDir();
const secondarytext = _('Set Desktop back to ~/');
this.defaultDesktopRow =
this.addActionRowButton(_('Restore Default Desktop Folder'),
`${secondarytext}${defaultDesktopPath}`,
_('Restore'),
this.restoreDefaultDesktop.bind(this)
);
this.desktopFolderGroup.add(this.defaultDesktopRow);
this.defaultDesktopRow.set_sensitive(!this.isDefaultDesktop);
const dropPlaceRow = this.addActionRowSwitch(this.desktopSettings,
'show-drop-place',
_('Highlight the drop grid'));
this.desktopSettings.bind('free-position-icons', dropPlaceRow,
'sensitive',
Gio.SettingsBindFlags.INVERT_BOOLEAN);
tweaksGroup.add(dropPlaceRow);
tweaksGroup.add(this.addActionRowSwitch(this.desktopSettings,
'show-link-emblem',
_('Add information emblems for links, encryption')));
tweaksGroup.add(this.addActionRowSwitch(this.desktopSettings,
'dark-text-in-labels',
_('Use dark text in icon labels')
));
tweaksGroup.add(this.addActionRowSwitch(this.desktopSettings,
'show-home',
_('Show the personal folder on the desktop')
));
tweaksGroup.add(this.addActionRowSwitch(this.desktopSettings,
'show-trash',
_('Show the trash icon on the desktop')
));
tweaksGroup.add(this.addActionRowSwitch(this.desktopSettings,
'show-volumes',
_('Show external drives on the desktop')
));
tweaksGroup.add(this.addActionRowSwitch(this.desktopSettings,
'show-network-volumes',
_('Show network drives on the desktop')
));
tweaksGroup.add(this.addActionRowSwitch(this.desktopSettings,
'add-volumes-opposite',
_('Add new drives to the opposite side of the desktop')
));
filesGroup.add(this.addActionRowSelector(this.nautilusSettings,
'click-policy',
_('Action to Open Items'),
{
'single': _('Single click'),
'double': _('Double click'),
}));
filesGroup.add(this.addActionRowSelector(this.nautilusSettings,
'show-image-thumbnails',
_('Show image thumbnails'),
{
'always': _('Always'),
'local-only': _('On this computer only'),
'never': _('Never'),
}));
filesGroup.add(this.addActionRowSwitch(this.nautilusSettings,
'show-delete-permanently',
_('Show a context menu item to delete permanently')
));
filesGroup.add(this.addActionRowSwitch(this.gtkSettings,
'show-hidden',
_('Show hidden files')
));
filesGroup.add(this.addActionRowSwitch(this.nautilusSettings,
'open-folder-on-dnd-hover',
_('Open folders on drag hover')
));
const aboutButton = new Adw.ActionRow();
aboutButton.set_title(_('About...'));
const icon = Gtk.Image.new_from_icon_name('window-pop-out-symbolic');
aboutButton.add_suffix(icon);
aboutButton.set_activatable_widget(icon);
aboutButton.connect('activated', () => {
const aboutDialog = new aboutApp({version: this.version});
aboutDialog.present(prefsWindow);
});
aboutGroup.add(aboutButton);
const tranlationGroup = new Adw.PreferencesGroup({
title: _('Translation'),
description: _('Machine translated using LibreTranslate. User verified and edited on Weblate.'),
});
aboutFrame.add(tranlationGroup);
tranlationGroup.add(this.addActionRowButton(_('Edit Translations'),
_('Verify, add or correct translation in your web browser'),
_('Translate'),
this.launchWebTranslation.bind(this)
));
// Track Alt key state using event controllers
this._altKeyPressed = false;
// Add key event controller to track Alt key
const keyController = new Gtk.EventControllerKey();
keyController.connect('key-pressed', (controller, keyval, _keycode, _state) => {
if (keyval === Gdk.KEY_Alt_L || keyval === Gdk.KEY_Alt_R)
this._altKeyPressed = true;
return false;
});
keyController.connect('key-released', (controller, keyval, _keycode, _state) => {
if (keyval === Gdk.KEY_Alt_L || keyval === Gdk.KEY_Alt_R)
this._altKeyPressed = false;
});
prefsWindow.add_controller(keyController);
// Add click gesture controller to detect Alt+Click on tab
const clickController = new Gtk.GestureClick();
clickController.connect('pressed', (gesture, _nPress, _x, _y) => {
const state = gesture.get_current_event().get_modifier_state();
this._altKeyPressed = (state & Gdk.ModifierType.ALT_MASK) !== 0;
});
prefsWindow.add_controller(clickController);
// Show CSS Override group only when navigating to More tab with Alt held
prefsWindow.connect('notify::visible-page', () => {
if (prefsWindow.get_visible_page() === aboutFrame)
this.cssOverrideGroup.set_visible(this._altKeyPressed);
});
prefsWindow.set_default_size(600, 650);
this._monitorDesktopDirChanges();
prefsWindow.connect(
'close-request',
() => {
this._stopMonitoring();
this.activeWindow = null;
}
);
if (!window)
return prefsWindow;
else
return true;
}
onDesktopFolderChanged(newDesktopDir) {
super.onDesktopFolderChanged(newDesktopDir);
const desktopPath = this._desktopDir.get_path();
this.desktopFolderGroup.set_description(
`${this.FolderGroupDescription} ${desktopPath}`
);
this.defaultDesktopRow.set_sensitive(!this.isDefaultDesktop);
}
launchWebTranslation() {
const translationUri =
'https://hosted.weblate.org/engage/gtk4-desktop-icons-ng';
this.launchUri(translationUri);
}
};

248
ding/app/appChooser.js Normal file
View File

@@ -0,0 +1,248 @@
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Gtk4 Port Copyright (C) 2023 Sundeep Mediratta (smedius@gmail.com)
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {Gtk, Gdk, Gio, Adw} from '../dependencies/gi.js';
import {_} from '../dependencies/gettext.js';
export {AppChooserDialog};
const AppChooserDialog = class {
constructor(fileItems, activeFileItem = null, dbusUtils, desktopIconsUtil) {
if (!activeFileItem)
activeFileItem = fileItems[0];
if (fileItems.length === 1) {
this.fileName = activeFileItem.displayName;
this.singleContentType = true;
} else {
this.fileName = null;
this.singleContentType = this._detectSingleContentType(fileItems);
}
this._dbusUtils = dbusUtils;
this._desktopIconsUtil = desktopIconsUtil;
this.mimeType = activeFileItem.attributeContentType;
this.mimeTypeIsDirectory = this.mimeType === 'inode/directory';
const appwindow =
this._desktopIconsUtil.getMainApp().get_active_window();
this.builderObject =
Gtk.Builder
.new_from_resource('/com/desktop/ding/ui/ding-app-chooser.ui');
this.builderObject.set_translation_domain('gtk4-ding');
this.appChooserDialog =
this.builderObject.get_object('DingAppChooser');
this.appChooserDialog.set_transient_for(appwindow);
this.appChooserDialog.set_title('DingAppChooser');
this.appChooserDialog.set_name('DingAppChooser');
const modal = true;
this._desktopIconsUtil
.windowHidePagerTaskbarModal(this.appChooserDialog, modal);
this.appChooserBox =
this.builderObject.get_object('app_chooser_widget_box');
this.appChooserBox.set_hexpand(true);
this.appChooserBox.set_halign(Gtk.Align.FILL);
this.appChooserWidget = Gtk.AppChooserWidget.new(this.mimeType);
this.appChooserWidget.set_show_default(true);
this.appChooserWidget.set_show_fallback(true);
this.appChooserWidget.set_show_other(true);
this.appChooserBox.append(this.appChooserWidget);
this.appChooserWidget.set_vexpand(true);
this.appChooserWidget.set_halign(Gtk.Align.FILL);
this.appChooserWidget.set_hexpand(true);
if (this.fileName !== null) {
const description =
_('Choose an application to open <b>{foo}</b>')
.replace('{foo}', this.fileName.replaceAll('&', '&amp;'));
this.appChooserLabel =
this.builderObject.get_object('label_description');
this.appChooserLabel.set_markup(description);
}
let headerTitle;
if (!this.singleContentType)
headerTitle = _('Open Items');
else if (this.mimeTypeIsDirectory)
headerTitle = _('Open Folder');
else
headerTitle = _('Open File');
this.appChooserDialogHeaderBar = this.appChooserDialog.get_header_bar();
this.appChooserDialogHeaderBar
.set_title_widget(Adw.WindowTitle.new(headerTitle, ''));
this.appChooserRowBox =
this.builderObject.get_object('set_default_box');
this.appChooserRow =
this.builderObject.get_object('set_default_row');
this.appChooserRowSwitch =
this.builderObject.get_object('set_as_default_switch');
this.selectedAppInfo = this.appChooserWidget.get_app_info();
if (this.selectedAppInfo !== null) {
this._onApplicationSelected(
this.appChooserWidget,
this.selectedAppInfo
);
}
this.appChooserWidget.connect(
'application-activated',
this._onApplicationActivated.bind(this)
);
this.appChooserWidget.connect(
'application-selected',
this._onApplicationSelected.bind(this)
);
if (this.singleContentType && !this.mimeTypeIsDirectory) {
let description = Gio.content_type_get_description(this.mimeType);
this.appChooserRow.set_subtitle(description);
} else {
this.appChooserRowBox.set_visible(false);
}
this.appChooserDialog.connect('close', () => {
this.appChooserDialog.response(Gtk.ResponseType.CANCEL);
});
this.appChooserDialog.connect('response', (actor, retval) => {
if (retval === Gtk.ResponseType.OK) {
this._checkUpdateDefaultAppForMimeType();
this.applicationSelectionComplete(this.selectedAppInfo);
} else {
this.applicationSelectionComplete(null);
}
});
}
_checkUpdateDefaultAppForMimeType() {
if (!this.singleContentType)
return;
let newAppSelected = false;
if (this.appChooserRowSwitch.get_sensitive())
newAppSelected = this.appChooserRowSwitch.get_active();
if (newAppSelected) {
let success =
this.selectedAppInfo.set_as_default_for_type(this.mimeType);
if (!success) {
let header =
_('Error changing default application');
let message =
_('Error while setting {foo} as default application for {mimetype}');
message =
message.replace(
'{foo}',
this.selectedAppInfo.get_display_name()
);
message =
message
.replace(
'{mimetype}',
Gio.content_type_get_description(this.mimeType)
);
this._dbusUtils.doNotify(header, message);
}
}
}
_onApplicationActivated(actor, appInfo) {
this.selectedAppInfo = appInfo;
this.appChooserDialog.response(Gtk.ResponseType.OK);
}
_onApplicationSelected(actor, appInfo) {
if (!this.appChooserDialog)
return;
this.selectedAppInfo = appInfo;
this.appChooserDialog
.set_response_sensitive(
Gtk.ResponseType.OK,
this.selectedAppInfo !== null
);
let defaultAppInfo =
Gio.AppInfo.get_default_for_type(this.mimeType, false);
let defaultSelected = false;
if (defaultAppInfo)
defaultSelected = defaultAppInfo.equal(this.selectedAppInfo);
this.appChooserRowSwitch.set_state(defaultSelected);
this.appChooserRowSwitch.set_sensitive(!defaultSelected);
}
_detectSingleContentType(fileItems) {
let mimetype = fileItems[0].attributeContentType;
for (let fileItem of fileItems) {
if (fileItem.attributeContentType !== mimetype)
return false;
}
return true;
}
show() {
this.appChooserDialog.show();
this.appChooserDialog.present_with_time(Gdk.CURRENT_TIME);
}
hide() {
this.appChooserDialog.hide();
}
finalize() {
this.appChooserDialog.destroy();
this.appChooserDialog = null;
this.builderObject = null;
}
getApplicationSelected() {
return new Promise(resolve => {
this.applicationSelectionComplete = resolve;
});
}
};

View File

@@ -0,0 +1,136 @@
/*
* Adw-DING Copyright (C) 2022, 2025 Sundeep Mediratta (smedius@gmail.com)
* Based on code original (C) Carlos Soriano and (c) Sergio Costas
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {Gio} from '../dependencies/gi.js';
import {FileItemIcon} from '../dependencies/localFiles.js';
import {_} from '../dependencies/gettext.js';
export {AppImageFileIcon};
const AppImageFileIcon = class extends FileItemIcon {
_updateMetadataFromFileInfo(fileInfo) {
super._updateMetadataFromFileInfo(fileInfo);
this._isAppImageFile =
this._attributeContentType === 'application/vnd.appimage';
this._trusted =
fileInfo.get_attribute_as_string('metadata::trusted') === 'true';
}
async onAllowDisallowLaunchingClicked() {
if (this._isAppImageFile)
this.metadataTrusted = !this.trustedAppImageFile;
await super.onAllowDisallowLaunchingClicked();
}
async _doOpenContext(context, fileList) {
if (this._isAppImageFile) {
try {
this._launchAppImageFile(context, fileList);
} catch (e) {}
return;
}
await super._doOpenContext(context, fileList);
}
_launchAppImageFile() {
if (this._writableByOthers || !this._attributeCanExecute) {
const title = _('Invalid Permissions on AppImage File');
const a = _('This AppImage File has incorrect Permissions.');
const aa = _('Right Click to edit Properties, then:');
let error = `${a} ${aa}\n`;
const b = _('Set Permissions, in');
const c = _('Others Access');
const d = _('Read Only');
const e = _('or');
const f = _('None');
const g = _('Enable option');
const h = _('Allow Executing File as a Program');
if (this._writableByOthers)
error += `\n${b} "${c}", "${d}" ${e} "${f}"`;
if (!this._attributeCanExecute)
error += `\n${g}, "${h}"`;
this._showerrorpopup(title, error);
return;
}
if (!this.trustedAppImageFile) {
const title = _('Untrusted AppImage File');
const a =
_('This AppImage file is not trusted, it can not be launched.');
const b = _('To enable launching, right-click, then:');
const c = _('enable');
const d = _('Allow Launching');
const error = `${a} ${b}\n\n${c} "${d}"`;
this._showerrorpopup(title, error);
return;
}
const appImageHandler =
Gio.AppInfo.get_all_for_type(this.attributeContentType);
if (appImageHandler.some(
app => {
if (app.get_name().toLowerCase().includes('appimagelauncher'))
return app.launch_uris([this.uri], null);
return false;
}
)
)
return;
this.DesktopIconsUtil.trySpawn(this._desktopDir.get_path(),
[this.path], null, false);
}
_addEmblemsToIconIfNeeded(iconPaintable, position = 0) {
let emblem = null;
let newIconPaintable = iconPaintable;
if (this.isAppImageFile && !this.trustedAppImageFile) {
emblem = Gio.ThemedIcon.new('icon-emblem-unreadable');
newIconPaintable =
this._addEmblem(newIconPaintable, emblem, position);
position += 1;
}
return super._addEmblemsToIconIfNeeded(newIconPaintable, position);
}
get isAppImageFile() {
return this._isAppImageFile;
}
get trustedAppImageFile() {
return this._isAppImageFile &&
this._attributeCanExecute &&
this.metadataTrusted &&
!this._desktopManager.writableByOthers &&
!this._writableByOthers;
}
};

217
ding/app/askRenamePopup.js Normal file
View File

@@ -0,0 +1,217 @@
/* eslint-disable object-curly-spacing */
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Copyright (C) 2019 Sergio Costas (rastersoft@gmail.com)
* Based on code original (C) Carlos Soriano
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {Gtk, GLib, Gio } from '../dependencies/gi.js';
import {_} from '../dependencies/gettext.js';
export {AskRenamePopup};
const AskRenamePopup = class {
constructor(
fileItem,
allowReturnOnSameName,
closeCB,
setPendingDropCoordinatesCB,
Data
) {
this.FileUtils = Data.FileUtils;
this.DesktopIconsUtil = Data.DesktopIconsUtil;
this.DBusUtils = Data.DBusUtils;
this.setPendingDropCoordinates = setPendingDropCoordinatesCB;
this._validateCancellable = new Gio.Cancellable();
this._closeCB = closeCB;
this._allowReturnOnSameName = allowReturnOnSameName;
this._desktopFile = Gio.File.new_for_path(
GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP));
this._fileItem = fileItem;
this._window = fileItem._grid._window;
this._popover = new Gtk.Popover();
this._popover.set_autohide(false);
let contentBox = new Gtk.Grid({
row_spacing: 6,
column_spacing: 6,
});
contentBox.set_margin_top(10);
contentBox.set_margin_bottom(10);
contentBox.set_margin_start(10);
contentBox.set_margin_end(10);
this._popover.set_child(contentBox);
let label = new Gtk.Label({
label: fileItem.isDirectory ? _('Folder name') : _('File name'),
justify: Gtk.Justification.LEFT,
halign: Gtk.Align.START,
});
contentBox.attach(label, 0, 0, 2, 1);
this._textArea = new Gtk.Entry();
this._textArea.text = fileItem.fileName;
contentBox.attach(this._textArea, 0, 1, 1, 1);
this._button =
new Gtk.Button(
{label: allowReturnOnSameName ? _('OK') : _('Rename')}
);
contentBox.attach(this._button, 1, 1, 1, 1);
this._buttonId =
this._button.connect(
'clicked',
this._do_rename.bind(this)
);
this._textAreaChangedId =
this._textArea.connect(
'changed',
() => {
this._validate()
.catch(e => console.error(e));
}
);
this._textAreaActivateId =
this._textArea.connect('activate', this._do_rename.bind(this));
this._textArea.set_activates_default(true);
this._popover.set_default_widget(this._textArea);
this._button.get_style_context().add_class('suggested-action');
contentBox.show();
this._popover.set_parent(this._window);
this._popover.set_pointing_to(fileItem.iconLocalWindowRectangle);
const menuGtkPosition =
fileItem
._grid
.getIntelligentPosition(
fileItem
._grid
.getGlobaltoLocalRectangle(fileItem.iconRectangle)
);
if (menuGtkPosition !== null)
this._popover.set_position(menuGtkPosition);
this._focusTracker = Gtk.EventControllerFocus.new();
this._popover.add_controller(this._focusTracker);
this._focusTrackerID =
this._focusTracker.connect('leave', this.close.bind(this));
this._popoverId =
this._popover.connect('closed', this.close.bind(this));
this._popover.popup();
this._validate().catch(e => console.error(e));
this._textArea.grab_focus_without_selecting();
this._textArea.select_region(
0,
this.DesktopIconsUtil
.getFileExtensionOffset(
fileItem.fileName,
{'isDirectory': fileItem.isDirectory}
)
.offset
);
}
async _validate() {
this._validateCancellable.cancel();
this._validateCancellable = new Gio.Cancellable();
let text = this._textArea.text;
if (!text.length || text.indexOf('/') !== -1) {
this._button.sensitive = false;
return;
}
if (text === this._fileItem.fileName) {
this._button.sensitive = !!this._allowReturnOnSameName;
return;
}
let sensitive = true;
try {
const finalFile = this._desktopFile.get_child(text);
if (
await this.FileUtils.queryExists(
finalFile,
this._validateCancellable
)
)
sensitive = false;
} catch (e) {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
return;
}
this._button.sensitive = sensitive;
}
_do_rename() {
if (!this._button.sensitive)
return;
let newFilePath =
GLib.build_filenamev(
[this._desktopFile.get_path(), this._textArea.text]
);
let newFile = Gio.File.new_for_path(newFilePath);
this.setPendingDropCoordinates(
newFile,
this._fileItem.savedCoordinates
);
this.DBusUtils.RemoteFileOperations.RenameURIRemote(
this._fileItem.file.get_uri(),
this._textArea.text
);
// popdown will trigger the 'close' signal, which,
// in turn, will call _closeCB()
this._popover.popdown();
}
close() {
this._validateCancellable.cancel();
this._button.disconnect(this._buttonId);
this._textArea.disconnect(this._textAreaActivateId);
this._textArea.disconnect(this._textAreaChangedId);
this._popover.disconnect(this._popoverId);
this._focusTracker.disconnect(this._focusTrackerID);
this._popover.unparent();
this._popover = null;
this._closeCB();
}
};

769
ding/app/autoAr.js Normal file
View File

@@ -0,0 +1,769 @@
/* eslint-disable no-template-curly-in-string */
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Copyright (C) 2022 Sergio Costas (sergio.costas@canonical.com)
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {Gtk, GLib, Gio, GnomeAutoar} from '../dependencies/gi.js';
import {_} from '../dependencies/gettext.js';
const fileProto = imports.system.version >= 17200
? Gio.File.prototype : Gio._LocalFilePrototype;
Gio._promisify(fileProto, 'make_directory_async');
const Signals = imports.signals;
export {AutoAr};
var AutoAr = class {
constructor(desktopManager) {
this._desktopManager = desktopManager;
this.FileUtils = desktopManager.FileUtils;
this._progressWindow = new Gtk.Window({
title: 'Archives Operations',
resizable: false,
deletable: false,
modal: false,
default_height: 100,
});
this._progressWindow.connect('close-request', () => {
return true;
});
this._progressContainer = new Gtk.Box({
spacing: 12,
margin_top: 15,
margin_bottom: 15,
margin_start: 30,
margin_end: 30,
halign: Gtk.Align.CENTER,
orientation: Gtk.Orientation.VERTICAL,
});
this._inhibitCookie = null;
this._progressElements = [];
const scroll = new Gtk.ScrolledWindow({
propagate_natural_width: true,
min_content_height: 300,
});
scroll.hscrollbar_policy = Gtk.PolicyType.NEVER;
scroll.vscrollbar_policy = Gtk.PolicyType.AUTOMATIC;
this._progressWindow.set_child(scroll);
const viewport = new Gtk.Viewport();
scroll.set_child(viewport);
viewport.set_child(this._progressContainer);
this._refreshExtensions();
}
checkAutoAr() {
if (GnomeAutoar === null) {
this._desktopManager.dbusManager.doNotify(_('AutoAr is not installed'),
_('To be able to work with compressed files, install file-roller and/or gir-1.2-gnomeAutoAr'));
}
return GnomeAutoar !== null;
}
_refreshExtensions() {
this._formats = [];
this._filters = [];
this._extensions = {};
this._combinedExtensions = {};
if (!GnomeAutoar)
return;
const lastFormat = GnomeAutoar.format_last();
const lastFilter = GnomeAutoar.filter_last();
for (let format = 0; format <= lastFormat; format++) {
try {
if (!GnomeAutoar.format_is_valid(format))
continue;
} catch (e) {
continue;
}
this._formats.push(format);
const extension = GnomeAutoar.format_get_extension(format);
if (!extension)
continue;
this._extensions[extension] = {
extension,
format,
filter: null,
};
}
for (let filter = 0; filter <= lastFilter; filter++) {
try {
if (!GnomeAutoar.filter_is_valid(filter))
continue;
} catch (e) {
continue;
}
this._filters.push(filter);
const extension = GnomeAutoar.filter_get_extension(filter);
if (!extension)
continue;
this._extensions[extension] = {
extension,
format: null,
filter,
};
}
for (let format of this._formats) {
for (let filter of this._filters) {
const extension = GnomeAutoar.format_filter_get_extension(format, filter);
if (!extension)
continue;
this._combinedExtensions[extension] = {
extension,
format,
filter,
};
}
}
}
extensionIsAvailable(extension) {
return (extension in this._extensions) || (extension in this._combinedExtensions);
}
getFormatAndFilterForExtension(extension) {
if (extension in this._extensions)
return this._extensions[extension];
if (extension in this._combinedExtensions)
return this._combinedExtensions[extension];
return null;
}
_getFormatAndFilterForFilename(fileName) {
for (let extension in this._combinedExtensions) {
if (fileName.endsWith(`.${extension}`))
return this._combinedExtensions[extension];
}
for (let extension in this._extensions) {
if (fileName.endsWith(`.${extension}`))
return this._extensions[extension];
}
return null;
}
fileIsCompressed(fileName) {
return this._getFormatAndFilterForFilename(fileName) !== null;
}
runToolAsync(autoArTool, cancellable) {
return new Promise((resolve, reject) => {
const connections = [];
connections.push(autoArTool.connect('cancelled', () => {
connections.forEach(c => autoArTool.disconnect(c));
reject(new GLib.Error(Gio.IOErrorEnum,
Gio.IOErrorEnum.CANCELLED,
'Operation was cancelled'));
}));
connections.push(autoArTool.connect('error', (holder, error) => {
connections.forEach(c => autoArTool.disconnect(c));
reject(error);
}));
connections.push(autoArTool.connect('completed', () => {
connections.forEach(c => autoArTool.disconnect(c));
resolve();
}));
autoArTool.start_async(cancellable);
});
}
extractFile(fileName) {
if (!this.checkAutoAr())
return;
const fullPath = GLib.build_filenamev([
GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP),
fileName,
]);
const formatFilter = this._getFormatAndFilterForFilename(fileName);
const extSize = formatFilter.extension.length;
const total = fullPath.length;
const folderName = fullPath.substring(0, total - extSize);
const folder = Gio.File.new_for_path(folderName);
const doExtract = new progressDialog(this, _('Extracting files'));
this._password = null;
doExtract.doExtractFile(fullPath, folder, folderName).catch(
e => console.error(e));
}
compressFileItems(fileList, destinationFolder) {
if (!this.checkAutoAr())
return;
new CompressDialog(this._desktopManager, fileList, destinationFolder);
}
compressFiles(fileList, outputFile, format, filter, password = null) {
if (!this.checkAutoAr())
return;
const doCompress = new progressDialog(this, _('Compressing files'));
doCompress.doCompressFiles(fileList, outputFile, format, filter, password).catch(
e => console.error(e));
}
notify(title, text) {
this._desktopManager.dbusManager.doNotify(title, text);
}
getProgressElements() {
return this._progressElements; // this._progressContainer.get_children();
}
removeProgressDialog(progressElement) {
this._progressElements = this._progressElements.filter(e => e !== progressElement);
if (!this._progressElements.length) {
this._progressWindow.hide();
if (this._inhibitCookie !== null) {
this._desktopManager.mainApp.uninhibit(this._inhibitCookie);
this._inhibitCookie = null;
}
}
progressElement.unparent();
progressElement = null;
this.emit('progress-elements-changed', this._progressElements);
}
addProgress(progressElement, message) {
this._progressContainer.append(progressElement);
if (!this._progressElements.length) {
this._inhibitCookie = this._desktopManager.mainApp.inhibit(null,
Gtk.ApplicationInhibitFlags.LOGOUT | Gtk.ApplicationInhibitFlags.SUSPEND,
message);
}
this._progressElements.push(progressElement);
this._progressWindow.show();
this._progressWindow.present();
this.emit('progress-elements-changed', this._progressElements);
}
};
Signals.addSignalMethods(AutoAr.prototype);
const progressDialog = class {
constructor(autoArClass, message) {
this._autoAr = autoArClass;
this.FileUtils = autoArClass.FileUtils;
this._waitingForPassword = false;
this._currentPassword = null;
this._buttonPromiseAccept = null;
this._container = new Gtk.Box({
spacing: 0,
halign: Gtk.Align.START,
orientation: Gtk.Orientation.VERTICAL,
});
this._processLabel = new Gtk.Label();
this._processBar = new Gtk.ProgressBar();
const container2 = new Gtk.Box({
spacing: 60,
margin_top: 15,
margin_bottom: 15,
margin_start: 15,
margin_end: 15,
halign: Gtk.Align.START,
orientation: Gtk.Orientation.HORIZONTAL,
});
const container3 = new Gtk.Box({
spacing: 10,
halign: Gtk.Align.START,
orientation: Gtk.Orientation.VERTICAL,
});
this._cancelButton = new Gtk.Button({label: _('Cancel')});
this._cancelButton.connect('clicked', () => {
if (this._buttonPromiseAccept) {
this._buttonPromiseAccept(false);
return;
}
this._cancellable.cancel();
});
this._passOkButton = new Gtk.Button({label: _('OK')});
this._passOkButton.get_style_context().add_class('suggested-action');
const passOKfunc = function () {
this._processBar.show();
this._passEntry.hide();
this._passOkButton.hide();
this._currentPassword = this._passEntry.get_text();
if (this._buttonPromiseAccept)
this._buttonPromiseAccept(true);
}.bind(this);
this._passOkButton.connect('clicked', passOKfunc);
this._passEntry = new Gtk.Entry({
placeholder_text: _('Enter a password here'),
input_purpose: Gtk.InputPurpose.PASSWORD,
visibility: false,
secondary_icon_name: 'view-conceal',
secondary_icon_activatable: true,
secondary_icon_sensitive: true,
});
container3.append(this._processLabel);
container3.append(this._processBar);
container3.append(this._passEntry);
container2.append(container3);
container2.append(this._passOkButton);
this._passOkButton.set_halign(Gtk.Align.END);
container2.append(this._cancelButton);
this._cancelButton.set_halign(Gtk.Align.END);
this._container.append(container2);
this._passEntry.connect('icon-release', () => {
this._passEntry.visibility = !this._passEntry.visibility;
});
this._passEntry.connect('activate', passOKfunc);
const separator = new Gtk.Separator({orientation: Gtk.Orientation.HORIZONTAL});
this._container.append(separator);
const updateSeparatorVisibility = () => {
const progressElements = this._autoAr.getProgressElements();
separator.visible = progressElements.length &&
this._container !== progressElements[progressElements.length - 1];
};
updateSeparatorVisibility();
this._elementsChangedId = this._autoAr.connect('progress-elements-changed',
updateSeparatorVisibility);
this._cancellable = new Gio.Cancellable();
this._autoAr.addProgress(this._container, message);
this._passEntry.hide();
this._passOkButton.hide();
}
async _cleanupFile(file, cancellable) {
if (!file.query_exists(null))
return;
this._processBar.set_fraction(0);
this._processLabel.set_label(_("Removing partial file '${outputFile}'").replace(
'${outputFile}', file.get_basename()));
this._removeTimer();
this._timer = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 250, () => {
this._processBar.pulse();
return true;
});
try {
await this.FileUtils.deleteFile(file, null, cancellable);
} catch (e) {
console.error(e, `Failed to remove ${file.get_path()}: ${e.message}`);
} finally {
this._removeTimer();
}
}
async doExtractFile(fullPath, folder, folderName, counter = 1) {
this._processLabel.set_label(_('Creating destination folder'));
this._processBar.pulse();
try {
await folder.make_directory_async(GLib.PRIORITY_DEFAULT, this._cancellable);
const info = new Gio.FileInfo();
info.set_attribute_uint32(Gio.FILE_ATTRIBUTE_UNIX_MODE, 0o700);
try {
await folder.set_attributes_async(info,
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_DEFAULT,
this._cancellable);
} catch (e) {
console.error(e, `Failed to set attributes to ${folder.get_path()}`);
}
} catch (e) {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
this._destroy();
return;
}
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS)) {
const newFolder = Gio.File.new_for_path(`${folderName} (${counter})`);
await this.doExtractFile(fullPath, newFolder, folderName, counter + 1);
return;
}
throw e;
}
this._processLabel.set_label(_("Extracting files into '${outputPath}'").replace(
'${outputPath}', folder.get_basename()));
const fullPathFile = Gio.File.new_for_path(fullPath);
const extractor = GnomeAutoar.Extractor.new(fullPathFile, folder);
extractor.set_output_is_dest(true);
if (extractor.set_passphrase && (this._currentPassword !== null))
extractor.set_passphrase(this._currentPassword);
this._removeTimer();
this._timer = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 250, () => {
this._processBar.pulse();
return true;
});
let progressTotal = -1;
const progressID = extractor.connect('progress', (holder, completedSize) => {
this._removeTimer();
if (progressTotal <= 0)
progressTotal = extractor.get_total_size();
if (progressTotal > 0)
this._processBar.set_fraction(completedSize / progressTotal);
});
try {
await this._autoAr.runToolAsync(extractor, this._cancellable);
this._autoAr.notify(_('Extraction completed'),
_("Extracting '${fullPathFile}' has been completed.").replace(
'${fullPathFile}', fullPathFile.get_basename()));
} catch (e) {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
this._cancellable = new Gio.Cancellable();
await this._cleanupFile(folder, this._cancellable);
this._autoAr.notify(_('Extraction cancelled'),
_("Extracting '${fullPathFile}' has been cancelled by the user.").replace(
'${fullPathFile}', fullPathFile.get_basename()));
} else {
if ((e.code === GnomeAutoar.PASSPHRASE_REQUIRED_ERRNO) && (e.domain === GnomeAutoar.Extractor.quark())) {
this._waitingForPassword = true;
this._processBar.hide();
this._passEntry.show();
this._passOkButton.show();
this._passOkButton.set_receives_default(true);
const tmpfile = Gio.File.new_for_path(fullPath);
this._processLabel.set_label(_('Passphrase required for ${filename}').replace('${filename}', tmpfile.get_basename()));
} else {
this._waitingForPassword = false;
this._autoAr.notify(_('Error during extraction'), e.message);
}
await this._cleanupFile(folder, this._cancellable);
}
} finally {
this._removeTimer();
extractor.disconnect(progressID);
if (!this._waitingForPassword)
this._destroy();
}
if (this._waitingForPassword) {
const retval = await this._waitButtons();
this._buttonPromiseAccept = null;
this._waitingForPassword = false;
if (retval === true)
await this.doExtractFile(fullPath, folder, folderName);
}
}
_waitButtons() {
return new Promise(accept => {
this._buttonPromiseAccept = accept;
});
}
async doCompressFiles(fileList, outputFile, format, filter, password = null) {
const output = Gio.File.new_for_path(outputFile);
this._processLabel.set_label(_("Compressing files into '${outputFile}'").replace(
'${outputFile}', output.get_basename()));
const compressor = GnomeAutoar.Compressor.new(fileList, output, format, filter, false);
compressor.set_output_is_dest(true);
if (password)
compressor.set_passphrase(password);
const progressID = compressor.connect('progress', () => this._processBar.pulse());
try {
await this._autoAr.runToolAsync(compressor, this._cancellable);
this._autoAr.notify(_('Compression completed'),
_("Compressing files into '${outputFile}' has been completed.").replace(
'${outputFile}', output.get_basename()));
} catch (e) {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS)) {
this._autoAr.notify(_('Cancelled compression'),
_("The output file '${outputFile}' already exists.").replace(
'${outputFile}', output.get_basename()));
} else {
this._cancellable = new Gio.Cancellable();
await this._cleanupFile(output, this._cancellable);
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
this._autoAr.notify(_('Cancelled compression'),
_("Compressing files into '${outputFile}' has been cancelled by the user.").replace(
'${outputFile}', output.get_basename()));
} else {
this._autoAr.notify(_('Error during compression'), e.message);
}
}
} finally {
compressor.disconnect(progressID);
this._destroy();
}
}
_removeTimer() {
if (this._timer) {
GLib.source_remove(this._timer);
this._timer = 0;
}
}
_destroy() {
this._autoAr.disconnect(this._elementsChangedId);
this._cancellable.cancel();
this._autoAr.removeProgressDialog(this._container);
}
};
const CompressDialog = class {
constructor(desktopManager, fileList, destinationFolder) {
this.Enums = desktopManager.Enums;
this.Prefs = desktopManager.Prefs;
this._fileList = [];
for (let file of fileList)
this._fileList.push(file.file);
this._desktopManager = desktopManager;
this._destinationFolder = destinationFolder;
this._dialog = new Gtk.Dialog({
title: _('Create archive'),
resizable: false,
modal: true,
use_header_bar: true,
default_width: 500,
default_height: 210,
});
const container = this._dialog.get_content_area();
container.orientation = Gtk.Orientation.VERTICAL;
container.margin_top = 30;
container.margin_bottom = 30;
container.margin_start = 30;
container.margin_end = 30;
container.width_request = 390;
container.halign = Gtk.Align.CENTER;
container.spacing = 6;
if (this.Prefs.nautilusCompression)
this._selectedType = this.Prefs.nautilusCompression.get_enum('default-compression-format');
else
this._selectedType = this.Enums.CompressionType.ZIP;
const archiveLabel = new Gtk.Label({
label: `<b>${_('Archive name')}</b>`,
xalign: 0,
use_markup: true,
});
container.append(archiveLabel);
const box1 = new Gtk.Box({
spacing: 12,
orientation: Gtk.Orientation.HORIZONTAL,
});
this._nameEntry = new Gtk.Entry({
hexpand: true,
width_chars: 30,
});
this._extensionDropdown = new Gtk.Button();
const extensionContainer = new Gtk.Box({
spacing: 2,
orientation: Gtk.Orientation.HORIZONTAL,
});
this._extensionLabel = new Gtk.Label();
this._extensionLock = new Gtk.Image({icon_name: 'dialog-password'});
extensionContainer.append(this._extensionLabel);
extensionContainer.append(this._extensionLock);
this._extensionDropdown.set_child(extensionContainer);
this._extensionPopover = new Gtk.Popover();
this._extensionPopover.set_parent(this._extensionDropdown);
this._extensionPopoverContainer = new Gtk.Box({
spacing: 4,
orientation: Gtk.Orientation.VERTICAL,
});
this._extensionPopover.set_child(this._extensionPopoverContainer);
this._passLabel = new Gtk.Label({
label: _('Password'),
margin_top: 6,
xalign: 0,
});
this._passEntry = new Gtk.PasswordEntry({placeholder_text: _('Enter a password here')});
this._passEntry.set_show_peek_icon(true);
container.append(box1);
box1.append(this._nameEntry);
box1.append(this._extensionDropdown);
container.append(this._passLabel);
container.append(this._passEntry);
this._okButton = this._dialog.add_button(_('Create'), Gtk.ResponseType.ACCEPT);
this._okButton.get_style_context().add_class('suggested-action');
this._okButton.set_receives_default(true);
this._cancelButton = this._dialog.add_button(_('Cancel'), Gtk.ResponseType.CANCEL);
this._cancelButton.set_receives_default(true);
this._fillComboBox();
this._dialog.show();
this._updateStatus();
this._extensionDropdown.connect('clicked', () => {
this._extensionPopoverContainer.show();
this._extensionPopover.popup();
for (let index in this._compressOptions) {
const data = this._compressOptions[index];
data.selected_icon.visible = index === this._selectedType;
}
});
this._nameEntry.connect('changed', () => this._updateStatus());
this._passEntry.connect('changed', () => this._updateStatus());
this._nameEntry.connect('activate', () => this._entryActivated());
this._passEntry.connect('activate', () => this._entryActivated());
this._dialog.connect('response', (dialog, id) => {
if (id === Gtk.ResponseType.ACCEPT) {
const data =
this._desktopManager
.autoAr.getFormatAndFilterForExtension(
this._compressOptions[this._selectedType]
.extension
);
const outputFile = GLib.build_filenamev([
this._destinationFolder,
this._nameEntry.get_text() + data.extension,
]);
const password = this._passEntry.get_text();
this._desktopManager
.autoAr
.compressFiles(
this._fileList,
outputFile,
data.format,
data.filter,
password
);
}
this._dialog.close();
this._extensionPopover.unparent();
this._dialog.destroy();
});
}
_entryActivated() {
this._updateStatus();
if (this._okButton.sensitive)
this._dialog.response(Gtk.ResponseType.ACCEPT);
}
_updateStatus() {
if (this.Prefs.nautilusCompression)
this.Prefs.nautilusCompression.set_enum('default-compression-format', this._selectedType);
const label = this._compressOptions[this._selectedType].extension;
this._extensionLabel.label = label;
this._extensionLock.visible = this._compressOptions[this._selectedType].password;
const password = this._compressOptions[this._selectedType].password;
const outputfile = this._nameEntry.get_text() + label;
this._passLabel.visible = password;
this._passEntry.visible = password;
let context = this._nameEntry.get_style_context();
this._okButton.sensitive = true;
if (this._desktopManager._displayList.map(f => f.fileName).includes(outputfile)) {
this._okButton.sensitive = false;
if (!context.has_class('not-found'))
context.add_class('not-found');
} else if (context.has_class('not-found')) {
context.remove_class('not-found');
}
if (password && (this._passEntry.get_text().length === 0))
this._okButton.sensitive = false;
if (this._nameEntry.get_text_length() === 0)
this._okButton.sensitive = false;
}
_fillComboBox() {
this._compressOptions = {};
this._addComboEntry(this.Enums.CompressionType.ZIP, {
extension: '.zip',
id: 'zip',
description: _('Compatible with all operating systems.'),
password: false,
});
this._addComboEntry(this.Enums.CompressionType.ENCRYPTED_ZIP, {
extension: '.zip',
id: 'encryptedzip',
description: _('Password protected .zip, must be installed on Windows and Mac.'),
password: true,
});
this._addComboEntry(this.Enums.CompressionType.TAR_XZ, {
extension: '.tar.xz',
id: 'tar.xz',
description: _('Smaller archives but Linux and Mac only.'),
password: false,
});
this._addComboEntry(this.Enums.CompressionType.SEVEN_ZIP, {
extension: '.7z',
id: '7z',
description: _('Smaller archives but must be installed on Windows and Mac.'),
password: false,
});
}
_addComboEntry(type, data) {
this._compressOptions[type] = data;
if (!this._desktopManager.autoAr.extensionIsAvailable(data.extension))
return;
const container = new Gtk.Box({orientation: Gtk.Orientation.VERTICAL});
const container2 = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL});
const container3 = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL});
container3.append(new Gtk.Label({
label: data.extension,
justify: Gtk.Justification.LEFT,
xalign: 0,
}));
if (data.password)
container3.append(new Gtk.Image({icon_name: 'dialog-password'}));
container.append(container3);
container.append(new Gtk.Label({
label: data.description,
justify: Gtk.Justification.LEFT,
xalign: 0,
}));
const button = new Gtk.Button();
container2.append(container);
data.selected_icon = new Gtk.Image({icon_name: 'emblem-default'});
container2.append(data.selected_icon);
button.set_child(container2);
this._extensionPopoverContainer.append(button);
button.connect('clicked', () => {
this._selectedType = type;
this._extensionPopover.popdown();
this._updateStatus();
});
}
};

Binary file not shown.

305
ding/app/desktopFileIcon.js Normal file
View File

@@ -0,0 +1,305 @@
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Adw-DING Copyright (C) 2022, 2025 Sundeep Mediratta (smedius@gmail.com)
* Based on code original (C) Carlos Soriano and (c) Sergio Costas
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {Gdk, Gio, GLib, DesktopAppInfo} from '../dependencies/gi.js';
import {FileItemIcon} from '../dependencies/localFiles.js';
import {_} from '../dependencies/gettext.js';
export {DesktopFileIcon};
const DesktopFileIcon = class extends FileItemIcon {
_updateMetadataFromFileInfo(fileInfo) {
super._updateMetadataFromFileInfo(fileInfo);
this._isDesktopFile =
this._attributeContentType === 'application/x-desktop';
if (this._isDesktopFile && this._writableByOthers) {
console.log(
`desktop-icons: File ${this._displayName} is writable` +
'by others - will not allow launching'
);
}
if (this._isDesktopFile) {
try {
this._desktopFile = DesktopAppInfo.new_from_filename(
this._file.get_path()
);
if (!this._desktopFile) {
console.log(
`Couldnt parse ${this._displayName} as a desktop` +
' file, will treat it as a regular file.'
);
this._isValidDesktopFile = false;
} else {
this._isValidDesktopFile = true;
}
} catch (e) {
console.log(`Error reading Desktop file ${this.uri}: ${e}`);
}
} else {
this._isValidDesktopFile = false;
}
if (this._isValidDesktopFile)
this._execLine = null;
this._trusted =
fileInfo.get_attribute_as_string('metadata::trusted') === 'true';
this._getActions();
}
_getActions() {
if (!this.trustedDesktopFile)
return;
this.desktopAppInfo =
DesktopAppInfo.new_from_filename(this.path);
this.actionMap = new Map();
const actions = this.desktopAppInfo.list_actions();
actions.forEach(action => {
const actionName =
this.desktopAppInfo.get_action_name(action);
this.actionMap.set(actionName, action);
});
}
_makeActionMenu() {
this._actionmenu = Gio.Menu.new();
for (const [actionName, action] of this.actionMap) {
const variant =
new GLib.Variant('as', [this.path, actionName, action]);
const menuItem = Gio.MenuItem.new(actionName, null);
menuItem.set_action_and_target_value('app.desktopAction', variant);
this._actionmenu.append_item(menuItem);
}
}
getMenu() {
if (!this.hasActions)
return null;
this._makeActionMenu();
return this._actionmenu;
}
async _doOpenContext(context, fileList = []) {
if (this._isDesktopFile) {
try {
this._launchDesktopFile(context, fileList);
} catch (e) {}
return;
}
await super._doOpenContext(context, fileList);
}
_launchDesktopFile(context, fileList) {
if (this._desktopManager.writableByOthers) {
const title = _('The Displayed Desktop is writable by others');
const error =
_(
'.deskop files cannot be launched from this Desktop' +
' as the Desktop Folder is writable by other users.\n\n' +
'Please check the permissions of this Desktop Folder,' +
' and make sure it is not writable by others.'
);
this._showerrorpopup(title, error);
return;
}
if (!this._isValidDesktopFile) {
const title = _('Broken Desktop File');
const error =
_(
'This .desktop file has errors or points to a program' +
' without permissions. It can not be executed.\n\n' +
'Edit the file to set the correct executable Program.'
);
this._showerrorpopup(title, error);
return;
}
if (this._writableByOthers || !this._attributeCanExecute) {
const title = _('Invalid Permissions on Desktop File');
const a = _('This Desktop File has incorrect Permissions.');
const aa = _('Right Click to edit Properties, then:');
let error = `${a} ${aa}\n`;
const b = _('Set Permissions, in');
const c = _('Others Access');
const d = _('Read Only');
const e = _('or');
const f = _('None');
const g = _('Enable option');
const h = _('Allow Executing File as a Program');
if (this._writableByOthers)
error += `\n${b} "${c}", "${d}" ${e} "${f}"`;
if (!this._attributeCanExecute)
error += `\n${g}, "${h}"`;
this._showerrorpopup(title, error);
return;
}
if (!this.trustedDesktopFile) {
const title = _('Untrusted Desktop File');
const a =
_('This Desktop file is not trusted, it can not be launched.');
const b = _('To enable launching, right-click, then:');
const c = _('enable');
const d = _('Allow Launching');
const error = `${a} ${b}\n\n${c} "${d}"`;
this._showerrorpopup(title, error);
return;
}
let object =
this.DesktopIconsUtil.checkAppOpensFileType(
this._desktopFile,
fileList[0],
null
);
if (this.trustedDesktopFile &&
(!fileList.length || object.canopenFile)
) {
this._desktopFile.launch_uris_as_manager(
fileList,
context,
GLib.SpawnFlags.SEARCH_PATH,
null,
null
);
} else if (this.trustedDesktopFile && !object.canopenFile) {
const Appname = object.Appname;
const title = _('Could not open File');
const error =
_('{appName} can not open files of this Type!')
.replace('{appName}', Appname);
this._showerrorpopup(title, error);
}
}
_updateName() {
if (this._isValidDesktopFile &&
!this._desktopManager.writableByOthers &&
!this._writableByOthers &&
this.trustedDesktopFile
)
this._setFileName(this._desktopFile.get_locale_string('Name'));
else
this._setFileName(this._getVisibleName());
this._setAccesibilityName();
}
_handleDroppedUris(
X, Y,
x, y,
fileList,
_gdkDropAction,
_localDrop,
_event
) {
if (this._isValidDesktopFile) {
// open the desktop file with these dropped files as the arguments
this.doOpen(fileList);
return Gdk.DragAction.COPY;
} else {
return false;
}
}
_dropCapable() {
if (this._isValidDesktopFile)
return true;
else
return false;
}
_addEmblemsToIconIfNeeded(iconPaintable, position = 0) {
let emblem = null;
let newIconPaintable = iconPaintable;
if (this._isDesktopFile &&
(!this._isValidDesktopFile || !this.trustedDesktopFile)
) {
emblem = Gio.ThemedIcon.new('icon-emblem-unreadable');
newIconPaintable =
this._addEmblem(newIconPaintable, emblem, position);
position += 1;
}
return super._addEmblemsToIconIfNeeded(newIconPaintable, position);
}
async onAllowDisallowLaunchingClicked() {
if (this._isDesktopFile)
this.metadataTrusted = !this.trustedDesktopFile;
await super.onAllowDisallowLaunchingClicked();
}
get canRename() {
return !this.trustedDesktopFile;
}
get displayName() {
if (this.trustedDesktopFile)
return this._desktopFile.get_name();
return super.displayName;
}
get isDesktopFile() {
return this._isDesktopFile;
}
get trustedDesktopFile() {
return this._isValidDesktopFile &&
this._attributeCanExecute &&
this.metadataTrusted &&
!this._desktopManager.writableByOthers &&
!this._writableByOthers;
}
get isValidDesktopFile() {
return this._isValidDesktopFile;
}
get hasActions() {
return this.trustedDesktopFile && this.actionMap.size > 0;
}
};

View File

@@ -0,0 +1,660 @@
/* ADW-DING: Desktop Icons New Generation for GNOME Shell
*
* Adw/Gtk4 Port Copyright (C) 2025 Sundeep Mediratta (smedius@gmail.com)
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {IconCreator} from '../dependencies/localFiles.js';
import {DesktopFolderUtils} from '../dependencies/localFiles.js';
import {Gio, GLib} from '../dependencies/gi.js';
import {_} from '../dependencies/gettext.js';
export {DesktopMonitor};
const DesktopMonitor = class extends DesktopFolderUtils {
constructor(desktopManager) {
super();
this.desktopManager = desktopManager;
this.mainApp = desktopManager.mainApp;
this.DesktopIconsUtil = desktopManager.DesktopIconsUtil;
this.desktopActions = desktopManager.desktopActions;
this.dbusManager = desktopManager.dbusManager;
this.windowManager = desktopManager.windowManager;
this.Prefs = desktopManager.Prefs;
this.FileUtils = desktopManager.FileUtils;
this.Enums = desktopManager.Enums;
this._desktopFilesChanged = false;
this._readingDesktopFiles = false;
this._fileList = [];
this._forcedExit = false;
this._writableByOthers = false;
this._updateWritableByOthers().catch(e => console.error(e));
this._createDesktopChangeActions();
this._monitorDesktopDirChanges();
this._monitorDesktopChanges();
this._monitorVolumes();
this.DBusUtils = this.desktopManager.DBusUtils;
this.DBusUtils.GtkVfsMetadata.connectSignalToProxy(
'AttributeChanged',
this._metadataChanged.bind(this)
);
this._updateFileList().catch(e => console.error(e));
}
_createDesktopChangeActions() {
const changeDesktop = Gio.SimpleAction.new('changeDesktop', null);
changeDesktop.connect('activate', () => {
this.changeDesktop();
});
this.mainApp.add_action(changeDesktop);
this.restoreDefaultDesktopAction =
Gio.SimpleAction.new('restoreDefaultDesktop', null);
this.restoreDefaultDesktopAction.connect('activate', () => {
this.restoreDefaultDesktop();
});
this.mainApp.add_action(this.restoreDefaultDesktopAction);
this.restoreDefaultDesktopAction
.set_enabled(!this.isDefaultDesktop);
}
stopMonitoring() {
if (this._monitorDesktopCancellable)
this._monitorDesktopCancellable.cancel();
this._forcedExit = true;
if (this._desktopEnumerateCancellable)
this._desktopEnumerateCancellable.cancel();
super._stopMonitoring();
}
onDesktopFolderChanged(newDesktopDir) {
const fileType =
newDesktopDir.query_file_type(
Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
null
);
if (fileType === Gio.FileType.SYMBOLIC_LINK) {
const header = _('Desktop Folder Change Failed');
const text = _('The new Desktop Folder is a Symbolic Link');
this.dbusManager.doNotify(header, text);
return;
} else if (fileType !== Gio.FileType.DIRECTORY) {
const header = _('Desktop Folder Change Failed');
const text =
_('The new Desktop Folder does not exist or is not a Folder!');
this.dbusManager.doNotify(header, text);
return;
}
if (newDesktopDir.get_path() ===
this._desktopDir.get_path()
)
return;
const header = _('Desktop Folder Changed');
const text = _('Switching to new Desktop...');
this.dbusManager.doNotify(header, text);
this._desktopDir = newDesktopDir;
this.restoreDefaultDesktopAction
.set_enabled(!this.isDefaultDesktop);
this._updateWritableByOthers()
.catch(e => console.error(e));
this._desktops.forEach(d => d.unsetErrorState());
this._updateFileList().catch(e => console.error(e));
this._monitorDesktopChanges();
}
async _updateWritableByOthers() {
try {
const info =
await this._desktopDir.query_info_async(
Gio.FILE_ATTRIBUTE_UNIX_MODE,
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_LOW,
null
);
this.unixMode =
info.get_attribute_uint32(Gio.FILE_ATTRIBUTE_UNIX_MODE);
let writableByOthers =
(this.unixMode & this.Enums.UnixPermissions.S_IWOTH) !== 0;
if (writableByOthers !== this._writableByOthers) {
this._writableByOthers = writableByOthers;
if (this._writableByOthers) {
console.log('desktop-icons: The desktop is writable by' +
' others. Not allowing launching any desktop files.'
);
}
return true;
} else {
return false;
}
} catch (e) {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) {
this._writableByOthers = true;
return true;
}
throw e;
}
}
_monitorDesktopChanges() {
const cancellable = new Gio.Cancellable();
if (this._monitorDesktopCancellable)
this._monitorDesktopCancellable.cancel();
this._monitorDesktopCancellable = cancellable;
this._monitorDesktopDir =
this._desktopDir.monitor_directory(
Gio.FileMonitorFlags.WATCH_MOVES,
cancellable
);
this._monitorDesktopDir.set_rate_limit(1000);
const monitorID = this._monitorDesktopDir.connect(
'changed',
(obj, file, otherFile, eventType) => {
this._updateFileListIfChanged(file, otherFile, eventType)
.catch(e => console.error(e));
}
);
cancellable.connect(
() => {
this._monitorDesktopDir.disconnect(monitorID);
this._monitorDesktopDir.cancel();
this._monitorDesktopDir = null;
this.monitorDesktopCancellable = null;
}
);
}
_monitorVolumes() {
this._volumeMonitor = Gio.VolumeMonitor.get();
this._volumeMonitor.connect(
'mount-added',
() => {
this.onMountAdded();
}
);
this._volumeMonitor.connect(
'mount-removed',
() => {
GLib.timeout_add(
GLib.PRIORITY_DEFAULT,
500,
() => {
this.onMountRemoved();
return GLib.SOURCE_REMOVE;
}
);
}
);
}
async _updateFileListIfChanged(file, otherFile, eventType) {
if (eventType === Gio.FileMonitorEvent.CHANGED) {
// use only CHANGES_DONE_HINT
return;
}
if (!this.Prefs.showHidden && (file.get_basename()[0] === '.')) {
// If the file is not visible, we don't need to refresh the desktop
// Unless it is a hidden file being renamed to visible
if (!otherFile || (otherFile.get_basename()[0] === '.'))
return;
}
switch (eventType) {
case Gio.FileMonitorEvent.MOVED_IN:
case Gio.FileMonitorEvent.MOVED_CREATED:
/* Remove the coordinates that could exist to avoid conflicts
between files that are already in the desktop and the new one
*/
try {
const info = new Gio.FileInfo();
info.set_attribute_string(
'metadata::desktop-icon-position', ''
);
file.set_attributes_async(
info,
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_LOW,
null
);
} catch (e) {
// can happen if a file is created and deleted very fast
}
break;
case Gio.FileMonitorEvent.ATTRIBUTE_CHANGED:
/* The desktop is what changed, and not a file inside it */
if (file.get_uri() === this._desktopDir.get_uri()) {
if (await this._updateWritableByOthers()) {
try {
await this._updateFileList();
} catch (e) {
console.error(
e,
'Exception while updating desktop from' +
` Directory Monitor attribute change: ${e.message}`
);
}
}
return;
}
break;
}
try {
await this._updateFileList();
} catch (e) {
console.error(
e,
'Exception while updating desktop from Directory Monitor: ' +
`${e.message}`
);
}
}
async _updateFileList() {
if (this._readingDesktopFiles) {
// just notify that the files changed while being read from disk.
this._desktopFilesChanged = true;
if (this._desktopEnumerateCancellable && !this._forceDraw) {
this._desktopEnumerateCancellable.cancel();
this._desktopEnumerateCancellable = null;
}
return;
}
this._readingDesktopFiles = true;
this._forceDraw = false;
this._lastDesktopUpdateRequest = GLib.get_monotonic_time();
let fileList;
while (true) {
this._desktopFilesChanged = false;
try {
// eslint-disable-next-line no-await-in-loop
fileList = await this._doReadAsync();
} catch (e) {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) {
fileList = [];
break;
}
throw e;
}
if (this._forcedExit)
return;
if (fileList !== null) {
if (!this._desktopFilesChanged)
break;
if (this._forceDraw) {
this._fileList = fileList;
this.desktopManager.refreshDesktop();
this._lastDesktopUpdateRequest = GLib.get_monotonic_time();
}
}
// eslint-disable-next-line no-await-in-loop
await this.DesktopIconsUtil.waitDelayMs(500);
if (
(GLib.get_monotonic_time() - this._lastDesktopUpdateRequest) >
1000000
)
this._forceDraw = true;
else
this._forceDraw = false;
}
this._readingDesktopFiles = false;
this._forceDraw = false;
this._fileList = fileList;
this.desktopManager.refreshDesktop();
}
async _doReadAsync() {
if (this._desktopEnumerateCancellable)
this._desktopEnumerateCancellable.cancel();
const cancellable = new Gio.Cancellable();
this._desktopEnumerateCancellable = cancellable;
try {
const fileList = [];
const extraFoldersItems =
this.DesktopIconsUtil.getExtraFolders().map(
async ([newFolder, fileTypeEnum]) => {
try {
if (imports.system.version < 17200) {
Gio._promisify(
newFolder.constructor.prototype,
'query_info_async'
);
}
const newFolderInfo =
await newFolder.query_info_async(
this.Enums.DEFAULT_ATTRIBUTES,
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_DEFAULT,
cancellable
);
fileList.push(
new IconCreator(
this.desktopManager,
newFolder,
newFolderInfo,
fileTypeEnum,
null
)
);
} catch (e) {
if (e.matches(
Gio.IOErrorEnum,
Gio.IOErrorEnum.CANCELLED)
)
throw e;
console.error(e,
`Failed with ${e.message} while adding` +
` extra folder ${newFolder.get_uri()}`
);
}
}
);
const getLocalFilesInfos =
async () => {
const childrenInfo =
await this.FileUtils.enumerateDir(
this._desktopDir,
cancellable,
GLib.PRIORITY_DEFAULT,
this.Enums.DEFAULT_ATTRIBUTES
);
childrenInfo?.forEach(info => {
const fileItem =
new IconCreator(
this.desktopManager,
this._desktopDir.get_child(info.get_name()),
info,
this.Enums.FileType.NONE,
null
);
if (fileItem.isHidden && !this.Prefs.showHidden) {
/* if there are hidden files in the desktop and the
user doesn't want to show them, remove the
coordinates. This ensures that if the user enables
showing them, they won't fight with other icons
for the same place
*/
if (fileItem.savedCoordinates) {
// only overwrite them if needed
fileItem.savedCoordinates = null;
}
return;
}
fileItem.savedCoordinates =
fileItem.savedCoordinates ?? null;
fileItem.dropCoordinates =
fileItem.dropCoordinates ?? null;
if (fileItem.savedCoordinates === null ||
fileItem.dropCoordinates === null
) {
const basename = fileItem.file.get_basename();
this._checkBasenameInPending(fileItem, basename);
}
fileList.push(fileItem);
});
};
const mountsItems =
this.DesktopIconsUtil.getMounts(this._volumeMonitor).map(
async ([newFolder, fileTypeEnum, gioMount]) => {
try {
if (imports.system.version < 17200) {
Gio._promisify(
newFolder.constructor.prototype,
'query_info_async'
);
}
const newFolderInfo =
await newFolder.query_info_async(
this.Enums.DEFAULT_ATTRIBUTES,
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_DEFAULT,
cancellable
);
fileList.push(
new IconCreator(
this.desktopManager,
newFolder,
newFolderInfo,
fileTypeEnum,
gioMount
)
);
} catch (e) {
if (e.matches(
Gio.IOErrorEnum,
Gio.IOErrorEnum.CANCELLED)
)
throw e;
console.error(e,
`Failed with ${e.message} while ` +
`adding volume ${newFolder}`
);
}
});
await Promise.all(
[
getLocalFilesInfos(),
...extraFoldersItems,
...mountsItems,
]
);
if (this._desktopFilesChanged && !this._forceDraw)
return null;
return fileList;
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
console.error(e,
'Failed to read contents of' +
`${this._desktopDir.get_path()}`
);
}
return null;
} finally {
if (cancellable === this._desktopEnumerateCancellable)
this._desktopEnumerateCancellable = null;
}
}
_checkBasenameInPending(fileItem, basename) {
if (basename in this._pendingSelfCopyFiles) {
if (fileItem.savedCoordinates === null) {
fileItem.savedCoordinates =
this._pendingSelfCopyFiles[basename];
}
delete this._pendingSelfCopyFiles[basename];
return;
}
if (basename in this._pendingDropFiles) {
fileItem.dropCoordinates = this._pendingDropFiles[basename];
delete this._pendingDropFiles[basename];
return;
}
const regex = /\(.*\)[^()]*$/;
let basenameStart;
const lastParenthesisPosition = basename.search(regex);
if (lastParenthesisPosition > 1) {
basenameStart = basename.slice(0, lastParenthesisPosition - 1);
if (basenameStart) {
for (let fileName of Object.keys(this._pendingDropFiles)) {
if (fileName.startsWith(basenameStart)) {
fileItem.dropCoordinates =
this._pendingDropFiles[fileName];
delete this._pendingDropFiles[fileName];
}
}
}
}
}
_metadataChanged(proxy, nameOwner, args) {
const filepath = GLib.build_filenamev([GLib.get_home_dir(), args[1]]);
if (this._desktopDir.get_path() === GLib.path_get_dirname(filepath)) {
for (let fileItem of this._fileList) {
if (fileItem.path === filepath) {
fileItem.updatedMetadata();
break;
}
}
}
}
onMountAdded() {
this._updateFileList().catch(e => {
console.log(
'Exception while updating Desktop after a' +
` mount was added: ${e.message}\n${e.stack}`
);
});
}
onMountRemoved() {
this._updateFileList().catch(e => {
console.log(
'Exception while updating Desktop after a ' +
`mount was removed: ${e.message}\n${e.stack}`
);
});
}
fileExistsOnDesktop(searchName) {
const listOfFileNamesOnDesktop = this._fileList.map(f => f.fileName);
if (listOfFileNamesOnDesktop.includes(searchName))
return true;
else
return false;
}
getDesktopUniqueFileName(fileName) {
let fileParts = this.DesktopIconsUtil.getFileExtensionOffset(fileName);
let i = 0;
let newName = fileName;
while (this.fileExistsOnDesktop(newName)) {
i += 1;
newName = `${fileParts.basename} ${i}${fileParts.extension}`;
}
return newName;
}
async getFileList() {
const fileList = await this._doReadAsync();
this._fileList = fileList;
return fileList;
}
async reLoadFileList() {
await this._updateFileList().catch(e => logError(e));
}
get fileList() {
return this._fileList;
}
get _desktops() {
return this.windowManager.desktops;
}
get _pendingDropFiles() {
return this.desktopManager.pendingDropFiles;
}
get _pendingSelfCopyFiles() {
return this.desktopManager.pendingSelfCopyFiles;
}
get desktopDir() {
return this._desktopDir;
}
};

3141
ding/app/desktopGrid.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,81 @@
/* Adw-DING: Desktop Icons New Generation for GNOME Shell
*
* Gtk4 Port Copyright (C) 2025 Sundeep Mediratta (smedius@gmail.com)
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {Gio} from '../dependencies/gi.js';
import {_} from '../dependencies/gettext.js';
import {
Enums,
SpecialFolderIcon,
VolumeIcon,
SymLinkIcon,
DesktopFileIcon,
AppImageFileIcon,
FileItemIcon
} from '../dependencies/localFiles.js';
export {IconCreator};
const IconCreator = class {
constructor(desktopManager, file, fileInfo, fileTypeEnum, gioMount) {
const isSymLink = fileInfo.get_attribute_boolean(
Gio.FILE_ATTRIBUTE_STANDARD_IS_SYMLINK);
const attributeContentType = fileInfo.get_content_type();
let BaseType;
switch (attributeContentType) {
case 'application/x-desktop':
BaseType = DesktopFileIcon;
break;
case 'application/vnd.appimage':
BaseType = AppImageFileIcon;
break;
default:
BaseType = FileItemIcon;
}
if (fileTypeEnum === Enums.FileType.USER_DIRECTORY_HOME ||
fileTypeEnum === Enums.FileType.USER_DIRECTORY_TRASH)
BaseType = SpecialFolderIcon;
if (fileTypeEnum === Enums.FileType.EXTERNAL_DRIVE)
BaseType = VolumeIcon;
if (!isSymLink) {
return new BaseType(
desktopManager,
file,
fileInfo,
fileTypeEnum,
gioMount
);
} else {
return new SymLinkIcon(
BaseType,
desktopManager,
file,
fileInfo,
fileTypeEnum,
gioMount
);
}
}
};

1037
ding/app/desktopIconItem.js Normal file

File diff suppressed because it is too large Load Diff

1874
ding/app/desktopManager.js Normal file

File diff suppressed because it is too large Load Diff

1191
ding/app/desktopMenu.js Normal file

File diff suppressed because it is too large Load Diff

987
ding/app/dragManager.js Normal file
View File

@@ -0,0 +1,987 @@
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Adw/Gtk4 Port Copyright (C) 2025 Sundeep Mediratta (smedius@gmail.com)
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {Gtk, Gdk, Gio, GLib} from '../dependencies/gi.js';
import {_} from '../dependencies/gettext.js';
export {DragManager};
const DragManager = class {
constructor(desktopManager) {
this._desktopManager = desktopManager;
this._mainApp = this._desktopManager.mainApp;
this._FileUtils = desktopManager.FileUtils;
this._DesktopIconsUtil = desktopManager.DesktopIconsUtil;
this._DBusUtils = desktopManager.DBusUtils;
this._Prefs = desktopManager.Prefs;
this._GnomeShellDragDrop = this._desktopManager.GnomeShellDragDrop;
this._Enums = this._desktopManager.Enums;
this._dbusManager = this._desktopManager.dbusManager;
this._pendingDropFiles = {};
this._pendingSelfCopyFiles = {};
this.pointerX = 0;
this.pointerY = 0;
this._dragList = null;
this.dragItem = null;
this.rubberBand = false;
this.localDragOffset = [0, 0];
}
// Drag and Drop local Methods
_saveCurrentFileCoordinatesForUndo(fileList = null) {
if (this._Prefs.keepArranged || this._Prefs.keepStacked)
return;
this._pendingDropFiles = {};
this._pendingSelfCopyFiles = {};
fileList = fileList ? fileList : this.currentSelection;
if (!fileList)
return;
fileList.forEach(f => {
const savedCoordinates = [
...f.savedCoordinates,
...f.normalCoordinates,
f.monitorIndex,
];
this._pendingSelfCopyFiles[f.fileName] = savedCoordinates;
});
}
_makeFileSystemLinks(fileList, destination) {
let gioDestination = Gio.File.new_for_uri(destination);
fileList.forEach(file => {
const fileGio = Gio.File.new_for_uri(file);
const baseNameParts =
this._DesktopIconsUtil.getFileExtensionOffset(
fileGio.get_basename()
);
let i = 0;
let newSymlinkName = fileGio.get_basename();
let checkSymlinkGio;
do {
checkSymlinkGio =
Gio.File.new_build_filenamev(
[gioDestination.get_path(), newSymlinkName]
);
try {
checkSymlinkGio.make_symbolic_link(
GLib.build_filenamev([fileGio.get_path()]),
null
);
break;
} catch (e) {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS)) {
i += 1;
newSymlinkName =
`${baseNameParts.basename}` +
`${i}${baseNameParts.extension}`;
} else {
console.error(e, 'Error making file-system links');
const header = _('Making SymLink Failed');
const text = _('Could not create symbolic link');
this._dbusManager.doNotify(header, text);
break;
}
}
} while (true);
});
}
async _detectURLorText(dropData, dropCoordinates) {
/**
* Checks to see if a string is a URL
*
* @param {string} str A text URL
* @returns {boolean} if the string is a URL
*/
function isValidURL(str) {
var pattern = new RegExp('^(https|http|ftp|rtsp|mms)?:\\/\\/?' +
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' +
'((\\d{1,3}\\.){3}\\d{1,3}))' +
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' +
'(\\?[;&a-z\\d%_.~+=-]*)?' +
'(\\#[-a-z\\d_]*)?$', 'i');
return !!pattern.test(str);
}
const text = dropData.toString();
if (text === '')
return;
if (isValidURL(text)) {
await this._writeURLlinktoDesktop(text, dropCoordinates);
} else {
let filename = 'Dragged Text';
const now = Date().valueOf().split(' ').join('').replace(/:/g, '-');
filename = `${filename}-${now}`;
await this._DesktopIconsUtil.writeTextFileToPath(
text,
this._desktopDir,
filename,
dropCoordinates
);
}
}
async _writeURLlinktoDesktop(link, dropCoordinates) {
let filename = link.split('?')[0];
filename = filename.split('//')[1];
filename = filename.split('/')[0];
const now = Date().valueOf().split(' ').join('').replace(/:/g, '-');
filename = `${filename}-${now}`;
await this._writeHTMLTypeLink(filename, link, dropCoordinates);
}
async _writeHTMLTypeLink(filename, link, dropCoordinates) {
filename += '.html';
let body = [
'<html>',
' <head>',
` <meta http-equiv="refresh" content="0; url=${link}" />`,
' </head>',
' <body>',
' </body>',
'</html>',
];
body = body.join('\n');
await this._DesktopIconsUtil.writeTextFileToPath(
body,
this._desktopDir,
filename,
dropCoordinates
);
}
_startGnomeShellDrag() {
if (!this.localDrag &&
this.dragItem &&
!this.gnomeShellDrag
) {
this.gnomeShellDrag =
new this._GnomeShellDragDrop
.GnomeShellDrag(this._desktopManager);
}
}
_stopGnomeShellDrag() {
this.gnomeShellDrag?.destroy();
this.gnomeShellDrag = null;
}
_localDrag() {
let localDrag = false;
this._desktops.forEach(d => {
if (d.localDrag)
localDrag = true;
});
return localDrag;
}
_positiveOffsetGridAim(xGlobalDestination, yGlobalDestination) {
// Find the grid where the destination lies and aim towards the positive
// side, middle of grid to ensure drop in the grid
let xbias = 0;
let ybias = 0;
for (let desktop of this._desktops) {
if (desktop.coordinatesBelongToThisGrid(
xGlobalDestination,
yGlobalDestination)) {
xbias = desktop._elementWidth / 2;
ybias = desktop._elementHeight / 2;
break;
}
}
return [xGlobalDestination + xbias, yGlobalDestination + ybias];
}
async _getFsId(file) {
/**
* Returns filesystem id of file or null if file does not exist
*
* @param {file} Gio.File
* @returns {str} filesystem ID of file or null
*/
const info =
await file.query_info_async(
'id::filesystem',
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_DEFAULT,
null
).catch(
e => {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND))
return null;
throw e;
}
);
if (info == null)
return null;
return info.get_attribute_string('id::filesystem');
}
async _desktopFsId() {
const desktopFsId = await this._getFsId(this._desktopDir);
return desktopFsId;
}
async _fileIsOnDesktopFileSystem(file) {
/**
* Checks to see if file is on the same filesystem as the Desktop Folder
* Consider trash:// URI to be in the same folder as the Desktop
* This forces a move from Trash instead of copy
*
* @param {file} Gio.File
* @returns {boolean} if the file is on the same filesystem as Desktop
* @returns {null} if the file does not exist
*/
const fileSystemID = await this._getFsId(file);
if (fileSystemID == null)
return null;
if (fileSystemID.startsWith('trash'))
return true;
const desktopFileSystemID = await this._desktopFsId();
if (fileSystemID === desktopFileSystemID)
return true;
return false;
}
_drawRubberBand() {
for (let grid of this._desktops)
grid.updateOverlay();
}
// Global Methods
// *******************************************************
// Drag Preperation
fillDragDataGet(target) {
const fileList = this.currentSelection;
if (!fileList)
return null;
let uriList = '';
let pathList = '';
switch (target) {
case this._Enums.DndTargetInfo.GNOME_ICON_LIST:
for (let fileItem of fileList) {
uriList += fileItem.uri;
const coordinates = fileItem.getCoordinates();
if (coordinates !== null) {
uriList += `\r
${coordinates[0]}:
${coordinates[1]}:
${coordinates[2] - coordinates[0] + 1}:
${coordinates[3] - coordinates[1] + 1}`;
}
uriList += '\r\n';
}
return uriList;
case this._Enums.DndTargetInfo.DING_ICON_LIST:
case this._Enums.DndTargetInfo.TEXT_URI_LIST:
uriList = fileList.map(f => f.uri).join('\r\n');
uriList += '\r\n';
return uriList;
case this._Enums.DndTargetInfo.TEXT_PLAIN:
pathList = fileList.map(f => f.path).join('n');
pathList += '\n';
return pathList;
}
return null;
}
makeFileListFromSelection(dropData, acceptFormat) {
if (!dropData)
return null;
if (acceptFormat === this._Enums.DndTargetInfo.TEXT_PLAIN)
return null;
let fileList;
if (acceptFormat === this._Enums.DndTargetInfo.GNOME_ICON_LIST) {
fileList = GLib.Uri.list_extract_uris(dropData);
} else if (acceptFormat === this._Enums.DndTargetInfo.DING_ICON_LIST) {
fileList = dropData.get_files().map(f => f.get_uri());
} else {
fileList = dropData.split('\n').map(f => {
if (GLib.Uri.peek_scheme(f))
return f;
else
return GLib.filename_to_uri(f, null);
});
}
// filename_to_uri can return null
fileList = fileList.filter(f => {
if (!f)
return false;
return true;
});
if (fileList && fileList.length)
return fileList;
else
return null;
}
saveCurrentFileCoordinatesForUndo(fileList) {
this._saveCurrentFileCoordinatesForUndo(fileList);
}
async clearFileCoordinates(fileList,
dropCoordinates,
opts = {doCopy: false}) {
if (this._Prefs.keepArranged || this._Prefs.keepStacked)
return;
this.pendingDropFiles = {};
this.pendingSelfCopyFiles = {};
await Promise.all(fileList.map(async element => {
const file = Gio.File.new_for_uri(element);
if (!file.is_native()) {
this.setPendingDropCoordinates(file, dropCoordinates);
return;
}
const info = new Gio.FileInfo();
info.set_attribute_string('metadata::desktop-icon-position', '');
if (dropCoordinates !== null) {
if (!opts.doCopy) {
info.set_attribute_string(
'metadata::nautilus-drop-position',
`${dropCoordinates[0]},${dropCoordinates[1]}`
);
} else {
this.setPendingDropCoordinates(file, dropCoordinates);
return;
}
}
try {
await file.set_attributes_async(info,
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_LOW,
null);
} catch (e) {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND))
this.setPendingDropCoordinates(file, dropCoordinates);
}
}));
}
setPendingDropCoordinates(file, dropCoordinates) {
if (!dropCoordinates)
return;
const basename = file.get_basename();
this._pendingDropFiles = {};
this._pendingSelfCopyFiles = {};
let selfCopy = false;
this.currentWorkingList.forEach(fileItem => {
if (fileItem.fileName === basename) {
this._pendingDropFiles[`${basename}COPYEXPECTED`] =
dropCoordinates;
this._pendingSelfCopyFiles[basename] =
fileItem.savedCoordinates;
selfCopy = true;
}
});
if (!selfCopy)
this._pendingDropFiles[basename] = dropCoordinates;
}
// Drag Methods
startRubberband(X, Y) {
this.rubberBandInitX = X;
this.rubberBandInitY = Y;
this.rubberBand = true;
for (let item of this._displayList)
item.touchedByRubberband = false;
}
onDragBegin(item) {
this._saveCurrentFileCoordinatesForUndo();
this.dragItem = item;
this._stopGnomeShellDrag();
}
onDragMotion(X, Y) {
if (this.dragItem === null) {
for (let desktop of this._desktops)
desktop.refreshDrag([[0, 0]], X, Y);
return;
}
if (this._dragList === null) {
const itemList = this._desktopManager.getCurrentSelection();
if (!itemList)
return;
let [x1, y1] = this.dragItem.getCoordinates().slice(0, 3);
let oX = x1;
let oY = y1;
this._dragList = [];
for (let item of itemList) {
[x1, y1] = item.getCoordinates().slice(0, 3);
this._dragList.push([x1 - oX, y1 - oY]);
}
}
for (let desktop of this._desktops)
desktop.refreshDrag(this._dragList, X, Y);
this._stopGnomeShellDrag();
this.dragItem.setHighLighted();
}
onDragLeave() {
this._dragList = null;
for (let desktop of this._desktops)
desktop.refreshDrag(null, 0, 0);
// Synthesise, extrapolate drag motion on a shell actor
this._startGnomeShellDrag();
}
onDragEnd() {
this.dragItem = null;
this._stopGnomeShellDrag();
}
async onDragDataReceived(
xGlobalDestination,
yGlobalDestination,
xlocalDestination,
ylocalDestination,
dropData,
acceptFormat,
gdkDropAction,
localDrop,
event,
dragItem
) {
this.onDragLeave();
let dropCoordinates;
let xOrigin;
let yOrigin;
const forceCopy = gdkDropAction === Gdk.DragAction.COPY;
const fileList = this.makeFileListFromSelection(dropData, acceptFormat);
if (!this._Prefs.freePositionIcons) {
[xGlobalDestination, yGlobalDestination] =
this._positiveOffsetGridAim(
xGlobalDestination,
yGlobalDestination
);
}
let returnAction;
switch (acceptFormat) {
case this._Enums.DndTargetInfo.DING_ICON_LIST:
[xOrigin, yOrigin] = dragItem.getCoordinates().slice(0, 3);
if (gdkDropAction === Gdk.DragAction.MOVE) {
this.doMoveWithDragAndDrop(
xOrigin,
yOrigin,
xGlobalDestination,
yGlobalDestination
);
returnAction = Gdk.DragAction.MOVE;
break;
}
// eslint-disable-next-line no-fallthrough
case this._Enums.DndTargetInfo.GNOME_ICON_LIST:
case this._Enums.DndTargetInfo.URI_LIST:
if (!fileList)
return;
if (gdkDropAction === Gdk.DragAction.MOVE ||
gdkDropAction === Gdk.DragAction.COPY) {
try {
if (!localDrop) {
await this.clearFileCoordinates(
fileList,
[xGlobalDestination, yGlobalDestination],
{doCopy: forceCopy}
);
}
returnAction = await this.copyOrMoveUris(
fileList,
this._desktopDir.get_uri(),
event,
{forceCopy}
);
} catch (e) {
console.error(e);
}
} else {
if (gdkDropAction >= Gdk.DragAction.LINK)
returnAction = Gdk.DragAction.LINK;
else
returnAction = Gdk.DragAction.COPY;
this.askWhatToDoWithFiles(
fileList,
this._desktopDir.get_uri(),
xGlobalDestination,
yGlobalDestination,
xlocalDestination,
ylocalDestination,
event
)
.catch(e => logError(e));
}
break;
case this._Enums.DndTargetInfo.TEXT_PLAIN:
returnAction = Gdk.DragAction.COPY;
dropCoordinates = [xGlobalDestination, yGlobalDestination];
this._detectURLorText(dropData, dropCoordinates);
break;
default:
returnAction = Gdk.DragAction.COPY;
}
// eslint-disable-next-line consistent-return
return returnAction;
}
doMoveWithDragAndDrop(xOrigin, yOrigin, xDestination, yDestination) {
const keepArranged =
this._Prefs.keepArranged || this._Prefs.keepStacked;
if (this._Prefs.sortSpecialFolders && keepArranged)
return;
let deltaX;
let deltaY;
if (!this._Prefs.freePositionIcons) {
deltaX = xDestination - xOrigin;
deltaY = yDestination - yOrigin;
} else {
deltaX = xDestination - xOrigin - this.localDragOffset[0] * 2;
deltaY = yDestination - yOrigin - this.localDragOffset[1];
}
const fileItems = [];
this._displayList.filter(item => item.isSelected).forEach(item => {
if (!keepArranged || item.isSpecial) {
fileItems.push(item);
item.removeFromGrid({callOnDestroy: false});
let [x, y] = item.getCoordinates().slice(0, 3);
item.temporarySavedPosition = [x + deltaX, y + deltaY];
}
});
// force to store the new coordinates
this._desktopManager._addFilesToDesktop(fileItems,
this._Enums.StoredCoordinates.OVERWRITE);
if (keepArranged) {
this._desktopManager.redrawDesktop().catch(e => {
console.log(
'Exception while doing move with drag and drop and' +
`"Keep arranged…": ${e.message}\n${e.stack}`);
});
}
}
onTextDrop(dropData, [xGlobalDestination, yGlobalDestination]) {
this._detectURLorText(
dropData,
[xGlobalDestination, yGlobalDestination]
);
}
// Drag Motion
onMotion(X, Y) {
this.pointerX = X;
this.pointerY = Y;
if (this.rubberBand) {
this.x1 = Math.min(X, this.rubberBandInitX);
this.x2 = Math.max(X, this.rubberBandInitX);
this.y1 = Math.min(Y, this.rubberBandInitY);
this.y2 = Math.max(Y, this.rubberBandInitY);
this.selectionRectangle =
new Gdk.Rectangle({
'x': this.x1,
'y': this.y1,
'width': this.x2 - this.x1,
'height': this.y2 - this.y1,
});
this._drawRubberBand();
for (let item of this._displayList) {
const labelintersect =
item.labelRectangle.intersect(this.selectionRectangle)[0];
const iconintersect =
item.iconRectangle.intersect(this.selectionRectangle)[0];
if (labelintersect || iconintersect) {
item.setSelected();
item.touchedByRubberband = true;
} else if (item.touchedByRubberband) {
item.unsetSelected();
}
}
}
}
onReleaseButton() {
if (this.rubberBand) {
this.rubberBand = false;
this.selectionRectangle = null;
}
for (let grid of this._desktops)
grid.updateOverlay();
return false;
}
// Selection HighLighting
unHighLightDropTarget() {
this._displayList.forEach(item => item.unHighLightDropTarget());
}
selected(fileItem, action) {
this._clearKeyboardSelection();
switch (action) {
case this._Enums.Selection.ALONE:
if (!fileItem.isSelected) {
for (let item of this._displayList) {
if (item === fileItem)
item.setSelected();
else
item.unsetSelected();
}
}
break;
case this._Enums.Selection.WITH_SHIFT:
fileItem.toggleSelected();
break;
case this._Enums.Selection.RIGHT_BUTTON:
if (!fileItem.isSelected) {
for (let item of this._displayList) {
if (item === fileItem)
item.setSelected();
else
item.unsetSelected();
}
}
break;
case this._Enums.Selection.ENTER:
if (this.rubberBand)
fileItem.setSelected();
break;
case this._Enums.Selection.RELEASE:
for (let item of this._displayList) {
if (item === fileItem) {
if (item.isSelected)
item.setSelected();
else
item.unsetSelected();
}
}
break;
}
}
_clearKeyboardSelection() {
this._displayList.forEach(item => item.keyboardUnSelected());
this._desktopManager.desktopActions.lastAnchorSelected = null;
}
// File Copy Move Link Methods
async copyOrMoveUris(uriList, destinationUri, event, params = {}) {
if (params.forceCopy) {
this._DBusUtils.RemoteFileOperations.pushEvent(event);
this._DBusUtils.RemoteFileOperations.CopyURIsRemote(
uriList,
destinationUri
);
return Gdk.DragAction.COPY;
}
const moveFiles = [];
const copyFiles = [];
await Promise.all(uriList.map(async uri => {
const f = Gio.File.new_for_uri(uri);
const localFile = await this._fileIsOnDesktopFileSystem(f);
// localFile is null if it does not exist, false if on different
// fileystem, true if on the same filesystem as the Desktop Folder
if (localFile == null) {
console.error(`Cannot Copy/Move, ${uri} does not exist`);
const header = _('Copy/Move Failed');
const text = _('{0} Does not exist').replace('{0}', uri);
this._dbusManager.doNotify(header, text);
return;
}
if (localFile)
moveFiles.push(uri);
else
copyFiles.push(uri);
}));
if (moveFiles.length) {
this._DBusUtils.RemoteFileOperations.pushEvent(event);
this._DBusUtils.RemoteFileOperations.MoveURIsRemote(
moveFiles,
destinationUri
);
}
if (copyFiles.length) {
this._DBusUtils.RemoteFileOperations.pushEvent(event);
this._DBusUtils.RemoteFileOperations.CopyURIsRemote(
copyFiles,
destinationUri
);
}
return moveFiles.length ? Gdk.DragAction.MOVE : Gdk.DragAction.COPY;
}
async askWhatToDoWithFiles(
fileList,
destinationuri,
X,
Y,
x,
y,
event,
opts = {desktopactions: true}
) {
const window = this._mainApp.get_active_window();
this._mainApp.activate_action('textEntryAccelsTurnOff', null);
const chooser = new Gtk.AlertDialog();
chooser.set_message(_('Choose Action for Files'));
chooser.buttons = [_('Move'), _('Copy'), _('Link'), _('Cancel')];
chooser.set_modal(false);
chooser.set_cancel_button(3);
chooser.set_default_button(3);
const cancellable = Gio.Cancellable.new();
if (this.dialogCancellable)
this.dialogCancellable.cancel();
this.dialogCancellable = cancellable;
const showdialog = new Promise(resolve => {
chooser.choose(window, cancellable, async (actor, choice) => {
let retval = Gtk.ResponseType.CANCEL;
try {
const buttonpress = actor.choose_finish(choice);
switch (buttonpress) {
case 0:
retval = Gdk.DragAction.MOVE;
try {
if (opts.desktopactions) {
await this.clearFileCoordinates(
fileList,
[X, Y]
);
}
let forceCopy = false;
await this.copyOrMoveUris(
fileList,
destinationuri,
event,
{forceCopy}
);
} catch {
console.error('Error moving files');
}
break;
case 1:
retval = Gdk.DragAction.COPY;
try {
if (opts.desktopactions) {
await this.clearFileCoordinates(
fileList,
[X, Y],
{dopCopy: true}
);
}
let forceCopy = true;
await this.copyOrMoveUris(fileList,
destinationuri, event, {forceCopy});
} catch {
console.error('Error copying files');
}
break;
case 2:
retval = Gdk.DragAction.LINK;
try {
if (opts.desktopactions) {
await this.makeLinks(
fileList,
destinationuri,
X,
Y
);
} else {
this._makeFileSystemLinks(
fileList,
destinationuri
);
}
} catch {
console.error('Error making links');
}
break;
default:
retval = Gtk.ResponseType.CANCEL;
}
resolve(retval);
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
console.error(
e,
'Error asking choosing what to do with Files ' +
`${e.message}`
);
}
resolve(retval);
}
});
});
const retval = await showdialog.catch(e => logError(e));
this.dialogCancellable = null;
this._mainApp.activate_action('textEntryAccelsTurnOn', null);
return retval;
}
async makeLinks(fileList, destination, X, Y) {
const gioDestination = Gio.File.new_for_uri(destination);
await Promise.all(fileList.map(async file => {
const fileGio = Gio.File.new_for_uri(file);
const newSymlinkName =
this._desktopManager.desktopMonitor
.getDesktopUniqueFileName(
fileGio.get_basename()
);
const symlinkGio =
Gio.File.new_build_filenamev(
[gioDestination.get_path(), newSymlinkName]
);
try {
const linkMade =
symlinkGio.make_symbolic_link(
GLib.build_filenamev([fileGio.get_path()]),
null
);
if (linkMade) {
const info = new Gio.FileInfo();
info.set_attribute_string(
'metadata::nautilus-drop-position',
`${X},${Y}`
);
info.set_attribute_string(
'metadata::desktop-icon-position',
''
);
try {
await symlinkGio.set_attributes_async(
info,
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_LOW,
null
);
} catch (e) {
console.error(e, 'Error setting link FileInfo');
}
}
} catch {
console.error('Error making desktop links');
const header = _('Making SymLink Failed');
const text = _('Could not create symbolic link');
this._dbusManager.doNotify(header, text);
}
}));
}
// Getters and Setters
get pendingDropFiles() {
return this._pendingDropFiles;
}
set pendingDropFiles(object) {
this._pendingDropFiles = object;
}
get pendingSelfCopyFiles() {
return this._pendingSelfCopyFiles;
}
set pendingSelfCopyFiles(object) {
this._pendingSelfCopyFiles = object;
}
get currentSelection() {
return this._desktopManager.getCurrentSelection();
}
get currentWorkingList() {
return this._desktopManager.currentWorkingList;
}
get _displayList() {
return this._desktopManager._displayList;
}
get _desktops() {
return this._desktopManager._desktops;
}
get _desktopDir() {
return this._desktopManager._desktopDir;
}
get localDrag() {
return this._localDrag();
}
};

198
ding/app/enums.js Normal file
View File

@@ -0,0 +1,198 @@
/* eslint-disable no-unused-vars */
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Copyright (C) 2024, 2025 Sundeep Mediratta (smedius@gmail.com)
* Copyright (C) 2019 Sergio Costas (rastersoft@gmail.com)
* Based on code original (C) Carlos Soriano
*
* 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, version 3 of the License.
*
* 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/>.
*/
export const ICON_SIZE = {'tiny': 36, 'small': 48, 'standard': 64, 'large': 96};
export const ICON_WIDTH = {'tiny': 70, 'small': 90, 'standard': 120, 'large': 130};
export const ICON_HEIGHT = {'tiny': 80, 'small': 90, 'standard': 106, 'large': 138};
export const START_CORNER = {
'top-left': [false, false],
'top-right': [true, false],
'bottom-left': [false, true],
'bottom-right': [true, true],
};
export const FileType = {
NONE: null,
USER_DIRECTORY_HOME: 'show-home',
USER_DIRECTORY_TRASH: 'show-trash',
EXTERNAL_DRIVE: 'external-drive',
STACK_TOP: 'stack-top',
};
export const StoredCoordinates = {
PRESERVE: 0,
OVERWRITE: 1,
ASSIGN: 2,
};
export const Selection = {
ALONE: 0,
WITH_SHIFT: 1,
RIGHT_BUTTON: 2,
ENTER: 3,
LEAVE: 4,
RELEASE: 5,
};
/* From NautilusFileUndoManagerState */
export const UndoStatus = {
NONE: 0,
UNDO: 1,
REDO: 2,
};
export const FileExistOperation = {
ASK: 0,
OVERWRITE: 1,
RENAME: 2,
SKIP: 3,
};
export const WhatToDoWithExecutable = {
EXECUTE: 0,
EXECUTE_IN_TERMINAL: 1,
DISPLAY: 2,
CANCEL: 3,
};
export const SortOrder = {
ORDER: 'arrangeorder',
NAME: 1,
DESCENDINGNAME: 2,
MODIFIEDTIME: 3,
KIND: 4,
SIZE: 5,
};
export const CompressionType = {
ZIP: 0,
TAR_XZ: 1,
SEVEN_ZIP: 2,
ENCRYPTED_ZIP: 3,
};
export const DndTargetInfo = {
DING_ICON_LIST: 'x-special/ding-icon-list',
GNOME_ICON_LIST: 'x-special/gnome-icon-list',
URI_LIST: 'text/uri-list',
TEXT_PLAIN: 'text/plain',
TEXT_PLAIN_UTF8: 'text/plain;charset=utf-8',
GDKFILELIST: 'GdkFileList',
GCHARARRAY: 'gchararray',
GFILE: 'GFile',
MIME_TYPES: ['x-special/ding-icon-list', 'x-special/gnome-icon-list', 'text/uri-list', 'text/plain', 'text/plain;charset=utf-8'],
};
// Since Gnome Shell 48 the enumeration of the cursor is different,
// the name has changed, althugh the value is the same;
// We use our own enumeration names to avoid problems with the version
// of the Gnome Shell, the enumeration integer points to the correct
// value in the Gnome Shell 48 and Meta 48 Enum and earlier.
// We use strings to avoid problems with the version of the Gnome Shell
// with corresponging Enum in DingManager.js. The strings are transmitted
// over DBus to the exetension.
export const ShellDropCursor = {
DEFAULT: 'default', // 2 META_CURSOR_DEFAULT Meta.Cursor.DEFAULT
NODROP: 'dndNoDropCursor', // 15 META_CURSOR_NO_DROP Meta.Cursor.DND_UNSUPPORTED_TARGET
COPY: 'dndCopyCursor', // 13 META_CURSOR_COPY Meta.Cursor.DND_COPY
MOVE: 'dndMoveCursor', // 14 META_CURSOR_MOVE Meta.Cursor.DND_MOVE
};
export const DEFAULT_ATTRIBUTES = 'metadata::*,standard::*,access::*,time::modified,unix::mode';
export const TERMINAL_SCHEMA = 'org.gnome.desktop.default-applications.terminal';
export const SCHEMA_NAUTILUS = 'org.gnome.nautilus.preferences';
export const SCHEMA_NAUTILUS_COMPRESSION = 'org.gnome.nautilus.compression';
export const SCHEMA_GTK = 'org.gtk.gtk4.Settings.FileChooser';
export const SCHEMA = 'org.gnome.shell.extensions.vesperos-taskbar.desktop-icons';
export const SCHEMA_MUTTER = 'org.gnome.mutter';
export const SCHEMA_GNOME_SETTINGS = 'org.gnome.desktop.interface';
export const DCONF_TERMINAL_EXEC_KEY = 'exec';
export const DCONF_TERMINAL_EXEC_STRING = 'exec-arg';
export const DESKTOPFILE_TERMINAL_EXEC_KEY = 'Exec';
export const DESKTOPFILE_TERMINAL_EXEC_SWITCH = 'X-ExecArg';
export const NAUTILUS_SCRIPTS_DIR = '.local/share/nautilus/scripts';
export const THUMBNAILS_DIR = '.cache/thumbnails';
export const DND_HOVER_TIMEOUT = 1500; // In milliseconds
export const DND_SHELL_HOVER_POLL = 200; // In milliseconds
export const TOOLTIP_HOVER_TIMEOUT = 1000; // In milliseconds
export const XDG_EMAIL_CMD = 'xdg-email';
export const XDG_EMAIL_CMD_OPTIONS = '--attach';
export const ZIP_CMD = 'zip';
export const ZIP_CMD_OPTIONS = '-r';
export const XDG_TERMINAL_LIST_FILE = 'xdg-terminals.list';
export const XDG_TERMINAL_DIR = 'xdg-terminal-exec';
export const SYSTEM_DATA_DIRS = ['/usr/local/share', '/usr/share'];
export const XDG_TERMINAL_EXEC = 'xdg-terminal-exec';
export const GRID_ELEMENT_SPACING = 2;
export const GRID_PADDING = 0;
export const WIDGET_GRID_SIZE = 10;
export const XDG_USER_DIRS = 'user-dirs.dirs';
export const XDG_SYSTEM_DIRS = 'user-dirs.defaults';
export const DEFAULT_DESKTOP_NAME = 'Desktop';
export const UnixPermissions = {
S_ISUID: 0o04000, // set-user-ID bit
S_ISGID: 0o02000, // set-group-ID bit (see below)
S_ISVTX: 0o01000, // sticky bit (see below)
S_IRWXU: 0o00700, // mask for file owner permissions
S_IRUSR: 0o00400, // owner has read permission
S_IWUSR: 0o00200, // owner has write permission
S_IXUSR: 0o00100, // owner has execute permission
S_IRWXG: 0o00070, // mask for group permissions
S_IRGRP: 0o00040, // group has read permission
S_IWGRP: 0o00020, // group has write permission
S_IXGRP: 0o00010, // group has execute permission
S_IRWXO: 0o00007, // mask for permissions for others (not in group)
S_IROTH: 0o00004, // others have read permission
S_IWOTH: 0o00002, // others have write permission
S_IXOTH: 0o00001, // others have execute permission
// From https://www.commandlinux.com/man-page/man2/lstat.2.html
};
export const IgnoreKeys = [
'KEY_space', 'KEY_Shift_L', 'KEY_Shift_R', 'KEY_Control_L',
'KEY_Control_R', 'KEY_Caps_Lock', 'KEY_Shift_Lock', 'KEY_Meta_L',
'KEY_Meta_R', 'KEY_Alt_L', 'KEY_Alt_R', 'KEY_Super_L',
'KEY_Super_R', 'KEY_ISO_Level3_Shift', 'KEY_ISO_Level5_Shift',
];
export const TRANSITIONDURATION = 500; // in ms
export const WidgetManagerDebugFlags = Object.freeze({
NONE: 0,
HOST_STATE: 1 << 0,
WIDGET_MESSAGES: 1 << 1,
});
export const WIDGET_MANAGER_DEBUG = WidgetManagerDebugFlags.NONE;
// For debugging use
// const WIDGET_MANAGER_DEBUG =
// WidgetManagerDebugFlags.HOST_STATE |
// WidgetManagerDebugFlags.WIDGET_MESSAGES;
export const CspProfile = Object.freeze({
STRICT: 0,
DEV: 1,
RELAXED: 2,
});
export const DEFAULT_CSP_PROFILE = CspProfile.STRICT;

850
ding/app/fileItemIcon.js Normal file
View File

@@ -0,0 +1,850 @@
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Adw-DING Copyright (C) 2022, 2025 Sundeep Mediratta (smedius@gmail.com)
* Based on code original (C) Carlos Soriano and (c) Sergio Costas
* SwitcherooControl code based on code original from Marsch84
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {Gtk, Gdk, Gio, GLib} from '../dependencies/gi.js';
import {DesktopIconItem} from '../dependencies/localFiles.js';
import {_} from '../dependencies/gettext.js';
export {FileItemIcon};
const Signals = imports.signals;
const FileItemIcon = class extends DesktopIconItem {
constructor(desktopManager, file, fileInfo, fileTypeEnum, gioMount) {
super(desktopManager, fileTypeEnum);
this.DBusUtils = desktopManager.DBusUtils;
this._fileInfo = fileInfo;
this._gioMount = gioMount;
this._file = file;
this.isStackTop = false;
this.stackUnique = false;
this.readSavedCoordinates();
this.readDropCoordinates();
this._createIconActor();
/* Set the metadata */
this._updateMetadataFromFileInfo(fileInfo);
if (this._attributeCanExecute)
this._execLine = this.file.get_path();
else
this._execLine = null;
this._updateName();
if (this._dropCoordinates)
this.setSelected();
}
/** *********************
* Destroyers *
***********************/
_destroy() {
super._destroy();
if (this._updatingIconCancellable)
this._updatingIconCancellable.cancel();
if (this._queryFileInfoCancellable)
this._queryFileInfoCancellable.cancel();
if (this._savedCoordinatesCancellable)
this._savedCoordinatesCancellable.cancel();
if (this._dropCoordinatesCancellable)
this._dropCoordinatesCancellable.cancel();
/* Metadata */
if (this._setMetadataTrustedCancellable)
this._setMetadataTrustedCancellable.cancel();
}
/** *********************
* Creators *
***********************/
_getVisibleName() {
return this._fileInfo.get_display_name();
}
_setFileName(text) {
this._setLabelName(text);
}
_setAccesibilityName() {
const visibleName = this._getVisibleName();
const folderName = _('Folder');
const fileName = _('File');
if (this._isDirectory) {
/** TRANSLATORS: when using a screen reader, this is the text
* read when a folder is selected. Example: if a folder named
* "things" is selected, it will say "things Folder" */
this.container.update_property(
[Gtk.AccessibleProperty.LABEL],
[`${visibleName} ${folderName}`]
);
} else {
/** TRANSLATORS: when using a screen reader, this is the text
* read when a normal file is selected. Example: if a file
* named "my_picture.jpg" is selected, it will say
* "my_picture.jpg File" */
this.container.update_property(
[Gtk.AccessibleProperty.LABEL],
[`${visibleName} ${fileName}`]
);
}
}
readSavedCoordinates() {
const array = this._readCoordinatesFromAttribute(this._fileInfo,
'metadata::desktop-icon-position'
);
this._parseSavedCoordinates(array);
}
readDropCoordinates() {
const array = this._readCoordinatesFromAttribute(this._fileInfo,
'metadata::nautilus-drop-position'
);
this._parseDropCoordinates(array);
}
_readCoordinatesFromAttribute(fileInfo, attribute) {
const readCoordinates = fileInfo.get_attribute_as_string(attribute);
if (readCoordinates !== null && readCoordinates !== '')
return readCoordinates.split(',');
return null;
}
async _refreshMetadataAsync(cancellable) {
if ((cancellable && cancellable.is_cancelled()) || this._destroyed) {
throw new GLib.Error(Gio.IOErrorEnum,
Gio.IOErrorEnum.CANCELLED,
'Operation was cancelled');
} else if (!cancellable) {
cancellable = new Gio.Cancellable();
}
if (this._queryFileInfoCancellable)
this._queryFileInfoCancellable.cancel();
this._queryFileInfoCancellable = cancellable;
try {
const newFileInfo =
await this._file.query_info_async(
this.Enums.DEFAULT_ATTRIBUTES,
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_DEFAULT,
cancellable
);
this._updateMetadataFromFileInfo(newFileInfo);
this._updateName();
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
console.error(e, `Error getting file info: ${e.message}`);
} finally {
if (this._queryFileInfoCancellable === cancellable)
this._queryFileInfoCancellable = null;
}
}
_updateMetadataFromFileInfo(fileInfo) {
this._fileInfo = fileInfo;
this._displayName = this._getVisibleName();
this._attributeCanExecute = fileInfo.get_attribute_boolean(
Gio.FILE_ATTRIBUTE_ACCESS_CAN_EXECUTE
);
this._unixmode = fileInfo.get_attribute_uint32(
Gio.FILE_ATTRIBUTE_UNIX_MODE
);
this._writableByOthers =
(this._unixmode & this.Enums.UnixPermissions.S_IWOTH) !== 0;
this._attributeContentType = fileInfo.get_content_type();
this._fileType = fileInfo.get_file_type();
this._isDirectory = this._fileType === Gio.FileType.DIRECTORY;
this._isSpecial = this._fileTypeEnum !== this.Enums.FileType.NONE;
this._isHidden =
fileInfo.get_attribute_boolean(
Gio.FILE_ATTRIBUTE_STANDARD_IS_HIDDEN) ||
fileInfo.get_attribute_boolean(
Gio.FILE_ATTRIBUTE_STANDARD_IS_BACKUP);
this._modifiedTime = fileInfo.get_attribute_uint64(
Gio.FILE_ATTRIBUTE_TIME_MODIFIED
);
if (this.Prefs.showLinkEmblem)
this._setEncryptionStatus().catch(logError);
}
async _setEncryptionStatus() {
if (this.isEncrypted)
return;
switch (this._attributeContentType) {
case 'application/x-7z-compressed':
this._isEncrypted =
this.DesktopIconsUtil.checkIf7zEncrypted(this._file);
break;
case 'application/pdf':
// eslint-disable-next-line no-case-declarations
const isEncrypted =
await this.DesktopIconsUtil.checkIfPdfEncrypted(this._file);
// File may have no password or null password, so we may still be
// able to read/display it. It will therefore have a generated
// thumbnail. Check by generating the thumbnail if needed.
// Don't show the locked item in this case, it is encrypted in pdf
// per pdf specification but a user can still read it.
if (isEncrypted && !this.thumbnail) {
this.thumbnail =
await this.ThumbnailLoader.getThumbnail(
this,
null
);
}
this._isEncrypted = isEncrypted && !this.thumbnail;
break;
case 'application/zip':
this._isEncrypted =
await this.DesktopIconsUtil.checkIfZipEncrypted(this._file);
break;
case 'application/epub+zip':
this._isEncrypted =
await this.DesktopIconsUtil.checkIfZipEncrypted(this._file);
break;
default:
this._isEncrypted = false;
}
if (!this._isEncrypted)
return;
this.updateIcon()
.catch(e =>
console.error(`Error updating after setting encryption status ${e}`)
);
}
async _doOpenContext(context = null, fileList) {
if (!fileList)
fileList = [];
if (!this.DBusUtils.GnomeArchiveManager.isAvailable &&
this._fileType === Gio.FileType.REGULAR &&
this._desktopManager.autoAr.fileIsCompressed(this.fileName)
) {
this._desktopManager.autoAr.extractFile(this.fileName);
return;
}
try {
await Gio.AppInfo.launch_default_for_uri_async(
this.file.get_uri(),
context,
null
);
} catch (e) {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_SUPPORTED)) {
const title = _('Opening File Failed');
const defaultAppInfo =
Gio.content_type_get_description(this.attributeContentType);
const error =
_('There is no application installed to open "{fo}" files.')
.replace('{fo}', defaultAppInfo);
const helpURI =
'https://gitlab.com/smedius/desktop-icons-ng/-/issues/73';
this._showerrorpopup(title, error, helpURI);
} else {
console.error(
e, `Error opening file ${this.file.get_uri()}: ${e.message}`
);
}
}
}
_showerrorpopup(title, error, helpURI = null) {
const errorDialog = this._desktopManager.showError(
title,
error,
helpURI
);
errorDialog.show();
}
_updateName() {
this._setFileName(this._getVisibleName());
this._setAccesibilityName();
}
/** *********************
* Button Clicks *
***********************/
_doButtonOnePressed(
button, nPress, X, Y, x, y, shiftPressed, controlPressed
) {
super._doButtonOnePressed(
button, nPress, X, Y, x, y, shiftPressed, controlPressed
);
if (nPress === 2 && !this.Prefs.CLICK_POLICY_SINGLE)
this.doOpen();
}
_doButtonOneReleased(
_button, nPress, _X, _Y, _x, _y, shiftPressed, controlPressed
) {
if (nPress === 1 &&
this.Prefs.CLICK_POLICY_SINGLE &&
!shiftPressed &&
!controlPressed)
this.doOpen();
}
/** *********************
* Drag and Drop *
***********************/
async receiveDrop(
X, Y,
x, y,
dropData,
acceptFormat,
gdkDropAction,
localDrop,
event,
dragItem
) {
if (!this.dropCapable)
return false;
if (acceptFormat !== this.Enums.DndTargetInfo.DING_ICON_LIST &&
acceptFormat !== this.Enums.DndTargetInfo.GNOME_ICON_LIST &&
acceptFormat !== this.Enums.DndTargetInfo.URI_LIST)
return false;
const fileList =
this._dragManager.makeFileListFromSelection(dropData, acceptFormat);
if (!fileList)
return false;
if (dragItem && (dragItem.uri === this._file.get_uri() ||
!(this._isValidDesktopFile || this.isDirectory))) {
// Dragging a file/folder over itself or over another file will
// do nothing, allow drag to directory or valid desktop file
return false;
}
const dropReturnValue = await this._handleDroppedUris(
X, Y,
x, y,
fileList,
gdkDropAction,
localDrop,
event
);
return dropReturnValue;
}
async _handleDroppedUris(
X, Y,
x, y,
fileList,
gdkDropAction,
localDrop,
event
) {
const forceCopy = gdkDropAction === Gdk.DragAction.COPY;
let returnAction;
if (gdkDropAction === Gdk.DragAction.MOVE ||
gdkDropAction === Gdk.DragAction.COPY
) {
if (localDrop)
this._dragManager.saveCurrentFileCoordinatesForUndo();
try {
returnAction =
await this._dragManager.copyOrMoveUris(
fileList, this._file.get_uri(), event, {forceCopy}
);
} catch (e) {
console.error(e);
return false;
}
} else {
if (gdkDropAction >= Gdk.DragAction.LINK)
returnAction = Gdk.DragAction.LINK;
else
returnAction = Gdk.DragAction.COPY;
this._dragManager.askWhatToDoWithFiles(
fileList,
this._file.get_uri(),
X, Y,
x, y,
event,
{desktopActions: false}
);
}
return returnAction;
}
_hasToRouteDragToGrid() {
return this._isSelected &&
this._dragManager.dragItem &&
this._dragManager.dragItem.uri !== this._file.get_uri();
}
_dropCapable() {
if (this._isDirectory ||
this._hasToRouteDragToGrid()
)
return true;
else
return false;
}
/** *********************
* Icon Rendering *
***********************/
async _reloadIcon(cancellable) {
if (!cancellable)
cancellable = new Gio.Cancellable();
this._updatingIconCancellable = cancellable;
try {
await this._refreshMetadataAsync(cancellable);
await this._updateIcon(cancellable);
this._icon.queue_draw();
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
console.error(
e,
`Exception while updating ${
this._getVisibleName()
? this._getVisibleName()
: 'updating icon'
}: ${e.message}`);
throw e;
}
} finally {
if (this._updatingIconCancellable === cancellable)
this._updatingIconCancellable = null;
}
}
_addEmblemsToIconIfNeeded(iconPaintable, position = 0) {
let emblem = null;
let newIconPaintable = iconPaintable;
if (this.isEncrypted && this.Prefs.showLinkEmblem) {
emblem = Gio.ThemedIcon.new('icon-emblem-locked');
newIconPaintable =
this._addEmblem(newIconPaintable, emblem, position);
position += 1;
}
return newIconPaintable;
}
/** *********************
* Class Methods *
***********************/
onAttributeChanged() {
if (this._destroyed)
return;
this._reloadIcon()
.catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
console.error(
e,
'Exception while updating icon on Attribute Changed: ' +
`${e.message}`
);
}
}
);
}
updatedMetadata() {
this._reloadIcon().catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
console.error(
e,
'Exception while updating icon on Metadata Changed: ' +
`${e.message}`
);
}
});
}
doOpen(fileList) {
if (!fileList)
fileList = [];
this._doOpenContext(null, fileList).catch(e => console.error(e));
}
async onAllowDisallowLaunchingClicked() {
/*
* we're marking as trusted, make the file executable too. Note that we
* do not ever remove the executable bit, since we don't know who set
* it.
*/
if (this.metadataTrusted && !this._attributeCanExecute) {
let info = new Gio.FileInfo();
let newUnixMode = this._unixmode |
this.Enums.UnixPermissions.S_IXUSR |
this.Enums.UnixPermissions.S_IXGRP |
this.Enums.UnixPermissions.S_IXOTH;
info.set_attribute_uint32(
Gio.FILE_ATTRIBUTE_UNIX_MODE,
newUnixMode
);
await this._setFileAttributes(info).catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
throw e;
});
}
this._updateName();
}
doDiscreteGpu() {
if (!this.DBusUtils.discreteGpuAvailable) {
console.log(
'Could not apply discrete GPU environment, switcheroo-control' +
' not available'
);
return;
}
let gpus = this.DBusUtils.SwitcherooControl.proxy.GPUs;
if (!gpus) {
console.log(
'Could not apply discrete GPU environment. No GPUs in list.'
);
return;
}
for (let gpu in gpus) {
if (!gpus[gpu])
continue;
let defaultVariant = gpus[gpu]['Default'];
if (!defaultVariant || defaultVariant.get_boolean())
continue;
let env = gpus[gpu]['Environment'];
if (!env)
continue;
let envS = env.get_strv();
let context = new Gio.AppLaunchContext();
for (let i = 0; i < envS.length; i += 2)
context.setenv(envS[i], envS[i + 1]);
this._doOpenContext(context, null).catch(e => console.error(e));
return;
}
console.log('Could not find discrete GPU data in switcheroo-control');
}
async _setFileAttributes(fileInfo, cancellable = null, updateIcon = true) {
await this._file.set_attributes_async(
fileInfo,
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_LOW,
cancellable
);
if (cancellable && cancellable.is_cancelled()) {
throw new GLib.Error(Gio.IOErrorEnum,
Gio.IOErrorEnum.CANCELLED,
'Operation was cancelled');
}
if (updateIcon) {
await this._reloadIcon(cancellable).catch(e => {
console.error(
'Error while updating icon while setting attributes'
);
throw e;
});
}
}
async _storeCoordinates(name, coords, cancellable = null) {
const info = new Gio.FileInfo();
info.set_attribute_string(
`metadata::${name}`,
`${coords ? coords.join(',') : ''}`
);
const updateIcon = true;
await this._setFileAttributes(info, cancellable, !updateIcon);
}
writeSavedCoordinates(pos) {
const oldPos = this._savedCoordinates;
this._parseSavedCoordinates(pos);
if (this._savedCoordinatesCancellable)
this._savedCoordinatesCancellable.cancel();
const cancellable = new Gio.Cancellable();
this._savedCoordinatesCancellable = cancellable;
this._storeCoordinates(
'desktop-icon-position',
pos,
cancellable
).catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
console.error(
e,
'Failed to store the desktop coordinates for ' +
`${this.uri}: ${e.message}`
);
this._savedCoordinates = oldPos;
}
}).finally(() => {
if (this._savedCoordinatesCancellable === cancellable)
this._savedCoordinatesCancellable = null;
});
}
writeDroppedCoordinates(pos) {
const oldPos = this._dropCoordinates;
this._parseDropCoordinates(pos);
if (this._dropCoordinatesCancellable)
this._dropCoordinatesCancellable.cancel();
const cancellable = new Gio.Cancellable();
this._dropCoordinatesCancellable = cancellable;
this._storeCoordinates(
'nautilus-drop-position',
pos,
cancellable
).catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
console.error(e,
'Failed to store the desktop coordinates for ' +
`${this.uri}: ${e.message}`
);
this._dropCoordinates = oldPos;
}
}).finally(() => {
if (this._dropCoordinatesCancellable === cancellable)
this._dropCoordinatesCancellable = null;
});
}
/** *********************
* Getters and setters *
***********************/
get attributeContentType() {
return this._attributeContentType;
}
get attributeCanExecute() {
return this._attributeCanExecute;
}
get canRename() {
return !this.trustedDesktopFile &&
(this._fileTypeEnum === this.Enums.FileType.NONE);
}
get displayName() {
return this._displayName || null;
}
get dropCoordinates() {
return this._dropCoordinates;
}
set dropCoordinates(pos) {
if (this.DesktopIconsUtil.coordinatesEqual(this._dropCoordinates, pos))
return;
this.writeDroppedCoordinates(pos);
}
get execLine() {
return this._execLine;
}
get executableContentType() {
return Gio.content_type_can_be_executable(this.attributeContentType);
}
get file() {
return this._file;
}
get fileContainsText() {
return this._attributeContentType === 'text/plain';
}
get fileName() {
return this._fileInfo.get_name();
}
get fileSize() {
return this._fileInfo.get_size();
}
get isAllSelectable() {
return this._fileTypeEnum === this.Enums.FileType.NONE;
}
get isDirectory() {
return this._isDirectory;
}
get isExecutable() {
return this._attributeCanExecute;
}
get isHidden() {
return this._isHidden;
}
get metadataTrusted() {
return this._trusted;
}
set metadataTrusted(value) {
this._trusted = value;
if (this._setMetadataTrustedCancellable)
this._setMetadataTrustedCancellable.cancel();
const cancellable = new Gio.Cancellable();
this._setMetadataTrustedCancellable = cancellable;
let info = new Gio.FileInfo();
info.set_attribute_string('metadata::trusted',
value ? 'true' : 'false');
this._setFileAttributes(info, cancellable)
.catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
console
.error(e, `Failed to set metadata::trusted: ${e.message}`);
}
})
.finally(() => {
if (cancellable === this._setMetadataTrustedCancellable)
this._setMetadataTrustedCancellable = null;
});
}
get modifiedTime() {
return this._modifiedTime;
}
get path() {
return this._file.get_path();
}
get savedCoordinates() {
return this._savedCoordinates;
}
set savedCoordinates(pos) {
if (this.DesktopIconsUtil.coordinatesEqual(this._savedCoordinates, pos))
return;
this.writeSavedCoordinates(pos);
}
get x() {
return this._x1;
}
get y() {
return this._y1;
}
get X() {
return this._savedCoordinates[0];
}
get Y() {
return this._savedCoordinates[1];
}
get uri() {
return this._file.get_uri();
}
get writableByOthers() {
return this._writableByOthers;
}
get isStackMarker() {
if (this.isStackTop && !this.stackUnique)
return true;
else
return false;
}
};
Signals.addSignalMethods(FileItemIcon.prototype);

1490
ding/app/fileItemMenu.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,368 @@
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Gtk4 Port Copyright (C) 2023 Sundeep Mediratta (smedius@gmail.com)
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {Gdk, Gio, GLib, DesktopAppInfo} from '../dependencies/gi.js';
import {_} from '../dependencies/gettext.js';
export {GnomeShellDrag};
const GnomeShellDrag = class {
constructor(desktopManager) {
this._DBusUtils = desktopManager.DBusUtils;
if (!this._DBusUtils.RemoteExtensionControl.isAvailable)
return;
this._desktopManager = desktopManager;
this._dragManager = desktopManager.dragManager;
this._dragItem = this._dragManager.dragItem;
this._DesktopIconsUtil = desktopManager.DesktopIconsUtil;
this._Enums = desktopManager.Enums;
this._Prefs = desktopManager.Prefs;
this._selectedFiles = desktopManager.getCurrentSelection();
this._selectedFilesURI = desktopManager.getCurrentSelectionAsUri();
this._dockSpringOpenFile = null;
this._currentDesktopFileAppPath = null;
this._dockSpringOpenTime = GLib.get_monotonic_time();
this._dockSpringOpenComplete = false;
this._startMonitoringDockUriNavigation();
}
destroy() {
this._stopMonitoringDockUriNavigation();
}
_startMonitoringDockUriNavigation() {
// Careful, we have to and are calling an async function
// in the timer, which will always return true,
// therefore the function has to kill itself if
// not killed by drag end...
this._dockUriSpringTimerID = GLib.timeout_add(
GLib.PRIORITY_DEFAULT,
this._Enums.DND_SHELL_HOVER_POLL,
async () => {
try {
await this._dockUriSpringTimerFunction();
} catch (e) {
console.error(e);
}
return GLib.SOURCE_REMOVE;
}
);
}
async _lookOffLeftEdgeForDrop(shellDropCoordinates) {
let leftEdge = shellDropCoordinates;
// look upto 50 pixels away for a drop target,
// off the drag cursor to the left, increment 10
// 5 iterations
for (let i = 0; i <= 50; i += 10) {
leftEdge[0] -= i;
this._currentDesktopFileAppPath =
// eslint-disable-next-line no-await-in-loop
await this._DBusUtils.RemoteExtensionControl
.getDropTargetAppInfoDesktopFile(leftEdge)
.catch(e => console.error(e));
if (this._currentDesktopFileAppPath)
break;
}
this._setShellDropCursor();
}
async _dockUriSpringTimerFunction() {
// Failsafe kill the function - remove the timer if going on for
// too long, default 30 seconds
if (!this._dragItem ||
(GLib.get_monotonic_time() - this._dockSpringOpenTime) >
this._Enums.DND_SHELL_HOVER_POLL * 150000
) {
const stopID = this._dockUriSpringTimerID;
this._dockUriSpringTimerID = 0;
this._setShellDropCursor(this._Enums.ShellDropCursor.DEFAULT);
if (stopID)
GLib.Source.remove(stopID);
return GLib.SOURCE_REMOVE;
}
const shellDropCoordinates =
await this._DBusUtils.RemoteExtensionControl
.getDropTargetCoordinates()
.catch(e => console.error(e));
this._lookOffLeftEdgeForDrop(shellDropCoordinates);
if (!this._currentDesktopFileAppPath ||
!(this._currentDesktopFileAppPath.endsWith('Nautilus.desktop') ||
this._currentDesktopFileAppPath.startsWith('file://') ||
this._currentDesktopFileAppPath.startsWith('davs://'))
)
return GLib.SOURCE_CONTINUE;
// On a URI, start hover timing and reset timer
if (!this._dockSpringOpenFile) {
this._dockSpringOpenFile = this._currentDesktopFileAppPath;
this._dockSpringOpenTime = GLib.get_monotonic_time();
return GLib.SOURCE_CONTINUE;
}
// Open the URI, got here after hover timing started
if (this._dockSpringOpenFile === this._currentDesktopFileAppPath &&
!this._dockSpringOpenComplete &&
((GLib.get_monotonic_time() - this._dockSpringOpenTime) >
this._Enums.DND_HOVER_TIMEOUT * 1000)
) {
const context = Gdk.Display.get_default().get_app_launch_context();
context.set_timestamp(Gdk.CURRENT_TIME);
let uri;
try {
if (this._dockSpringOpenFile.endsWith('Nautilus.desktop'))
uri = this._desktopDir.get_uri();
else
uri = this._dockSpringOpenFile;
if (this._Prefs.openFolderOnDndHover)
Gio.AppInfo.launch_default_for_uri(uri, context);
this._dockSpringOpenComplete = true;
} catch (e) {
console.error(
e, `Error opening ${uri} in GNOME Files: ${e.message}`
);
}
return GLib.SOURCE_CONTINUE;
}
// URI is the same, window is opened, do nothing
if (this._dockSpringOpenFile === this._currentDesktopFileAppPath &&
this._dockSpringOpenComplete
)
return GLib.SOURCE_CONTINUE;
// If still alive, window is opened and uri is changed, reset
if (this._dockSpringOpenFile !== this._currentDesktopFileAppPath &&
this._dockSpringOpenComplete
) {
this._dockSpringOpenFile = null;
this._dockSpringOpenComplete = false;
return GLib.SOURCE_CONTINUE;
}
return GLib.SOURCE_REMOVE;
}
_stopMonitoringDockUriNavigation() {
if (this._dockUriSpringTimerID) {
GLib.Source.remove(this._dockUriSpringTimerID);
this._currentDesktopFileAppPath = null;
this._setShellDropCursor(this._Enums.ShellDropCursor.DEFAULT);
}
this._dockUriSpringTimerID = 0;
}
_setShellDropCursor(cursor = null) {
if (!this._DBusUtils.RemoteExtensionControl.isAvailable)
return;
if (cursor) {
this._DBusUtils.RemoteExtensionControl.setDragCursor(cursor);
return;
}
if (!this._currentDesktopFileAppPath) {
this._DBusUtils.RemoteExtensionControl
.setDragCursor(this._Enums.ShellDropCursor.NODROP);
return;
}
try {
if (this._currentDesktopFileAppPath.endsWith('.desktop')) {
const desktopFile =
DesktopAppInfo.new_from_filename(
GLib.build_filenamev(
[this._currentDesktopFileAppPath]
)
);
if (!desktopFile) {
console.log(
'Could not parse desktopFile as a desktop file,' +
' cannot set shell cursor'
);
this._DBusUtils.RemoteExtensionControl
.setDragCursor(this._Enums.ShellDropCursor.NODROP);
return;
}
let object =
this._DesktopIconsUtil
.checkAppOpensFileType(
desktopFile,
null,
this._selectedFiles[0].attributeContentType
);
if (object.canopenFile) {
this._DBusUtils.RemoteExtensionControl
.setDragCursor(this._Enums.ShellDropCursor.COPY);
return;
} else if (
this._currentDesktopFileAppPath
.endsWith('Nautilus.desktop') &&
this._Prefs.openFolderOnDndHover
) {
this._DBusUtils.RemoteExtensionControl
.setDragCursor(this._Enums.ShellDropCursor.MOVE);
return;
} else {
this._DBusUtils.RemoteExtensionControl
.setDragCursor(this._Enums.ShellDropCursor.NODROP);
}
} else if (
this._currentDesktopFileAppPath.startsWith('file://') ||
this._currentDesktopFileAppPath.startsWith('davs://') ||
this._currentDesktopFileAppPath.startsWith('trash://')
) {
this._DBusUtils.RemoteExtensionControl
.setDragCursor(this._Enums.ShellDropCursor.MOVE);
return;
} else {
this._DBusUtils.RemoteExtensionControl
.setDragCursor(this._Enums.ShellDropCursor.NODROP);
return;
}
} catch (e) {
console.error(e,
'Error reading desktop file. Cannot set shell Cursor'
);
}
this._DBusUtils.RemoteExtensionControl
.setDragCursor(this._Enums.ShellDropCursor.NODROP);
}
async completeGnomeShellDrop() {
if (!this._currentDesktopFileAppPath)
return false;
if (this._currentDesktopFileAppPath.endsWith('.desktop')) {
try {
const desktopFile =
DesktopAppInfo
.new_from_filename(
GLib.build_filenamev([this._currentDesktopFileAppPath])
);
if (!desktopFile) {
console.log('Could not parse desktopFile as desktop file');
return false;
}
const object =
this._DesktopIconsUtil
.checkAppOpensFileType(
desktopFile,
null,
this._selectedFiles[0].attributeContentType
);
if (object.canopenFile) {
const context =
Gdk.Display.get_default().get_app_launch_context();
context.set_timestamp(Gdk.CURRENT_TIME);
desktopFile.launch_uris_as_manager(
this._selectedFilesURI,
context,
GLib.SpawnFlags.SEARCH_PATH,
null,
null
);
return true;
} else {
this._showAppCannotOpenError(object.Appname);
return false;
}
} catch (e) {
console.error(e,
'Error reading desktop file. Cannot launch application.'
);
return false;
}
}
if (this._currentDesktopFileAppPath === 'trash:///') {
this._desktopManager.mainApp.activate_action('movetotrash', null);
return true;
}
if (this._currentDesktopFileAppPath.startsWith('file:///') ||
this._currentDesktopFileAppPath.startsWith('davs://')
) {
await this._desktopManager.copyOrMoveUris(
this._selectedFilesURI,
this._currentDesktopFileAppPath,
{},
{}
)
.catch(e => console.error(e));
return true;
}
return false;
}
_showAppCannotOpenError(Appname) {
const timeout = 3000; // In ms
this._desktopManager.showError(
_('Could not open File'),
_('$appName$ can not open files of this Type!')
.replace('$appName$', Appname),
null,
timeout
);
return false;
}
get _desktopDir() {
return this._desktopManager._desktopDir;
}
};

508
ding/app/htmlWidgetHost.js Normal file
View File

@@ -0,0 +1,508 @@
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Gtk4 Port Copyright (C) 2022 - 2025 Sundeep Mediratta (smedius@gmail.com)
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {GLib, GObject, Graphene, Gtk, Gsk} from '../dependencies/gi.js';
import {WidgetApi} from '../dependencies/localFiles.js';
export {HtmlWidgetHost};
const HtmlWidgetHost = class {
/**
* @param {object} params
* {
* instanceId: string,
* widgetId: string,
* frameRect: {x, y, width, height},
* widgetRegistry: WidgetRegistry | null,
* webContext: WebKit.WebContext,
* mode: 'prefs' or 'widget'
* prefsUri: string relative uri
* }
*/
constructor(params) {
this._instanceId = params.instanceId;
this._widgetId = params.widgetId;
this._frameRect = params.frameRect;
this._widgetRegistry = params.widgetRegistry;
this._webContext = params.webContext;
this._mode = params.mode === 'prefs' ? 'prefs' : 'widget';
this._prefsUri = params.prefsUri || null;
this._pendingHostStatePatches = [];
this._pendingPostMessages = [];
this._webView = null;
this._destroyed = false;
this._tickId = 0;
this._mappedNotifyId = 0;
this._makeGtkWidget();
this._start();
}
get actor() {
return this._frame;
}
_webViewReadyPromise() {
if (!this._webViewPromise) {
this._webViewPromise = new Promise(resolve => {
this._webViewResolve = resolve;
});
}
return this._webViewPromise;
}
getWebViewAsync() {
if (this._webView)
return this._webView;
return this._webViewReadyPromise();
}
updateFrame(frameRect) {
this._frameRect = frameRect;
this._frame.set_size_request(
frameRect.width,
frameRect.height
);
}
isAlive() {
return !this._destroyed;
}
destroy() {
this._destroyed = true;
if (this._mappedNotifyId && this._webView)
this._webView.disconnect(this._mappedNotifyId);
this._mappedNotifyId = 0;
if (this._tickId)
this._webView?.remove_tick_callback(this._tickId);
this._tickId = 0;
this._frame.set_child(null);
this._webView.unparent();
this._webView.run_dispose();
this._webView = null;
this._frame = null;
this._pendingHostStatePatches = [];
}
setHostStatePatch(patch) {
if (!patch || typeof patch !== 'object')
return;
if (this._destroyed || !this._webView) {
this._pendingHostStatePatches.push(patch);
return;
}
this._sendHostStatePatch(patch);
}
postMessage(msg) {
if (!msg || typeof msg !== 'object')
return;
if (this._destroyed || !this._webView) {
this._pendingPostMessages.push(msg);
return;
}
this._postMessage(msg);
}
async requestRender() {
if (this._destroyed)
return;
await this.getWebViewAsync();
this._pokeWebViewRender();
}
_makeGtkWidget() {
this._frame = new DingRoundedClip({radius: 8});
this._frame.set_size_request(
this._frameRect.width,
this._frameRect.height
);
this._frame.instanceId = this._instanceId;
this._frame.widgetId = this._widgetId;
}
async _makeWebView() {
this._webView =
await this._webContext.newViewForInstance(
this._widgetId,
this._instanceId
);
this._webView.set_overflow(Gtk.Overflow.HIDDEN);
this._webView.set_name('ding-widget-webview');
this._frame.set_child(this._webView);
}
async _makeUrl() {
let rel = null;
if (this._mode === 'prefs') {
rel = this._prefsUri || null;
} else {
const entryFile =
await this._widgetRegistry.getHtmlEntryFile(this._widgetId);
rel = entryFile ? entryFile.get_basename() : null;
}
if (!rel)
return null;
const id = GLib.uri_escape_string(this._instanceId, null, true);
const path = `/${GLib.uri_escape_string(rel, '/', true)}`;
const mode = this._mode === 'prefs' ? 'prefs' : 'widget';
const query =
`dingMode=${mode}&dingInstanceId=${id}`;
const guri = GLib.uri_build(
GLib.UriFlags.NONE,
'ding-widget',
null,
id,
-1,
path,
query,
null
);
return guri.to_string();
}
// ─────────────────────────
// start orchestration
// ─────────────────────────
async _start() {
const [_, url] = await Promise.all([
this._makeWebView(),
this._makeUrl(),
]).catch(e => logError(e));
this._webViewResolve?.(this._webView);
this._webViewPromise = null;
this._webViewResolve = null;
if (!this._webView)
return;
if (url)
this._webView.load_uri(url);
else
this._loadFallback('Missing entry/prefs URL');
this._installWebViewRenderPoke();
this._flushPendingHostStatePatches();
this._flushPendingMessages();
}
_flushPendingHostStatePatches() {
for (const patch of this._pendingHostStatePatches)
this._sendHostStatePatch(patch);
this._pendingHostStatePatches.length = 0;
}
_flushPendingMessages() {
for (const msg of this._pendingPostMessages)
this._postMessage(msg);
this._pendingPostMessages.length = 0;
}
_sendHostStatePatch(patch) {
let script;
try {
script =
'if (window.ding && ' +
'typeof window.ding._setHostState === "function") ' +
`window.ding._setHostState(${
JSON.stringify(patch)
});`;
} catch (e) {
console.error('HtmlWidgetHost: failed to build host state script:', e);
return;
}
this._evaluateScript(script);
}
_pokeWebViewRender() {
if (!this._webView || this._destroyed)
return;
const wv = this._webView;
if (this._tickId) {
wv.remove_tick_callback(this._tickId);
this._tickId = 0;
}
this._tickId = wv.add_tick_callback(() => {
const w = wv.get_allocated_width();
const h = wv.get_allocated_height();
if (w <= 1 || h <= 1)
return GObject.SOURCE_CONTINUE;
this._tickId = 0;
// Host-side “poke”: invalidate + optional JS nudge
wv.queue_draw();
wv.queue_allocate();
this._frame?.queue_draw();
this._frame?.queue_allocate();
this._nudgeWebViewDomRender();
return GObject.SOURCE_REMOVE;
});
}
_installWebViewRenderPoke() {
const wv = this._webView;
this._mappedNotifyId = wv.connect('notify::mapped', () => {
if (wv.get_mapped())
this._pokeWebViewRender();
});
if (wv.get_mapped())
this._pokeWebViewRender();
}
_nudgeWebViewDomRender() {
if (!this._webView || this._destroyed)
return;
this._evaluateScript(
`try {
const t = String(Date.now());
const de = document.documentElement;
const body = document.body;
if (de) {
de.style.setProperty('--ding-render-poke', t);
de.setAttribute('data-ding-render-poke', t);
}
if (body) {
body.style.setProperty('--ding-render-poke', t);
body.setAttribute('data-ding-render-poke', t);
}
window.dispatchEvent(new Event('resize'));
document.dispatchEvent(new Event('visibilitychange'));
window.dispatchEvent(new Event('pageshow'));
window.dispatchEvent(new Event('focus'));
requestAnimationFrame(() => {});
} catch (e) {}`
);
}
_loadFallback(reason) {
if (!this._webView)
return;
const safeReason = GLib.markup_escape_text(String(reason), -1);
const html = WidgetApi.WIDGET_UNAVAILABLE_HTML.replace(
'__REASON__',
safeReason
);
this._webView.load_html(html, null);
}
_postMessage(msg) {
let script;
try {
script =
'if (typeof window.postMessage === "function") ' +
`window.postMessage(${JSON.stringify(msg)}, "*")`;
} catch (e) {
console.error(
'HtmlWidgetHost: failed to build postMessage script:', e
);
return;
}
this._evaluateScript(script);
}
_evaluateScript(script) {
if (this._destroyed || !this._webView)
return;
try {
this._webView?.evaluate_javascript(
script,
-1,
null,
null,
null,
(wv, res) => {
try {
if (!this._webView)
return;
wv?.evaluate_javascript_finish(res);
} catch (e) {
console.error(
'HtmlWidgetHost: failed to postMessage JS:', e
);
}
}
);
} catch (e) {
console.error(
'HtmlWidgetHost: failed to postMessage to widget:', e
);
}
}
};
/**
* DingRoundedClip
*
* A tiny single-child container that clips its child to a rounded rect
* using GTK4 snapshot APIs (push_rounded_clip).
*
* Intended to be used as the "frame" root for WebKitWebView so that GTK CSS
* border-radius matches the child's visible corners without visual
* artifacts as CSS is not clipping the webview's contents with a box or frame.
*/
export const DingRoundedClip = GObject.registerClass({
GTypeName: 'DingRoundedClip',
Properties: {
radius: GObject.ParamSpec.double(
'radius',
'Radius',
'Corner radius in pixels',
GObject.ParamFlags.READWRITE | GObject.ParamFlags.EXPLICIT_NOTIFY,
0.0,
4096.0,
12.0
),
},
}, class DingRoundedClip extends Gtk.Widget {
_init(params = {}) {
super._init(params);
this._child = null;
this._radius = 12.0;
}
// ─────────────────────────
// properties
// ─────────────────────────
get radius() {
return this._radius;
}
set radius(v) {
const r = Math.max(0.0, Number(v) || 0.0);
if (r === this._radius)
return;
this._radius = r;
this.notify('radius');
this.queue_draw();
}
// ─────────────────────────
// child management
// ─────────────────────────
set_child(child) {
if (child === this._child)
return;
if (this._child) {
this._child.unparent();
this._child = null;
}
this._child = child ?? null;
if (this._child)
this._child.set_parent(this);
this.queue_resize();
}
get_child() {
return this._child;
}
// ─────────────────────────
// layout
// ─────────────────────────
vfunc_measure(orientation, forSize) {
if (!this._child)
return [0, 0, -1, -1];
return this._child.measure(orientation, forSize);
}
vfunc_size_allocate(width, height, baseline) {
if (!this._child)
return;
this._child.allocate(width, height, baseline, null);
}
// ─────────────────────────
// rendering
//
// Snapshot is called by GTK4 to render the widget inside the clipped area.
// ─────────────────────────
vfunc_snapshot(snapshot) {
if (!this._child)
return;
const width = this.get_width();
const height = this.get_height();
if (width <= 0 || height <= 0)
return;
const rect = new Graphene.Rect();
rect.init(0, 0, width, height);
const r = this._radius;
const size = new Graphene.Size();
size.init(r, r);
const rr = new Gsk.RoundedRect();
rr.init(rect, size, size, size, size);
snapshot.push_rounded_clip(rr);
this.snapshot_child(this._child, snapshot);
snapshot.pop();
}
});

View File

@@ -0,0 +1,599 @@
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Gtk4 Port Copyright (C) 2022 - 2025 Sundeep Mediratta (smedius@gmail.com)
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {Gio, GLib} from '../dependencies/gi.js';
import {HtmlWidgetHost} from '../dependencies/localFiles.js';
export {HtmlWidgetHostWithBackend};
const HtmlWidgetHostWithBackend = class extends HtmlWidgetHost {
constructor(params) {
super(params);
this._backendProc = null;
this._backendIn = null;
this._backendOut = null;
this._backendErr = null;
this._backendReading = false;
this._backendPending = new Map();
this._decoder = new TextDecoder('utf-8');
this._pendingBackendRequests = [];
this._pendingBackendEvents = [];
this._backendEnsurePromise = null;
}
async _ensureBackend(inst) {
if (!inst || this._destroyed)
return false;
if (this._backendProc)
return true;
if (this._backendEnsurePromise)
return this._backendEnsurePromise;
const ensurePromise = this._startBackend(inst);
this._backendEnsurePromise = ensurePromise;
let result;
try {
result = await ensurePromise;
} finally {
if (this._backendEnsurePromise === ensurePromise)
this._backendEnsurePromise = null;
}
if (result?.ok) {
this._flushPendingBackendRequests();
this._flushPendingBackendEvents();
return true;
}
this._failPendingBackendRequests(
inst,
result?.error ?? {
code: 'E_NO_BACKEND',
message: 'No backend configured',
}
);
return false;
}
async _buildBackendSpec(inst) {
if (!inst)
return null;
if (inst.backendSpec)
return inst.backendSpec;
if (!this._widgetRegistry)
return null;
let desc = null;
try {
desc = await this._widgetRegistry.getDescriptor(inst.widgetId);
} catch (e) {
console.error(
'HtmlWidgetHostWithBackend: failed to fetch descriptor:',
e
);
return null;
}
if (!desc) {
console.error(
'HtmlWidgetHostWithBackend: no descriptor for widget',
inst?.widgetId ?? '<unknown>'
);
return null;
}
const spec =
this._widgetRegistry.normalizeBackendSpec(desc, inst);
inst.backendSpec = spec || null;
return spec;
}
async _startBackend(inst) {
let spec = inst?.backendSpec;
if (!spec)
spec = await this._buildBackendSpec(inst);
if (!spec?.argv?.length) {
console.error(
'HtmlWidgetHostWithBackend: no backend configured for widget',
inst?.widgetId ?? '<unknown>'
);
return {
ok: false,
error: {code: 'E_NO_BACKEND', message: 'No backend configured'},
};
}
try {
const launcher = new Gio.SubprocessLauncher({
flags: Gio.SubprocessFlags.STDIN_PIPE |
Gio.SubprocessFlags.STDOUT_PIPE |
Gio.SubprocessFlags.STDERR_PIPE,
});
if (spec.cwd)
launcher.set_cwd(spec.cwd);
if (spec.env) {
for (const [key, value] of Object.entries(spec.env)) {
if (typeof key !== 'string')
continue;
launcher.setenv(key, String(value ?? ''), true);
}
}
const argv = Array.isArray(spec.argv) ? [...spec.argv] : [];
this._backendProc = launcher.spawnv(argv);
this._backendIn = new Gio.DataOutputStream({
base_stream: this._backendProc.get_stdin_pipe(),
});
this._backendOut = new Gio.DataInputStream({
base_stream: this._backendProc.get_stdout_pipe(),
});
this._backendErr = new Gio.DataInputStream({
base_stream: this._backendProc.get_stderr_pipe(),
});
this._backendReading = true;
// Read stdout (protocol messages)
this._readBackendStream(
inst,
this._backendOut,
msg => this._handleBackendMessage(inst, msg),
'stdout'
).catch(e => {
console.error('BACKEND stdout loop error:', e?.message ?? e);
});
// Read stderr (debug/logs)
this._readBackendStream(
inst,
this._backendErr,
line => {
try {
console.warn(
'BACKEND stderr:',
inst?.instanceId ?? '<unknown>',
line.trim()
);
} catch (_e) {}
},
'stderr'
).catch(e => {
console.error('BACKEND stderr loop error:', e?.message ?? e);
});
this._waitBackend(inst);
this._sendBackend({
type: 'hello',
instanceId: inst.instanceId,
widgetId: inst.widgetId,
mode: 'widget',
config: inst.config || {},
});
return {ok: true};
} catch (e) {
console.error(
'HtmlWidgetHostWithBackend: failed to start backend:', e
);
this._handleBackendExit(inst, {
code: 'E_BACKEND_START',
message: e?.message ?? 'Failed to start backend',
});
return {
ok: false,
error: {
code: 'E_BACKEND_START',
message: e?.message ?? 'Failed to start backend',
},
};
}
}
// Backend expects newline-delimited JSON objects. Known outbound shapes:
// - hello: {type, instanceId, widgetId, mode, config}
// - request {type, id, method, params}
_sendBackend(obj) {
if (!this._backendIn)
return;
try {
this._backendIn.put_string(`${JSON.stringify(obj)}\n`, null);
this._backendIn.flush(null);
} catch (e) {
console.error(
'HtmlWidgetHostWithBackend: write backend failed:', e
);
}
}
async _readBackendStream(inst, stream, onLine, label) {
if (!stream)
return;
while (this._backendReading && stream) {
let line;
try {
// eslint-disable-next-line no-await-in-loop
const [bytes] = await stream.read_line_async(
GLib.PRIORITY_DEFAULT,
null
);
if (!bytes)
break;
line = this._decoder.decode(bytes);
} catch (e) {
console.error('BACKEND stream read error:', label, e?.message ?? e);
break;
}
try {
let payload = line;
if (label === 'stdout') {
try {
payload = JSON.parse(line);
} catch (e) {
console.error('BACKEND stdout JSON parse error:', e?.message ?? e);
continue;
}
}
onLine?.(payload);
} catch (_e) {
// Ignore per-line handler errors
}
}
if (this._backendReading) {
this._backendReading = false;
this._handleBackendExit(inst, {
code: 'E_BACKEND_EXIT',
message: 'Backend process exited',
});
}
}
_flushPendingBackendRequests() {
if (!this._pendingBackendRequests.length)
return;
for (const entry of this._pendingBackendRequests)
this._dispatchBackendRequest(entry.payload);
this._pendingBackendRequests.length = 0;
}
_flushPendingBackendEvents() {
if (!this._pendingBackendEvents.length)
return;
for (const entry of this._pendingBackendEvents) {
this._sendBackend({
type: 'event',
name: entry.name,
payload: entry.payload || {},
});
}
this._pendingBackendEvents.length = 0;
}
_handleBackendExit(inst, error) {
this._backendReading = false;
const proc = this._backendProc;
this._backendProc = null;
this._backendIn = null;
this._backendOut = null;
this._backendErr = null;
if (this._destroyed)
return;
this._logBackendExit(inst, proc, error);
this._failInFlightBackendRequests(inst, error);
this._pendingBackendRequests.length = 0;
this._pendingBackendEvents.length = 0;
}
_failPendingBackendRequests(inst, error) {
if (!this._pendingBackendRequests.length || this._destroyed) {
this._pendingBackendRequests.length = 0;
return;
}
const requestIds = [];
for (const entry of this._pendingBackendRequests) {
const requestId = entry.payload?.requestId;
if (requestId)
requestIds.push(requestId);
}
this._failBackendRequestIds(inst, requestIds, error);
this._pendingBackendRequests.length = 0;
}
_failInFlightBackendRequests(inst, error) {
if (!this._backendPending.size || this._destroyed) {
this._backendPending.clear();
return;
}
const requestIds = Array.from(this._backendPending.keys());
this._backendPending.clear();
this._failBackendRequestIds(inst, requestIds, error);
}
_failBackendRequestIds(inst, requestIds, error) {
if (!requestIds.length || this._destroyed)
return;
const instanceId = inst?.instanceId;
if (!instanceId)
return;
const err = error || {
code: 'E_BACKEND_FAILURE',
message: 'Backend unavailable',
};
for (const requestId of requestIds) {
this.postMessage({
_dingInternal: true,
type: 'backendReply',
instanceId,
requestId,
ok: false,
error: err,
});
}
}
_dispatchBackendRequest(payload) {
if (!payload || !this._backendProc || this._destroyed)
return;
const {requestId, method, params} = payload;
if (requestId === undefined || requestId === null)
return;
this._backendPending.set(requestId, true);
this._sendBackend({
type: 'request',
id: requestId,
method,
params: params || {},
});
}
// Inbound JSON objects are expected to be of type response, event or log.
// - response: {type, id, ok, result, error}
// - event: {type, name, payload}
// - log: {type, level, message}
// with author-defined 'name' and custom JSON 'payload'
_handleBackendMessage(inst, msg) {
if (!msg || typeof msg !== 'object')
return;
switch (msg.type) {
case 'response': {
const requestId = msg.id;
this._backendPending.delete(requestId);
this.postMessage({
_dingInternal: true,
type: 'backendReply',
instanceId: inst.instanceId,
requestId,
ok: !!msg.ok,
result: msg.result,
error: msg.error,
});
break;
}
case 'event': {
this.postMessage({
_dingInternal: true,
type: 'backendEvent',
instanceId: inst.instanceId,
name: msg.name,
payload: msg.payload,
});
break;
}
case 'log': {
const level = msg.level || 'log';
const text = msg.message || '';
console.log(`HtmlWidget backend ${level}:`, inst.instanceId, text);
break;
}
default:
break;
}
}
async backendRequest(inst, payload) {
if (!inst || !payload || this._destroyed)
return;
if (this._backendProc) {
this._dispatchBackendRequest(payload);
return;
}
this._pendingBackendRequests.push({
instanceId: inst.instanceId,
payload,
});
await this._ensureBackend(inst);
}
backendSend(inst, payload) {
if (!inst || !payload || this._destroyed)
return;
const {name, payload: data} = payload || {};
if (this._backendProc) {
this._sendBackend({
type: 'event',
name,
payload: data || {},
});
return;
}
this._pendingBackendEvents.push({
name,
payload: data || {},
});
this._ensureBackend(inst);
}
destroy() {
this._destroyed = true;
try {
if (this._backendIn)
this._sendBackend({type: 'shutdown'});
} catch {}
this._backendReading = false;
try {
this._backendProc?.send_signal?.(15);
} catch {}
try {
this._backendProc?.force_exit();
} catch {}
this._backendProc = null;
this._backendIn = null;
this._backendOut = null;
this._backendErr = null;
this._pendingBackendRequests.length = 0;
this._pendingBackendEvents.length = 0;
this._backendEnsurePromise = null;
super.destroy();
}
async _readBackendStderr(inst) {
if (!this._backendErr)
return;
while (!this._destroyed && this._backendErr) {
try {
// eslint-disable-next-line no-await-in-loop
const [bytes] = await this._backendErr.read_line_async(
GLib.PRIORITY_DEFAULT,
null
);
if (!bytes)
break;
const line = this._decoder.decode(bytes);
console.error(
'BACKEND STDERR:',
inst?.instanceId ?? '<unknown>',
line.trim()
);
} catch (_e) {
break;
}
}
}
async _waitBackend(inst) {
const proc = this._backendProc;
if (!proc)
return;
try {
const ok = await new Promise((resolve, reject) => {
proc.wait_check_async(null, (p, res) => {
try {
resolve(p.wait_check_finish(res));
} catch (e) {
reject(e);
}
});
});
console.error(
'BACKEND EXIT STATUS:',
inst?.instanceId ?? '<unknown>',
ok ? 'ok' : 'fail',
'status:',
proc.get_exit_status()
);
} catch (e) {
if (this._destroyed)
return;
console.error(
'BACKEND EXIT wait error:',
inst?.instanceId ?? '<unknown>',
e?.message ?? e
);
}
}
_logBackendExit(inst, proc, error) {
if (!proc)
return;
const pid = proc.get_identifier();
const msg = error?.message ?? 'Backend process exited';
const code = error?.code ? `(${error.code})` : '';
const pidStr = pid ? `pid ${pid}` : 'pid unknown';
console.error(
'HtmlWidget backend exit:',
inst?.instanceId ?? '<unknown>',
pidStr,
msg,
code
);
}
};

1102
ding/app/preferences.js Normal file

File diff suppressed because it is too large Load Diff

732
ding/app/shortcutManager.js Normal file
View File

@@ -0,0 +1,732 @@
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Adw/Gtk4 Port Copyright (C) 2025 Sundeep Mediratta (smedius@gmail.com)
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {Adw, Gdk, Gio, GLib, GObject, Gtk, Pango} from '../dependencies/gi.js';
import {_} from '../dependencies/gettext.js';
import {DefaultShortcuts} from '../dependencies/localFiles.js';
import {GlobalShortcuts} from '../dependencies/localFiles.js';
export {ShortcutManager};
const DisplayShortcutRow = GObject.registerClass(
class DisplayShortcutRow extends Adw.ActionRow {
constructor({actionname, actionmap, readaccel}) {
super({});
this.actionNamed = actionname;
this.actionMap = actionmap;
this.readaccel = readaccel;
this._defaultShortcuts = DefaultShortcuts;
this.accelLabel = new Gtk.Label({
label: '',
xalign: 1,
css_classes: ['monospace'],
ellipsize: Pango.EllipsizeMode.END,
max_width_chars: 28,
width_chars: 16,
halign: Gtk.Align.END,
hexpand: false,
single_line_mode: true,
});
this.add_suffix(this.accelLabel);
this.updateRow();
}
updateRow() {
const accels = this.readaccel(this.actionNamed);
let accelList = [];
if (Array.isArray(accels))
accelList = accels;
else if (typeof accels === 'string' && accels.length)
accelList = accels.split(',');
this.accelText = _('None');
if (accelList.length)
this.accelText = accelList.map(a => a.trim()).join(', ');
this.accelLabel.set_label(this.accelText);
this.accelLabel.set_tooltip_text(this.accelText);
this.description =
this._defaultShortcuts[this.actionNamed].Hint ||
this._prettify(this.actionNamed);
this.set_title(this.description);
}
_prettify(name) {
const prettyName =
name.charAt(0).toUpperCase() +
name.slice(1).replace(/[-_]/g, ' ');
return prettyName;
}
}
);
const EditableShortcutRow = GObject.registerClass(
class EditableShortcutRow extends DisplayShortcutRow {
constructor({actionname, actionmap, readaccel, writeaccel}) {
super({actionname, actionmap, readaccel});
this.writeaccel = writeaccel;
if (Adw.get_minor_version() > 2)
this.set_subtitle_selectable(false);
this.addEditor();
}
updateRow() {
super.updateRow();
this.use_markup = false;
this.defaultAccel = this._defaultShortcuts[this.actionNamed].Accel;
const subtitlestring = _('Default Shortcut:');
const subtitle = this.defaultAccel ? this.defaultAccel : _('None');
this.set_subtitle(`${subtitlestring} ${subtitle}`);
}
addEditor() {
this.editIcon = Gtk.Image.new_from_icon_name('xapp-edit-symbolic');
this.editIcon.margin_start = 10;
this.add_suffix(this.editIcon);
this.set_activatable_widget(this.editIcon);
this.makeActive();
}
makeActive() {
this.activatable = true;
this.set_sensitive = true;
this.connect('activated', () => this.setShortcut());
}
setShortcut() {
if (this.changingKey)
return;
this.changingKey = true;
this.accelLabel.set_label(_('Type new...'));
this.resetIcon = Gtk.Image.new_from_icon_name('revert');
this.resetIcon.margin_start = 10;
this.clearIcon = Gtk.Image.new_from_icon_name('no');
this.clearIcon.margin_start = 10;
const shortcutEditor = new Gtk.Entry({
editable: false,
hexpand: false,
vexpand: false,
halign: Gtk.Align.END,
valign: Gtk.Align.CENTER,
xalign: 0, // Right-align
placeholder_text:
_('Modifier + Key (e.g. Ctrl + Alt + D)'),
width_chars: 30,
can_focus: true,
has_frame: true,
primary_icon_name: 'edit-undo-symbolic',
primary_icon_tooltip_text: _('Reset to Default'),
primary_icon_sensitive: this.defaultAccel !== this.accelText,
primary_icon_activatable: true,
secondary_icon_name: 'ding-edit-delete-symbolic',
secondary_icon_tooltip_text: _('No Accelerator'),
secondary_icon_sensitive: true,
secondary_icon_activatable: true,
});
const keyController = new Gtk.EventControllerKey();
shortcutEditor.add_controller(keyController);
let popover = new Gtk.Popover({
has_arrow: false,
autohide: true,
child: shortcutEditor,
});
popover.set_parent(this.accelLabel);
popover.set_position(Gtk.PositionType.BOTTOM);
popover.popup();
shortcutEditor.grab_focus_without_selecting();
const finishEditing = () => {
this.changingKey = false;
this.updateRow();
};
shortcutEditor.connect('activate', () => {
const newaccelstring = '';
shortcutEditor.set_text('');
this.writeaccel(this.actionNamed, newaccelstring);
popover.popdown();
}); // on Enter
shortcutEditor.connect('icon-press', (entry, position) => {
switch (position) {
case Gtk.EntryIconPosition.PRIMARY:
this.writeaccel(this.actionNamed, this.defaultAccel);
popover.hide();
break;
case Gtk.EntryIconPosition.SECONDARY:
shortcutEditor.emit('activate');
break;
}
});
// On popover close (via outside click)
popover.connect('hide', () => {
finishEditing();
popover.unparent();
popover = null;
});
keyController.connect(
'key-pressed', (actor, keyval, keycode, state) => {
let newaccelstring;
if (keyval === Gdk.KEY_Escape)
popover.popdown();
if (state &&
keyval !== Gdk.KEY_Shift_L &&
keyval !== Gdk.KEY_Shift_R &&
keyval !== Gdk.KEY_Control_L &&
keyval !== Gdk.KEY_Control_R &&
keyval !== Gdk.KEY_Alt_L &&
keyval !== Gdk.KEY_Alt_R &&
keyval !== Gdk.KEY_Meta_L &&
keyval !== Gdk.KEY_Meta_R &&
keyval !== Gdk.KEY_Super_L &&
keyval !== Gdk.KEY_Super_R &&
keyval !== Gdk.KEY_Caps_Lock &&
keyval !== Gdk.KEY_Num_Lock &&
keyval !== Gdk.KEY_AltGr_L &&
keyval !== Gdk.KEY_AltGr_R &&
keyval !== Gdk.KEY_ISO_Level3_Shift &&
keyval !== Gdk.KEY_ISO_Level3_Lock &&
keyval !== Gdk.KEY_ISO_Level5_Shift &&
keyval !== Gdk.KEY_ISO_Level5_Lock
) {
const mask =
state & Gtk.accelerator_get_default_mod_mask();
newaccelstring = Gtk.accelerator_name(keyval, mask);
shortcutEditor.set_text(newaccelstring);
const oldaccelstring = this.readaccel(this.actionNamed);
if (oldaccelstring !== newaccelstring)
this.writeaccel(this.actionNamed, newaccelstring);
popover.hide();
}
return true;
});
}
}
);
const ShortcutViewer = GObject.registerClass(
class ShortcutViewer extends Adw.PreferencesGroup {
constructor(params = {}) {
super({});
this._shortcutManager = params.manager;
this._actionMap = this._shortcutManager._mainApp;
this._localShortcuts = this._shortcutManager._localShortcuts;
this.readaccel =
this._shortcutManager.readActionShortcut
.bind(this._shortcutManager);
this.set_title(_('System Shortcuts'));
this.set_description(_('Common System Defined Keyboard Shortcuts'));
this._addLocalShortcuts();
}
_addLocalShortcuts() {
if (!this._actionMap)
return;
const actions =
this._actionMap.list_actions()
.sort((a, b) => {
return a
.localeCompare(
b,
{
sensitivity: 'accent',
numeric: 'true',
localeMatcher: 'lookup',
}
);
});
for (const action of actions) {
if (this._localShortcuts[action]?.Edit ||
this._localShortcuts[action]?.Global ||
!this._localShortcuts[action]?.Accel
)
continue;
const actionRow =
new DisplayShortcutRow({
'actionname': action,
'actionmap': this._actionMap,
'readaccel': this.readaccel.bind(this),
});
this.add(actionRow);
}
}
});
const LocalShortcutEditor = GObject.registerClass(
class LocalShortcutEditor extends Adw.PreferencesGroup {
constructor(params = {}) {
super({});
this._shortcutManager = params.manager;
this._actionMap = this._shortcutManager._mainApp;
this._localShortcuts = this._shortcutManager._localShortcuts;
this._rows = [];
this.readaccel =
this._shortcutManager.readActionShortcut
.bind(this._shortcutManager);
this.writeaccel =
this._shortcutManager.writeActionShortcut
.bind(this._shortcutManager);
this.set_title(_('Local Shortcuts'));
this.set_description(_('Application Keyboard Shortcuts'));
this._addLocalShortcuts();
}
_addLocalShortcuts() {
if (!this._actionMap)
return;
const actions =
this._actionMap.list_actions()
.sort((a, b) => {
return a
.localeCompare(
b,
{
sensitivity: 'accent',
numeric: 'true',
localeMatcher: 'lookup',
}
);
});
for (const action of actions) {
if (!this._localShortcuts[action]?.Edit)
continue;
const actionRow =
new EditableShortcutRow({
'actionname': action,
'actionmap': this._actionMap,
'readaccel': this.readaccel.bind(this),
'writeaccel': this.writeaccel.bind(this),
});
this.add(actionRow);
this._rows.push(actionRow);
}
}
update() {
this._rows.forEach(row => row.updateRow());
}
});
const GlobalShortcutEditor = GObject.registerClass(
class GlobalShortcutEditor extends Adw.PreferencesGroup {
constructor(params = {}) {
super({});
this._shortcutManager = params.manager;
this._actionMap = this._shortcutManager._mainApp;
this._globalShortcuts = this._shortcutManager._globalShortcuts;
this._rows = [];
this.readaccel =
this._shortcutManager.readGlobalActionShortcut
.bind(this._shortcutManager);
this.writeaccel =
this._shortcutManager.writeGlobalActionShortcut
.bind(this._shortcutManager);
this.set_title(_('Global Shortcuts'));
this.set_description(_('System Keyboard Shortcuts'));
this._addGlobalShortcuts();
}
_addGlobalShortcuts() {
if (!this._actionMap)
return;
const actions =
this._actionMap.list_actions()
.sort((a, b) => {
return a
.localeCompare(
b,
{
sensitivity: 'accent',
numeric: 'true',
localeMatcher: 'lookup',
}
);
});
for (const action of actions) {
if (!this._globalShortcuts[action]?.Global)
continue;
const actionRow =
new EditableShortcutRow({
'actionname': action,
'actionmap': this._actionMap,
'readaccel': this.readaccel.bind(this),
'writeaccel': this.writeaccel.bind(this),
});
this.add(actionRow);
this._rows.push(actionRow);
}
}
update() {
this._rows.forEach(row => row.updateRow());
}
}
);
const ShortcutManager = class {
constructor(desktopManager) {
this._desktopManager = desktopManager;
this._desktopSettings = desktopManager.Prefs.desktopSettings;
this._mainApp = desktopManager.mainApp;
this._globalShortcuts = GlobalShortcuts;
this._localShortcuts = DefaultShortcuts;
this._overRideMap = new Map();
this._initializeOurShortcuts();
this._monitorUserShortcuts();
this._refreshUserShortcuts();
this._addTextEntryActions();
this._mainApp.connect(
'action-added',
(_app, name) => this._setAccel(name)
);
this._mainApp.connect(
'action-enabled-changed',
(_app, name, _enabled) => this._setAccel(name)
);
// Global shortcuts are automatically monitored and set by the
// extension from settings
}
_addTextEntryActions() {
const textEntryOn = Gio.SimpleAction.new('textEntryOn', null);
textEntryOn.connect('activate', this._textEntryAccelsTurnOn.bind(this));
this._mainApp.add_action(textEntryOn);
const textEntryOff = Gio.SimpleAction.new('textEntryOff', null);
textEntryOff.connect('activate', this._textEntryAccelsTurnOff.bind(this));
this._mainApp.add_action(textEntryOff);
}
// this function is not used, but is another way of setting action
// descriptions.
_setStateHints() {
for (const [actionName, {Hint}] of
Object.entries(this._localShortcuts)
) {
const action = this._mainApp.lookup_action(actionName);
if (action) {
action.set_state_hint(
GLib.Variant.new_string(Hint)
);
}
}
}
_monitorUserShortcuts() {
this._userShortcutMonitor = this._desktopSettings.connect(
'changed',
(obj, key) => {
if (key === 'shortcutoverrides')
this._refreshUserShortcuts();
}
);
}
_refreshUserShortcuts() {
this._readUserShortcuts();
this._setAllAccels();
}
_readUserShortcuts() {
const value =
this._desktopSettings.get_value('shortcutoverrides')
.deep_unpack();
this._overRideMap = new Map(Object.entries(value));
}
_writeUserShortcuts() {
const value = Object.fromEntries(this._overRideMap);
const variant = new GLib.Variant('a{ss}', value);
this._desktopSettings.set_value('shortcutoverrides', variant);
}
_setAllAccels() {
for (const actionName of Object.keys(this._localShortcuts))
this._setAccel(actionName);
}
_setAccel(actionName) {
const action = this._mainApp.lookup_action(actionName);
if (!action)
return;
const accel = this._readOverRideActionShortcut(actionName);
const accelarray = accel.length ? accel.split(',') : [];
this._mainApp.set_accels_for_action(
`app.${actionName}`, accelarray
);
}
_readOverRideActionShortcut(actionName) {
const defaultShortCut = this._localShortcuts[actionName].Accel ?? '';
const userShortcut = this._overRideMap.get(actionName);
const overrideShortCut = this._overRideMap.has(actionName)
? userShortcut
: defaultShortCut;
return overrideShortCut;
}
readActionShortcut(actionName) {
return this._mainApp.get_accels_for_action(`app.${actionName}`);
}
writeActionShortcut(actionName, accel) {
if (accel.length || accel === '')
this._overRideMap.set(actionName, accel);
else
this._overRideMap.delete(actionName);
this._desktopSettings.block_signal_handler(this._userShortcutMonitor);
this._writeUserShortcuts();
this._setAccel(actionName);
this._desktopSettings.unblock_signal_handler(this._userShortcutMonitor);
}
readGlobalActionShortcut(actionName) {
return this._desktopSettings.get_strv(actionName.toLowerCase());
}
writeGlobalActionShortcut(actionName, accel) {
let accelArray = [];
if (Array.isArray(accel))
accelArray = accel;
else if (accel.length)
accelArray = accel.split(',');
this._desktopSettings.set_strv(actionName.toLowerCase(), accelArray);
}
_initializeOurShortcuts() {
const showShortcutViewer =
Gio.SimpleAction.new('showShortcutViewer', null);
showShortcutViewer.connect('activate', () => {
this._showShortcutViewer();
});
this._mainApp.add_action(showShortcutViewer);
const textEntryAccelsTurnOn =
Gio.SimpleAction.new('textEntryAccelsTurnOn', null);
textEntryAccelsTurnOn.connect('activate', () => {
this._textEntryAccelsTurnOn();
});
this._mainApp.add_action(textEntryAccelsTurnOn);
const textEntryAccelsTurnOff =
Gio.SimpleAction.new('textEntryAccelsTurnOff', null);
textEntryAccelsTurnOff.connect('activate', () => {
this._textEntryAccelsTurnOff();
});
this._mainApp.add_action(textEntryAccelsTurnOff);
}
_textEntryAccelsTurnOn() {
this._mainApp.set_accels_for_action(
'app.previewAction',
this._localShortcuts.previewAction.Accel.split(',')
);
this._mainApp.set_accels_for_action(
'app.unselectAll',
this._localShortcuts.unselectAll.Accel.split(',')
);
this._mainApp.set_accels_for_action(
'app.openOneFileAction',
this._localShortcuts.openOneFileAction.Accel.split(',')
);
this._mainApp.set_accels_for_action(
'app.movetotrash',
this._localShortcuts.movetotrash.Accel.split(',')
);
this._mainApp.set_accels_for_action(
'app.chooseIconLeft',
this._localShortcuts.chooseIconLeft.Accel.split(',')
);
this._mainApp.set_accels_for_action(
'app.chooseIconRight',
this._localShortcuts.chooseIconRight.Accel.split(',')
);
this._mainApp.set_accels_for_action(
'app.chooseIconUp',
this._localShortcuts.chooseIconUp.Accel.split(',')
);
this._mainApp.set_accels_for_action(
'app.chooseIconDown',
this._localShortcuts.chooseIconDown.Accel.split(',')
);
this._mainApp.set_accels_for_action(
'app.menuKeyPressed',
this._localShortcuts.menuKeyPressed.Accel.split(',')
);
this._mainApp.set_accels_for_action(
'app.findFiles',
this._localShortcuts.findFiles.Accel.split(',')
);
this._mainApp.set_accels_for_action(
'app.toggleKeyboardSelection',
this._localShortcuts.toggleKeyboardSelection.Accel.split(',')
);
}
_textEntryAccelsTurnOff() {
this._mainApp.set_accels_for_action('app.previewAction', ['']);
this._mainApp.set_accels_for_action('app.unselectAll', ['']);
this._mainApp.set_accels_for_action('app.openOneFileAction', ['']);
this._mainApp.set_accels_for_action('app.movetotrash', ['']);
this._mainApp.set_accels_for_action('app.chooseIconLeft', ['']);
this._mainApp.set_accels_for_action('app.chooseIconRight', ['']);
this._mainApp.set_accels_for_action('app.chooseIconUp', ['']);
this._mainApp.set_accels_for_action('app.chooseIconDown', ['']);
this._mainApp.set_accels_for_action('app.menuKeyPressed', ['']);
this._mainApp.set_accels_for_action('app.findFiles', ['']);
this._mainApp.set_accels_for_action(
'app.toggleKeyboardSelection',
['']
);
}
_resetGlobalShortcuts() {
Object.keys(this._globalShortcuts).forEach(actionKey => {
const defaultAccel = this._globalShortcuts[actionKey]?.Accel;
this.writeGlobalActionShortcut(actionKey, defaultAccel);
});
this.globalShortcutGroup?.update();
}
_resetLocalShortcuts() {
this._overRideMap = new Map();
this._writeUserShortcuts();
this._refreshUserShortcuts();
this.localShortcutGroup?.update();
}
_resetAllShortcuts() {
this._resetGlobalShortcuts();
this._resetLocalShortcuts();
console.log('All Shortcuts reset to Defaults!');
}
_showShortcutViewer() {
if (this._shortCutsWindow)
return;
const shortcutsWindow = new Adw.PreferencesWindow();
shortcutsWindow.set_can_navigate_back(true);
shortcutsWindow.set_search_enabled(true);
shortcutsWindow.set_application(this._mainApp);
shortcutsWindow.set_default_size(400, 600);
shortcutsWindow.set_decorated(true);
shortcutsWindow.set_deletable(true);
shortcutsWindow.set_name('shortcutsWindow');
shortcutsWindow.set_title('Shortcuts');
shortcutsWindow.set_default_size(600, 650);
// Do not make modal or skip-taskbar as we have a .desktop icon
// showing up in the dock for the window to assist navigation.
// const modal = true;
// this._DesktopIconsUtil.windowHidePagerTaskbarModal(
// shortcutsWindow, modal);
const shortcutsFrame = Adw.PreferencesPage.new();
shortcutsFrame.set_name(_('Keyboard Shortcuts'));
const systemShortcutGroup = new ShortcutViewer({manager: this});
shortcutsFrame.add(systemShortcutGroup);
this.globalShortcutGroup = new GlobalShortcutEditor({manager: this});
shortcutsFrame.add(this.globalShortcutGroup);
this.localShortcutGroup = new LocalShortcutEditor({manager: this});
shortcutsFrame.add(this.localShortcutGroup);
const resetGroup = new Adw.PreferencesGroup({
title: _('Reset Shortcuts'),
description: _('Reset all shortcuts to Defaults'),
});
const resetButton = new Adw.ActionRow({
title: _('Reset All...'),
});
const icon = Gtk.Image.new_from_icon_name('edit-undo-symbolic');
resetButton.add_suffix(icon);
resetButton.set_activatable_widget(icon);
resetButton.connect('activated', this._resetAllShortcuts.bind(this));
resetButton.get_style_context().add_class('destructive-action');
resetGroup.add(resetButton);
shortcutsFrame.add(resetGroup);
shortcutsWindow.add(shortcutsFrame);
this._shortCutsWindow = shortcutsWindow;
shortcutsWindow.connect('close-request', () => {
this._shortCutsWindow = null;
this.globalShortcutGroup = null;
this.localShortcutGroup = null;
});
shortcutsWindow.show();
}
};

102
ding/app/shortcuts.js Normal file
View File

@@ -0,0 +1,102 @@
import {_} from '../dependencies/gettext.js';
// app.actionName, Hint: Hint to display for action, Accel: Accelerator Key
// Editing this file will automatically set the hints and Accelerator when
// the program is started.
//
// Make sure file is not broken by edits!
export const DefaultShortcuts = {
doNewFolder: {Hint: _('New Folder'), Accel: '<Control><Shift>N', Edit: true},
doPaste: {Hint: _('Paste'), Accel: '<Control>V'},
doUndo: {Hint: _('Undo'), Accel: '<Control>Z'},
doRedo: {Hint: _('Redo'), Accel: '<Control><Shift>Z'},
selectAll: {Hint: _('Select All'), Accel: '<Control>A'},
showDesktopInFiles: {Hint: _('Show Desktop in Files'), Accel: '', Edit: true},
openInTerminal: {Hint: _('Open in Terminal'), Accel: '', Edit: true},
changeBackGround: {Hint: _('Change Background'), Accel: '', Edit: true},
changeDisplaySettings: {Hint: _('Change Display Settings'), Accel: '', Edit: true},
changeDesktopIconSettings: {Hint: _('Change Desktop Icon Settings'), Accel: '', Edit: true},
cleanUpIcons: {Hint: _('Clean Up Icons'), Accel: '', Edit: true},
'keep-arranged': {Hint: _('Keep Arranged'), Accel: '', Edit: true},
'keep-stacked': {Hint: _('Keep Stacked'), Accel: '', Edit: true},
sortSpecialFolders: {Hint: _('Sort Special Folders'), Accel: ''},
arrangeByName: {Hint: _('Arrange Icons by Name'), Accel: '', Edit: true},
arrangeByDescendingName: {Hint: _('Arrange Icons By Descending Name'), Accel: '', Edit: true},
arrangeByModifiedTime: {Hint: _('Arrange Icons By Modified Time'), Accel: '', Edit: true},
arrangeByKind: {Hint: _('Arrange Icons By Kind'), Accel: '', Edit: true},
arrangeBySize: {Hint: _('Arrange Icons By Size'), Accel: '', Edit: true},
findFiles: {Hint: _('Find Files'), Accel: '<Control>F', Edit: true},
updateDesktop: {Hint: _('Update Desktop'), Accel: 'F5', Edit: true},
showHideHiddenFiles: {Hint: _('Show Hidden Files'), Accel: '<Control>H'},
unselectAll: {Hint: _('Unselect All'), Accel: 'Escape'},
previewAction: {Hint: _('Preview'), Accel: 'space'},
toggleKeyboardSelection: {Hint: _('Toggle Keyboard Selection'),
Accel: '<Control>space'},
toggleWidgetLayer: {Hint: _('Toggle Widget Layer'), Accel: '<Control><Shift>L', Edit: true},
addWidget: {Hint: _('Add Widget'), Accel: '<Shift><Control>plus', Edit: true},
// Allow navigation while holding Shift/Ctrl/Alt (and their shift combos)
chooseIconLeft: {Hint: _('Choose Icon Left'),
Accel: 'Left,<Shift>Left,<Control>Left,<Alt>Left,' +
'<Shift><Control>Left,<Shift><Alt>Left'},
chooseIconRight: {Hint: _('Choose Icon Right'),
Accel: 'Right,<Shift>Right,<Control>Right,<Alt>Right,' +
'<Shift><Control>Right,<Shift><Alt>Right'},
chooseIconUp: {Hint: _('Choose Icon Up'),
Accel: 'Up,<Shift>Up,<Control>Up,<Alt>Up,' +
'<Shift><Control>Up,<Shift><Alt>Up'},
chooseIconDown: {Hint: _('Choose Icon Down'),
Accel: 'Down,<Shift>Down,<Control>Down,<Alt>Down,' +
'<Shift><Control>Down,<Shift><Alt>Down'},
menuKeyPressed: {Hint: _('Show Menu'), Accel: 'Menu,<Shift>F10'},
displayShellBackgroundMenu: {Hint: _('Display Shell Background Menu'), Accel: ''},
createDesktopShortcut: {Hint: _('Create Desktop Shortcut'), Accel: '', Edit: true},
textEntryAccelsTurnOn: {Hint: _('Text Entry Accels Turn On'), Accel: ''},
textEntryAccelsTurnOff: {Hint: _('Text Entry Accels Turn Off'), Accel: ''},
newDocument: {Hint: _('New Document'), Accel: ''},
showShortcutViewer: {Hint: _('Show Shortcut Viewer'), Accel: '', Edit: true},
toggleVisibility: {Hint: _('Show Or Hide Desktop Icons'), Accel: '', Global: true},
// FileItem Menu Actions
openMultipleFileAction: {Hint: 'Open All', Accel: '<Control>Return', Edit: true},
openOneFileAction: {Hint: 'Open Item', Accel: 'Return', Edit: true},
stackunstack: {Hint: 'Stack/Unstack', Accel: ''},
doopenwith: {Hint: 'Open With', Accel: '', Edit: true},
graphicslaunch: {Hint: 'Launch using Integrated Graphics Card', Accel: ''},
runasaprogram: {Hint: 'Run as a Program', Accel: ''},
docut: {Hint: 'Cut Item', Accel: '<Control>X'},
docopy: {Hint: 'Copy Item', Accel: '<Control>C'},
dorename: {Hint: 'Rename Item', Accel: 'F2', Edit: true},
movetotrash: {Hint: 'Move to Trash', Accel: 'Delete'},
deletepermanantly: {Hint: 'Delete Permanently', Accel: '<Shift>Delete'},
emptytrash: {Hint: 'Empty Trash', Accel: '', Edit: true},
allowdisallowlaunching: {Hint: 'Allow/Disallow Launching', Accel: '', Edit: true},
eject: {Hint: 'Eject', Accel: '', Edit: true},
unmount: {Hint: 'Unmount', Accel: '', Edit: true},
extractautoar: {Hint: 'Extract Here', Accel: ''},
extracthere: {Hint: 'Extract Here', Accel: ''},
extractto: {Hint: 'Extract To', Accel: ''},
sendto: {Hint: 'Email to', Accel: '', Edit: true},
compressfiles: {Hint: 'Compress Files', Accel: '', Edit: true},
newfolderfromselection: {Hint: 'New Folder from Selection', Accel: '', Edit: true},
properties: {Hint: 'Show Properties', Accel: '<Control>I', Edit: true},
showinfiles: {Hint: 'Show in Files', Accel: '', Edit: true},
openinterminal: {Hint: 'Open Terminal with Shell at this path', Accel: '', Edit: true},
openDesktopInTerminal: {Hint: 'Open Terminal at Desktop path', Accel: '', Edit: true},
makeLinks: {Hint: 'Create Link to Item', Accel: '<Shift><Control>M', Edit: true},
bulkCopy: {Hint: 'Copy to', Accel: '', Edit: true},
bulkMove: {Hint: 'Move to', Accel: '', Edit: true},
onScriptClicked: {Hint: 'Run Script', Accel: ''},
closeWidget: {Hint: 'Close Selected Widget', Accel: '<Shift><Control>X', Edit: true},
toggleWidgetGrid: {Hint: 'Toggle Widget Grid', Accel: '<Control><Shift>G', Edit: true},
};
// Following Global shortcuts will be added for editing and are editable
// However we need to add the key - the actioinName in lowercase to schemas
// for this to work, whithout the key added, it will not work.
// For example, for the one below, togglevisibility key added as {as} gvariant
// The program will automatically look for the lowercase key in schemas by
// converting the actionName.toLowerCase().
export const GlobalShortcuts = {
toggleVisibility: DefaultShortcuts.toggleVisibility,
};

108
ding/app/showErrorPopup.js Normal file
View File

@@ -0,0 +1,108 @@
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Copyright (C) 2022, 2025 Sundeep Mediratta (smedius@gmail.com) gtk4 port
* Copyright (C) 2019 Sergio Costas (rastersoft@gmail.com)
* Based on code original (C) Carlos Soriano
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {Adw, Gdk, Gio} from '../dependencies/gi.js';
import {_} from '../dependencies/gettext.js';
export {ShowErrorPopup};
const ShowErrorPopup = class {
constructor(text, secondaryText, waitDelayMs, helpURL = null) {
this._waitDelayMs = waitDelayMs; // async function
this._applicationId = Gio.Application.get_default();
this._window = this._applicationId.get_active_window();
this._dialog = new Adw.AlertDialog();
this._dialog.set_body_use_markup(true);
this._dialog.set_heading_use_markup(true);
if (text)
this._dialog.set_heading(text);
if (secondaryText)
this._dialog.set_body(secondaryText);
if (helpURL) {
this._helpURL = helpURL;
this._dialog.add_response('0', _('Cancel'));
this._dialog.add_response('1', _('More Information'));
this._dialog.set_close_response('0');
this._dialog.set_default_response('1');
this._dialog.set_response_appearance(
'1',
Adw.ResponseAppearance.SUGGESTED
);
this._dialog.set_response_appearance(
'0',
Adw.ResponseAppearance.DEFAULT
);
this._dialog.set_prefer_wide_layout(true);
} else {
this._dialog.add_response('0', _('Cancel'));
this._dialog.set_close_response('0');
this._dialog.set_default_response('0');
this._dialog.set_response_appearance(
'0',
Adw.ResponseAppearance.DEFAULT
);
}
this._dialog.connect('response', this._callback.bind(this));
}
show() {
this._dialog.present(this._window);
}
_callback(actor, response) {
if (response === '1' && this._helpURL)
this._launchUri(this._helpURL);
}
run() {
return new Promise(resolve => {
this._dialog.choose(this._window, null, (actor, asyncResult) => {
const response = actor.choose_finish(asyncResult);
resolve(response);
});
});
}
async runAutoClose(time) {
this.show();
await this._timeoutClose(time);
}
close() {
this._dialog.close();
}
async _timeoutClose(time) {
await this._waitDelayMs(time);
this._dialog.set_response_enabled('0', false);
this.close();
}
_launchUri(uri) {
const context = Gdk.Display.get_default().get_app_launch_context();
context.set_timestamp(Gdk.CURRENT_TIME);
Gio.AppInfo.launch_default_for_uri(uri, context);
}
};

View File

@@ -0,0 +1,174 @@
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Gtk4 Port Copyright (C) 2022 - 2025 Sundeep Mediratta (smedius@gmail.com)
* Based on code original (C) Carlos Soriano and Sergio Costas
* SwitcherooControl code based on code original from Marsch84
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {Gtk, Gdk, Gio} from '../dependencies/gi.js';
import {FileItemIcon} from '../dependencies/localFiles.js';
import {_} from '../dependencies/gettext.js';
export {SpecialFolderIcon};
const SpecialFolderIcon = class extends FileItemIcon {
constructor(desktopManager, file, fileInfo, fileTypeEnum, gioMount) {
super(desktopManager, file, fileInfo, fileTypeEnum, gioMount);
this._isTrash =
this._fileTypeEnum === this.Enums.FileType.USER_DIRECTORY_TRASH;
if (this.isTrash) {
// if this icon is the trash, monitor the state of the
// directory to update the icon
this._monitorTrash();
} else {
this._monitorTrashId = 0;
}
}
_destroy() {
super._destroy();
/* Trash */
if (this._monitorTrashId) {
this._monitorTrashDir.disconnect(this._monitorTrashId);
this._monitorTrashDir.cancel();
this._monitorTrashId = 0;
}
}
_setFileName(text) {
if (this._fileTypeEnum === this.Enums.FileType.USER_DIRECTORY_HOME) {
// TRANSLATORS: "Home" is the text that will be shown in
// the user's personal folder
text = _('Home');
}
super._setLabelName(text);
}
_setAccesibilityName() {
const trashName = _('Trash');
switch (this._fileTypeEnum) {
case this.Enums.FileType.USER_DIRECTORY_HOME:
this.container.update_property(
[Gtk.AccessibleProperty.LABEL],
[_('Home')]
);
break;
case this.Enums.FileType.USER_DIRECTORY_TRASH:
/** TRANSLATORS: when using a screen reader,this is the text read
* when the trash folder is selected. */
this.container.update_property(
[Gtk.AccessibleProperty.LABEL],
[`${trashName}`]
);
break;
}
}
_updateMetadataFromFileInfo(fileInfo) {
super._updateMetadataFromFileInfo(fileInfo);
this._isTrash =
this._fileTypeEnum === this.Enums.FileType.USER_DIRECTORY_TRASH;
}
_monitorTrash() {
this._monitorTrashDir =
this._file.monitor_directory(
Gio.FileMonitorFlags.WATCH_MOVES,
null
);
this._monitorTrashDir.set_rate_limit(1000);
this._monitorTrashId =
this._monitorTrashDir.connect(
'changed',
(_obj, _file, _otherFile, eventType) => {
this._refreshTrashIcon(eventType);
}
);
}
async _refreshTrashIcon(eventType) {
switch (eventType) {
case Gio.FileMonitorEvent.DELETED:
case Gio.FileMonitorEvent.MOVED_OUT:
case Gio.FileMonitorEvent.CREATED:
case Gio.FileMonitorEvent.MOVED_IN:
await this._reloadIcon().catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
console.error(
e,
`Exception while updating ${
this._getVisibleName()
? this._getVisibleName()
: 'Trash icon'
}: ${e.message}`);
}
});
break;
}
return false;
}
async _handleDroppedUris(
X, Y,
x, y,
fileList,
gdkDropAction,
localDrop,
event
) {
const forceCopy = gdkDropAction === Gdk.DragAction.COPY;
if (this._fileTypeEnum === this.Enums.FileType.USER_DIRECTORY_TRASH) {
if (localDrop) {
this._desktopManager
.fileItemActions
.doTrash(localDrop, event);
} else {
this.DBusUtils.RemoteFileOperations.pushEvent(event);
this.DBusUtils.RemoteFileOperations.TrashURIsRemote(fileList);
}
if (forceCopy)
return Gdk.DragAction.COPY;
else
return Gdk.DragAction.MOVE;
}
const returnaction = await super._handleDroppedUris(
X, Y,
x, y,
fileList,
gdkDropAction,
localDrop,
event
);
return returnaction;
}
get isTrash() {
return this._isTrash;
}
};

236
ding/app/stackItem.js Normal file
View File

@@ -0,0 +1,236 @@
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Copyright (C) Gtk4 port 2022, 2025 Sundeep Mediratta (smedius@gmail.com)
* Copyright (C) 2019 Sergio Costas (rastersoft@gmail.com)
* Based on code original (C) Carlos Soriano
* SwitcherooControl code based on code original from Marsch84
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {_} from '../dependencies/gettext.js';
import {Gdk, Gio, Graphene, Gtk, Gsk, GLib} from '../dependencies/gi.js';
import * as DesktopIconItem from './desktopIconItem.js';
export {StackItem};
const Signals = imports.signals;
const StackItem = class extends DesktopIconItem.DesktopIconItem {
constructor(desktopManager, file, attributeContentType, fileTypeEnum) {
super(desktopManager, fileTypeEnum);
this._isSpecial = false;
this._file = file;
this.isStackTop = true;
this.stackUnique = false;
this._size = null;
this._modifiedTime = null;
this._attributeContentType = attributeContentType;
this._createIconActor();
this._createStackTopIcon();
const stackName = this._file;
/** TRANSLATORS: when using a screen reader,
* this is the text read when a stack is
* selected. Example: if a stack named "pictures"
* is selected, it will say "Stack pictures" */
const accessibleName = _('Stack');
this._setLabelName(stackName);
this.container.update_property(
[Gtk.AccessibleProperty.LABEL],
[`${accessibleName} ${stackName}`]
);
this._savedCoordinates = null;
}
_createStackedAttributeContentTypeIcon() {
const stackIcon = Gtk.Snapshot.new();
/* A shadow for the pile of icons gives a sense of floating. */
const stackShadow = {
color: {red: 0, green: 0, blue: 0, alpha: 0.15},
dx: 2,
dy: 0,
radius: 1,
};
/* A slight shadow swhich makes each icon in the stack look separate. */
const iconShadow = {
color: {red: 0, green: 0, blue: 0, alpha: 0.30},
dx: 1,
dy: 0,
radius: 1,
};
const numberOfIcons = 5;
let yOffset = 0;
let xOffset = this.unStacked ? 8 : 4;
const icon = Gio.content_type_get_icon(this._attributeContentType);
const theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default());
const scale = this._icon.get_scale_factor();
let iconPaintable = null;
try {
iconPaintable = theme.lookup_by_gicon(
icon,
this.Prefs.IconSize,
scale, Gtk.TextDirection.NONE,
Gtk.IconLookupFlags.FORCE_SIZE
);
} catch (e) {
iconPaintable = theme.lookup_icon(
'image-missing',
[],
this.Prefs.IconSize,
scale,
Gtk.TextDirection.NONE,
Gtk.IconLookupFlags.FORCE_SIZE
);
}
const stackIconArray = Array(numberOfIcons).fill(iconPaintable);
const w = iconPaintable.get_intrinsic_width();
const h = iconPaintable.get_intrinsic_height();
let X = xOffset * numberOfIcons;
let Y = yOffset;
stackIcon.translate(new Graphene.Point({x: X, y: Y}));
stackIcon.push_shadow([new Gsk.Shadow(stackShadow)]);
stackIconArray.forEach(paintableWidget => {
// Position each widget from right to left
X = -xOffset;
stackIcon.translate(new Graphene.Point({x: X, y: Y}));
stackIcon.push_shadow([new Gsk.Shadow(iconShadow)]);
// Render the paintable widget
paintableWidget.snapshot(stackIcon, w, h);
stackIcon.pop(); // Remove shadow effect for the next widget
});
// Remove the initial transformation & shadow
stackIcon.pop();
return stackIcon.to_paintable(null);
}
_createStackTopIcon() {
const stackIcon = this._createStackedAttributeContentTypeIcon();
const iconPaintable = this._addEmblemsToIconIfNeeded(stackIcon);
this._icon.set_paintable(iconPaintable);
}
// eslint-disable-next-line no-unused-vars
_doButtonOnePressed(button, X, Y, x, y, shiftPressed, controlPressed) {
const variant = GLib.Variant.new('s', this.attributeContentType);
this._desktopManager.mainApp.activate_action(
'stackunstack',
variant
);
}
setSelected() {
this.container.grab_focus();
}
unsetSelected() {
this.keyboardUnSelected();
}
updateIcon() {
this._createStackTopIcon();
}
_addEmblemsToIconIfNeeded(iconPaintable) {
let emblem = null;
if (this.isStackTop && !this.stackUnique)
emblem = Gio.ThemedIcon.new('icon-emblem-stack');
return this._addEmblem(iconPaintable, emblem);
}
/** *********************
* Getters and setters *
***********************/
get attributeContentType() {
return this._attributeContentType;
}
get displayName() {
return this._file;
}
get file() {
return this._file;
}
get fileName() {
return this._file;
}
get fileSize() {
return this._size;
}
get isAllSelectable() {
return false;
}
get modifiedTime() {
return this._modifiedTime;
}
get path() {
return `/tmp/${this._file}`;
}
get uri() {
return `file:///tmp/${this._file}`;
}
get isStackMarker() {
return true;
}
get savedCoordinates() {
return this._savedCoordinates;
}
get unStacked() {
return this.Prefs.UnstackList.includes(this._attributeContentType);
}
get x() {
return this._x1;
}
get y() {
return this._y1;
}
get X() {
return this._savedCoordinates[0];
}
get Y() {
return this._savedCoordinates[1];
}
set size(size) {
this._size = size;
}
set time(time) {
this._modifiedTime = time;
}
set savedCoordinates(pos) {
}
};
Signals.addSignalMethods(StackItem.prototype);

178
ding/app/symLinkIcon.js Normal file
View File

@@ -0,0 +1,178 @@
/*
* Adw-DING Copyright (C) 2022, 2025 Sundeep Mediratta (smedius@gmail.com)
* Based on code original (C) Carlos Soriano and (c) Sergio Costas
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {Gio} from '../dependencies/gi.js';
import {_} from '../dependencies/gettext.js';
export {SymLinkIcon};
const SymLinkIcon = class {
constructor(
Basetype,
ddesktopManager,
ffile,
ffileInfo,
ffileTypeEnum,
ggioMount
) {
const SymLinkSuperClass = class extends Basetype {
constructor(
desktopManager,
file,
fileInfo,
fileTypeEnum,
gioMount
) {
super(desktopManager, file, fileInfo, fileTypeEnum, gioMount);
this._isSymlink = fileInfo.get_attribute_boolean(
Gio.FILE_ATTRIBUTE_STANDARD_IS_SYMLINK
);
/*
* This is a glib trick to detect broken symlinks. If a file is a
* symlink, the filetype points to the final file, unless it is broken;
* thus if the file type is SYMBOLIC_LINK, it must be a broken link.
* https://developer.gnome.org/gio/stable/GFile.html#g-file-query-info
*/
this._isBrokenSymlink =
this._isSymlink &&
this._fileType === Gio.FileType.SYMBOLIC_LINK;
if (this._isSymlink && !this._symlinkFileMonitor)
this._monitorSymlink();
}
_updateMetadataFromFileInfo(fileInfo) {
this._isSymlink = fileInfo.get_attribute_boolean(
Gio.FILE_ATTRIBUTE_STANDARD_IS_SYMLINK
);
/*
* This is a glib trick to detect broken symlinks. If a file is a
* symlink, the filetype points to the final file, unless it is broken;
* thus if the file type is SYMBOLIC_LINK, it must be a broken link.
* https://developer.gnome.org/gio/stable/GFile.html#g-file-query-info
*/
this._isBrokenSymlink =
this._isSymlink &&
this._fileType === Gio.FileType.SYMBOLIC_LINK;
super._updateMetadataFromFileInfo(fileInfo);
}
_destroy() {
super._destroy();
if (this._symlinkFileMonitorId) {
this._symlinkFileMonitor.disconnect(this._symlinkFileMonitorId);
this._symlinkFileMonitor.cancel();
this._symlinkFileMonitorId = 0;
}
}
async _doOpenContext(context, fileList) {
if (!fileList)
fileList = [];
if (this._isBrokenSymlink) {
try {
console.log(
`Error: Cant open ${this.file.get_uri()}` +
' because it is a broken symlink.'
);
const title = _('Broken Link');
const error =
_('Can not open this File because it is a Broken Symlink');
this._showerrorpopup(title, error);
} catch (e) {}
return;
}
await super._doOpenContext(context, fileList);
}
_monitorSymlink() {
let symlinkTarget = this._fileInfo.get_symlink_target();
let symlinkTargetGioFile = Gio.File.new_for_path(symlinkTarget);
this._symlinkFileMonitor = symlinkTargetGioFile.monitor(
Gio.FileMonitorFlags.WATCH_MOVES,
null
);
this._symlinkFileMonitor.set_rate_limit(1000);
this._symlinkFileMonitorId = this._symlinkFileMonitor.connect(
'changed',
this._updateSymlinkIcon.bind(this)
);
}
async _updateSymlinkIcon() {
await this._reloadIcon().catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
console.error(
e,
`Exception while updating ${
this._getVisibleName()
? this._getVisibleName()
: 'symlink icon'
}: ${e.message}`);
}
});
}
_addEmblemsToIconIfNeeded(iconPaintable, position = 0) {
let emblem = null;
let newIconPaintable = iconPaintable;
if (this._isSymlink && this.Prefs.showLinkEmblem) {
emblem = Gio.ThemedIcon.new('icon-emblem-symbolic-link');
newIconPaintable =
this._addEmblem(newIconPaintable, emblem, position);
position += 1;
}
if (this._isBrokenSymlink) {
emblem = Gio.ThemedIcon.new('icon-emblem-unreadable');
newIconPaintable =
this._addEmblem(newIconPaintable, emblem, position);
position += 1;
}
return super._addEmblemsToIconIfNeeded(newIconPaintable, position);
}
};
return new SymLinkSuperClass(
ddesktopManager,
ffile,
ffileInfo,
ffileTypeEnum,
ggioMount
);
}
};

View File

@@ -0,0 +1,308 @@
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Gtk4 Port Copyright (C) 2022, 2025 Sundeep Mediratta (smedius@gmail.com)
* Copyright (C) 2020 Sergio Costas (rastersoft@gmail.com)
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {Gio, GLib} from '../dependencies/gi.js';
export {TemplatesScriptsManager};
const MAX_DIRS = 100;
const MAX_MENUENTRIES = 50;
const MAX_MENU_DEPTH = 10;
const TemplatesScriptsManager = class {
constructor(baseFolder, selectionfilter, Data) {
this._selectionFilter = selectionfilter;
this._actionName = Data.appName;
this.FileUtils = Data.FileUtils;
this.Enums = Data.Enums;
this._entries = [];
this._entriesEnumerateCancellable = null;
this._entriesDir = baseFolder;
this._entriesDirMonitors = [];
this.gioMenu = null;
if (this._entriesDir === GLib.get_home_dir())
this._entriesDir = null;
if (this._entriesDir !== null) {
this._monitorDir =
baseFolder.monitor_directory(
Gio.FileMonitorFlags.WATCH_MOVES,
null
);
this._monitorDir.set_rate_limit(1000);
this._monitorDir.connect(
'changed',
() => {
this.updateEntries()
.catch(
e => {
console.log(
'Exception while updating entries in ' +
`monitor: ${e.message}\n${e.stack}`
);
}
);
}
);
this.updateEntries()
.catch(e => {
console.log(
'Exception while updating entries: ' +
`${e.message}\n${e.stack}`
);
});
}
}
async updateEntries() {
if (this._entriesEnumerateCancellable)
this._entriesEnumerateCancellable.cancel();
const cancellable = new Gio.Cancellable();
this._entriesEnumerateCancellable = cancellable;
this._entriesDirMonitors.forEach(f => {
f[0].disconnect(f[1]);
f[0].cancel();
});
this._entriesDirMonitors = [];
this._menuEntries = new Set();
let entriesList;
try {
entriesList = await this._processDirectory(this._entriesDir,
cancellable);
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED) &&
!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND))
console.error(e);
} finally {
if (this._entriesEnumerateCancellable === cancellable)
this._entriesEnumerateCancellable = null;
}
[this._entries, this.gioMenu] =
entriesList !== null
? entriesList
: [null, null];
}
async _processDirectory(directory, cancellable, recursionLevel = 0) {
const localRecursionLevel = recursionLevel += 1;
var files = null;
try {
files = await this._readDirectory(directory, cancellable);
} catch (e) {
console.error(e);
return null;
}
if (files === null)
return null;
let outputEntries = [];
let menu = new Gio.Menu();
let menuhasentries = false;
for (let file of files) {
let menuItemName = file[0];
if (file[2] === null) {
outputEntries.push(file);
let menuItemPath = file[1];
if (this._menuEntries.has(menuItemPath))
continue;
this._menuEntries.add(menuItemPath);
let menuItem = Gio.MenuItem.new(`${menuItemName}`, null);
menuItem.set_action_and_target_value(
this._actionName,
GLib.Variant.new('s', `${menuItemPath}`)
);
menu.append_item(menuItem);
menuhasentries = true;
continue;
}
if (this._entriesDirMonitors.length > MAX_DIRS) {
console.log(
'Limiting the number of folders monitored in ' +
'templates/scripts...'
);
continue;
}
if (localRecursionLevel > MAX_MENU_DEPTH) {
console.log(
'Limiting submenu depth of folders monitored' +
' in templates/scripts...'
);
continue;
}
let dirpath = file[1].get_path();
const newFileInfo =
// eslint-disable-next-line no-await-in-loop
await file[1].query_info_async(
this.Enums.DEFAULT_ATTRIBUTES,
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_DEFAULT,
cancellable
);
if (newFileInfo
.get_attribute_boolean(
Gio.FILE_ATTRIBUTE_STANDARD_IS_SYMLINK
)
)
dirpath = newFileInfo.get_symlink_target();
if (this._menuEntries.has(dirpath))
continue;
this._menuEntries.add(dirpath);
let monitorDir =
file[1].monitor_directory(
Gio.FileMonitorFlags.WATCH_MOVES,
null
);
monitorDir.set_rate_limit(1000);
let monitorId =
monitorDir.connect(
'changed',
() => {
this.updateEntries();
}
);
this._entriesDirMonitors.push([monitorDir, monitorId]);
let submenu;
let subentriesList;
subentriesList =
// eslint-disable-next-line no-await-in-loop
await this._processDirectory(
file[1],
cancellable,
localRecursionLevel
);
if (subentriesList === null)
return null;
[file[2], submenu] = subentriesList;
if (file[2].length !== 0)
outputEntries.push(file);
if (submenu) {
const menuItem =
Gio.MenuItem.new_submenu(`${menuItemName}`, submenu);
menu.append_item(menuItem);
menuhasentries = true;
}
}
if (!menuhasentries)
menu = null;
return [outputEntries, menu];
}
async _readDirectory(directory, cancellable) {
const childrenInfo =
await this.FileUtils.enumerateDir(
directory,
cancellable,
GLib.PRIORITY_DEFAULT,
this.Enums.DEFAULT_ATTRIBUTES
);
const fileList = [];
childrenInfo.forEach(info => {
const menuitemName = this._selectionFilter(info);
if (!menuitemName)
return;
if (fileList.length > MAX_MENUENTRIES) {
console.log('Truncating menu entries templates/scripts submenu');
return;
}
const isDir = info.get_file_type() === Gio.FileType.DIRECTORY;
const isSymlink =
info
.get_attribute_boolean(Gio.FILE_ATTRIBUTE_STANDARD_IS_SYMLINK);
if (isDir && isSymlink) {
console.warn(
'Folder Symlink in monitored templates/scripts folder...\n',
'This can lead to unlimited recursion.'
);
}
const child = directory.get_child(info.get_name());
fileList.push([
menuitemName,
isDir ? child : child.get_path(),
isDir ? [] : null,
]);
});
fileList.sort(
(a, b) => {
return a[0]
.localeCompare(
b[0],
{
sensitivity: 'accent',
numeric: 'true',
localeMatcher: 'lookup',
}
);
}
);
return fileList;
}
getGioMenu() {
return this.gioMenu;
}
};

494
ding/app/thumbnails.js Normal file
View File

@@ -0,0 +1,494 @@
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Copyright (C) 2022, 2024 Sundeep Mediratta (smedius@gmail.com) port to work with
* gnome desktop 4
*
* Code cherry picked from Marco Trevisan for async methods to generate icons.
*
* Copyright (C) 2021 Sergio Costas (rastersoft@gmail.com)
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {Cairo, Gdk, GdkPixbuf, GLib, Gio, GnomeDesktop, Poppler} from
'../dependencies/gi.js';
export {ThumbnailLoader};
Gio._promisify(GnomeDesktop.DesktopThumbnailFactory.prototype,
'generate_thumbnail_async',
'generate_thumbnail_finish');
Gio._promisify(GnomeDesktop.DesktopThumbnailFactory.prototype,
'create_failed_thumbnail_async',
'create_failed_thumbnail_finish');
Gio._promisify(GnomeDesktop.DesktopThumbnailFactory.prototype,
'save_thumbnail_async',
'save_thumbnail_finish');
const PIXBUF_CONTENT_TYPES = new Set();
GdkPixbuf.Pixbuf
.get_formats().forEach(f => PIXBUF_CONTENT_TYPES.add(...f.get_mime_types()));
// Max file size for which to attempt thumbnail generation with local code
const MAX_FILE_SIZE = 5242880;
// Width and height of icons generated by local code
const WIDTH = 130;
const HEIGHT = 130;
const ThumbnailLoader = class {
constructor(FileUtils) {
this.FileUtils = FileUtils;
this._timeoutValue = 5000;
this._thumbnailFactory =
GnomeDesktop.DesktopThumbnailFactory
.new(GnomeDesktop.DesktopThumbnailSize.LARGE);
this.standardThumbnailsFolder =
GLib.build_filenamev([GLib.get_home_dir(), '.cache/thumbnails']);
this.standardThumbnailSubFolders = ['large', 'normal'];
this.gimpSnapThumbnailsFolder =
GLib.build_filenamev(
[
GLib.get_home_dir(),
'snap/common/gimp',
'.cache/thumbnails',
]
);
this.gimpFlatPackThumbnailsFolder =
GLib.build_filenamev(
[
GLib.get_home_dir(),
'.var/app/org.gimp.GIMP',
'cache/thumbnails',
]
);
this.md5Hasher = GLib.Checksum.new(GLib.ChecksumType.MD5);
this.textCoder = new TextEncoder();
}
async _generateThumbnail(file, cancellable) {
if (!await this.FileUtils.queryExists(file.file))
return null;
if (this._thumbnailFactory
.has_valid_failed_thumbnail(file.uri, file.modifiedTime)
)
return null;
if (!await this._createThumbnailAsync(file, cancellable)) {
await this._createFailedThumbnailAsync(file, cancellable);
return null;
}
if (cancellable.is_cancelled())
return null;
return this._thumbnailFactory.lookup(file.uri, file.modifiedTime);
}
async _createThumbnailAsync(file, cancellable) {
let gotTimeout = false;
let timeoutId =
GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._timeoutValue, () => {
console.log(
`Timeout while generating thumbnail for ${file.displayName}`
);
timeoutId = 0;
gotTimeout = true;
cancellable.cancel();
return GLib.SOURCE_REMOVE;
});
try {
const thumbnailPixbuf =
await this._thumbnailFactory
.generate_thumbnail_async(
file.uri,
file.attributeContentType,
cancellable
);
await this._thumbnailFactory
.save_thumbnail_async(
thumbnailPixbuf,
file.uri,
file.modifiedTime,
cancellable
)
.catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
console.error(`Error saving thumbnail ${e}`);
});
return true;
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
console.error(e,
'Error creating thumbnail with thumbnailFactory: ' +
`${e.message}`
);
}
return await this._createFallBackThumbnailAsync(
file,
gotTimeout && cancellable.is_cancelled() ? null : cancellable
);
} finally {
if (timeoutId)
GLib.source_remove(timeoutId);
}
}
async _createFallBackThumbnailAsync(file, cancellable) {
const thumbnailPixbuf = this._createThumbnailLocally(file, cancellable);
if (thumbnailPixbuf !== null) {
await this._thumbnailFactory
.save_thumbnail_async(
thumbnailPixbuf,
file.uri,
file.modifiedTime,
cancellable
)
.catch(e => {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
console.error(`Error saving thumbnail ${e}`);
});
return true;
}
return false;
}
async _createFailedThumbnailAsync(file, cancellable) {
try {
await this._thumbnailFactory.create_failed_thumbnail_async(
file.uri,
file.modifiedTime,
cancellable
);
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
console.error(e,
`Error while creating failed thumbnail: ${e.message}`
);
}
}
}
_createThumbnailLocally(file, cancellable) {
let thumbnailPixbuf = null;
if (file.fileSize < MAX_FILE_SIZE) {
const contentType = file.attributeContentType;
try {
if (PIXBUF_CONTENT_TYPES.has(contentType))
thumbnailPixbuf = this._loadImageAsIcon(file, cancellable);
else if (
contentType === 'application/pdf' ||
contentType === 'x-pdf'
)
thumbnailPixbuf = this._loadPdfAsIcon(file, cancellable);
} catch (e) {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
throw e;
console.error(e,
`Error while generating icon image: ${e.message}`
);
}
}
return thumbnailPixbuf;
}
_loadPdfAsIcon(file, cancellable) {
let thumbnailPixbuf = null;
try {
// Assume no password
const password = null;
const popplerDocument =
Poppler.Document.new_from_gfile(
file.file,
password,
cancellable
);
if (!popplerDocument)
return thumbnailPixbuf;
const firstPage = popplerDocument.get_page(0);
if (!firstPage)
return thumbnailPixbuf;
const [pagewidth, pageheight] = firstPage.get_size();
let width = WIDTH;
let height = HEIGHT;
const aspectRatio = pagewidth / pageheight;
if ((width / height) > aspectRatio)
width = height * aspectRatio;
else
height = width / aspectRatio;
const hScale = width / pagewidth;
const vScale = height / pageheight;
const imageSurface =
new Cairo.ImageSurface(
Cairo.Format.ARGB32,
pagewidth,
pageheight
);
const ctx = new Cairo.Context(imageSurface);
this._drawPdfOn(ctx, firstPage);
const scaledSurface =
new Cairo.ImageSurface(Cairo.Format.ARGB32, width, height);
const scaledCtx = new Cairo.Context(scaledSurface);
scaledCtx.scale(hScale, vScale);
scaledCtx.setSourceSurface(imageSurface, 0, 0);
scaledCtx.paint();
thumbnailPixbuf =
Gdk.pixbuf_get_from_surface(scaledSurface, 0, 0, width, height);
ctx.$dispose();
scaledCtx.$dispose();
} catch (e) {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
throw e;
if (!e.matches(Poppler.Error, Poppler.Error.ENCRYPTED)) {
console.error(e,
`Error creating pdf thumbnail pixbuf ${file.uri}`
);
}
}
return thumbnailPixbuf;
}
_drawPdfOn(ctx, firstPage) {
ctx.setSourceRGBA(1, 1, 1, 1);
ctx.save();
ctx.paint();
ctx.restore();
firstPage.render(ctx);
ctx.save();
}
_loadImageAsIcon(file) {
let thumbnailPixbuf = null;
try {
const pixbuf = GdkPixbuf.Pixbuf.new_from_file(file.path);
let width = WIDTH;
let height = HEIGHT;
const aspectRatio = pixbuf.width / pixbuf.height;
if ((width / height) > aspectRatio)
width = height * aspectRatio;
else
height = width / aspectRatio;
thumbnailPixbuf =
pixbuf.scale_simple(
width,
height,
GdkPixbuf.InterpType.BILINEAR
);
return thumbnailPixbuf;
} catch (e) {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
throw e;
console.error(e,
`Error creating image thumbnail pixbuf ${file.uri}`
);
}
return thumbnailPixbuf;
}
/*
* ExtraCode to find thumbnail in the thumbnail Folder
* Was Used to find GIMP thumbnails, however ThumbnailFactoryNormal
* can now find it.
*
* However to do that you have to start two ThumbnailFactories,
* this is simpler and lighter, can be used to search arbitrary folders
* for thumbnails in Futre if necessary, not just Subfolders
*/
_findThumbnail(file, basePath, subFolders = null, cancellable) {
if (!basePath)
return null;
let md5FileUriHash = this._getMD5Hash(file.uri);
if (!md5FileUriHash)
return null;
let thumbnailMD5Name = `${md5FileUriHash}.png`;
let thumbnailFilePath = null;
let thumbnailFileSearchPath = null;
if (subFolders) {
for (const subfolder of subFolders) {
thumbnailFileSearchPath =
GLib
.build_filenamev([basePath, subfolder, thumbnailMD5Name]);
if (
Gio.File.new_for_path(thumbnailFileSearchPath)
.query_exists(cancellable)
) {
thumbnailFilePath = thumbnailFileSearchPath;
break;
}
}
return thumbnailFilePath;
}
thumbnailFileSearchPath =
GLib.build_filenamev([basePath, thumbnailMD5Name]);
if (
Gio.File.new_for_path(thumbnailFileSearchPath)
.query_exists(cancellable)
)
thumbnailFilePath = thumbnailFileSearchPath;
return thumbnailFilePath;
}
_getMD5Hash(string) {
let hashString = null;
this.md5Hasher.update(this.textCoder.encode(string));
hashString = this.md5Hasher.get_string();
this.md5Hasher.reset();
return hashString;
}
canThumbnail(file) {
return this._thumbnailFactory
.can_thumbnail(
file.uri,
file.attributeContentType,
file.modifiedTime
);
}
_lookupThumbnail(file, cancellable) {
let thumbnail = null;
// do searches for only special cases to conserve resources //
if (file.attributeContentType === 'image/x-xcf') {
// lets do a local search in thumbnails dir, look only in normal
// subfolder as we already searched large
thumbnail =
this._findThumbnail(
file,
this.standardThumbnailsFolder,
['normal'],
cancellable
);
if (thumbnail)
return thumbnail;
// we can now search far and wide in snaps and flatpacks if we want.
thumbnail =
this._findThumbnail(
file,
this.gimpSnapThumbnailsFolder,
this.standardThumbnailSubFolders,
cancellable
);
if (!thumbnail) {
thumbnail =
this._findThumbnail(
file,
this.gimpFlatPackThumbnailsFolder,
this.standardThumbnailSubFolders,
cancellable
);
}
return thumbnail;
}
return thumbnail;
}
hasThumbnail(file, cancellable) {
let thumbnail = null;
thumbnail = this._thumbnailFactory.lookup(file.uri, file.modifiedTime);
if (thumbnail)
return thumbnail;
thumbnail = this._lookupThumbnail(file, cancellable);
return thumbnail;
}
async getThumbnail(file, cancellable) {
try {
let thumbnail = this.hasThumbnail(file, cancellable);
if (!thumbnail && this.canThumbnail(file))
thumbnail = await this._generateThumbnail(file, cancellable);
if (
!thumbnail &&
await this._createFallBackThumbnailAsync(file, cancellable)
) {
thumbnail =
this._thumbnailFactory.lookup(file.uri, file.modifiedTime);
}
return thumbnail;
} catch (error) {
console.log(
`Error when asking for a thumbnail for ${file.displayName}:` +
` ${error.message}\n${error.stack}`);
}
return null;
}
};

View File

@@ -0,0 +1,243 @@
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Copyright (C) 2022 Sergio Costas (rastersoft@gmail.com)
*
* 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, version 3 of the License.
*
* 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/>.
*/
export {DBusInterfaces};
const DBusInterfaces = {
// net.haddes.SwitcherooControl
'net.hadess.SwitcherooControl': `<node>
<interface name="net.hadess.SwitcherooControl">
<property name="HasDualGpu" type="b" access="read"/>
<property name="NumGPUs" type="u" access="read"/>
<property name="GPUs" type="aa{sv}" access="read"/>
</interface>
</node>`,
// org.freedesktop.FileManager1
'org.freedesktop.FileManager1': `<node>
<interface name='org.freedesktop.FileManager1'>
<method name='ShowItems'>
<arg name='URIs' type='as' direction='in'/>
<arg name='StartupId' type='s' direction='in'/>
</method>
<method name='ShowItemProperties'>
<arg name='URIs' type='as' direction='in'/>
<arg name='StartupId' type='s' direction='in'/>
</method>
</interface>
</node>`,
// org.gnome.ArchiveManager1
'org.gnome.ArchiveManager1': `<node>
<interface name="org.gnome.ArchiveManager1">
<method name="GetSupportedTypes">
<arg name="action" type="s" direction="in"/>
<arg name="types" type="aa{ss}" direction="out"/>
</method>
<method name="AddToArchive">
<arg name="archive" type="s" direction="in"/>
<arg name="files" type="as" direction="in"/>
<arg name="use_progress_dialog" type="b" direction="in"/>
</method>
<method name='Compress'>
<arg name="files" type="as" direction="in"/>
<arg name="destination" type="s" direction="in"/>
<arg name="use_progress_dialog" type="b" direction="in"/>
</method>
<method name="Extract">
<arg name="archive" type="s" direction="in"/>
<arg name="destination" type="s" direction="in"/>
<arg name="use_progress_dialog" type="b" direction="in"/>
</method>
<method name="ExtractHere">
<arg name="archive" type="s" direction="in"/>
<arg name="use_progress_dialog" type="b" direction="in"/>
</method>
<signal name="Progress">
<arg name="fraction" type="d"/>
<arg name="details" type="s"/>
</signal>
</interface>
</node>`,
// org.gnome.Nautilus.FileOperations2
'org.gnome.Nautilus.FileOperations2': `<node>
<interface name='org.gnome.Nautilus.FileOperations2'>
<method name='CopyURIs'>
<arg type='as' name='sources' direction='in'/>
<arg type='s' name='destination' direction='in'/>
<arg type='a{sv}' name='platform_data' direction='in'/>
</method>
<method name='MoveURIs'>
<arg type='as' name='sources' direction='in'/>
<arg type='s' name='destination' direction='in'/>
<arg type='a{sv}' name='platform_data' direction='in'/>
</method>
<method name='EmptyTrash'>
<arg type="b" name="ask_confirmation" direction='in'/>
<arg type='a{sv}' name='platform_data' direction='in'/>
</method>
<method name='TrashURIs'>
<arg type='as' name='uris' direction='in'/>
<arg type='a{sv}' name='platform_data' direction='in'/>
</method>
<method name='DeleteURIs'>
<arg type='as' name='uris' direction='in'/>
<arg type='a{sv}' name='platform_data' direction='in'/>
</method>
<method name='CreateFolder'>
<arg type='s' name='parent_uri' direction='in'/>
<arg type='s' name='new_folder_name' direction='in'/>
<arg type='a{sv}' name='platform_data' direction='in'/>
</method>
<method name='RenameURI'>
<arg type='s' name='uri' direction='in'/>
<arg type='s' name='new_name' direction='in'/>
<arg type='a{sv}' name='platform_data' direction='in'/>
</method>
<method name='Undo'>
<arg type='a{sv}' name='platform_data' direction='in'/>
</method>
<method name='Redo'>
<arg type='a{sv}' name='platform_data' direction='in'/>
</method>
<property name="UndoStatus" type="i" access="read"/>
</interface>
</node>`,
// org.gnome.NautilusPreviewer
'org.gnome.NautilusPreviewer': `<node>
<interface name='org.gnome.NautilusPreviewer'>
<method name='ShowFile'>
<arg name='FileUri' type='s' direction='in'/>
<arg name='ParentXid' type='i' direction='in'/>
<arg name='CloseIfShown' type='b' direction='in'/>
</method>
</interface>
</node>`,
// org.gtk.vfs.Metadata
'org.gtk.vfs.Metadata': `<node>
<interface name='org.gtk.vfs.Metadata'>
<method name="Set">
<arg type='ay' name='treefile' direction='in'/>
<arg type='ay' name='path' direction='in'/>
<arg type='a{sv}' name='data' direction='in'/>
</method>
<method name="Remove">
<arg type='ay' name='treefile' direction='in'/>
<arg type='ay' name='path' direction='in'/>
</method>
<method name="Move">
<arg type='ay' name='treefile' direction='in'/>
<arg type='ay' name='path' direction='in'/>
<arg type='ay' name='dest_path' direction='in'/>
</method>
<method name="GetTreeFromDevice">
<arg type='u' name='major' direction='in'/>
<arg type='u' name='minor' direction='in'/>
<arg type='s' name='tree' direction='out'/>
</method>
<signal name="AttributeChanged">
<arg type='s' name='tree_path'/>
<arg type='s' name='file_path'/>
</signal>
</interface>
</node>`,
// org.freedesktop.DBus.Introspectable
'org.freedesktop.DBus.Introspectable': `<node>
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg direction="out" type="s"/>
</method>
</interface>
</node>`,
// org.freedesktop.Notifications
'org.freedesktop.Notifications': `<node>
<interface name="org.freedesktop.Notifications">
<method name="Notify">
<arg type="s" name="arg_0" direction="in">
</arg>
<arg type="u" name="arg_1" direction="in">
</arg>
<arg type="s" name="arg_2" direction="in">
</arg>
<arg type="s" name="arg_3" direction="in">
</arg>
<arg type="s" name="arg_4" direction="in">
</arg>
<arg type="as" name="arg_5" direction="in">
</arg>
<arg type="a{sv}" name="arg_6" direction="in">
</arg>
<arg type="i" name="arg_7" direction="in">
</arg>
<arg type="u" name="arg_8" direction="out">
</arg>
</method>
<method name="CloseNotification">
<arg type="u" name="arg_0" direction="in">
</arg>
</method>
<method name="GetCapabilities">
<arg type="as" name="arg_0" direction="out">
</arg>
</method>
<method name="GetServerInformation">
<arg type="s" name="arg_0" direction="out">
</arg>
<arg type="s" name="arg_1" direction="out">
</arg>
<arg type="s" name="arg_2" direction="out">
</arg>
<arg type="s" name="arg_3" direction="out">
</arg>
</method>
<signal name="NotificationClosed">
<arg type="u" name="arg_0">
</arg>
<arg type="u" name="arg_1">
</arg>
</signal>
<signal name="ActionInvoked">
<arg type="u" name="arg_0">
</arg>
<arg type="s" name="arg_1">
</arg>
</signal>
</interface>
</node>`,
// com.desktop.dingextension/service
'com.desktop.dingextension.service': `<node>
<interface name="com.desktop.dingextension.service">
<method name="updateDesktopGeometry"/>
<method name="getDropTargetAppInfoDesktopFile">
<arg type="ad" direction="in" name="Global Drop Coordinates"/>
<arg type="s" direction="out" name=".desktop Application File Path or 'null'"/>
</method>
<method name="getShellGlobalCoordinates">
<arg type="ai" direction="out" name="Global pointer Coordinates"/>
</method>
<method name="setDragCursor">
<arg type="s" direction="in" name="Set Shell Cursor"/>
</method>
<method name="showShellBackgroundMenu"/>
</interface>
</node>`,
};

1638
ding/app/utils/dbusUtils.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,350 @@
/* ADW-DING: Desktop Icons New Generation for GNOME Shell
*
* Adw/Gtk4 Port Copyright (C) 2025 Sundeep Mediratta (smedius@gmail.com)
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {gettext, Gio, GLib, Gtk} from '../../dependencies/gi.js';
import {_} from '../../dependencies/gettext.js';
import {Enums} from '../../dependencies/localFiles.js';
export {DesktopFolderUtils};
const DesktopFolderUtils = class {
constructor(window) {
this._activeWindow = window;
this.Enums = Enums;
this._desktopDir = this.getDesktopDir();
// Bind xdg-user-dirs translation domain
gettext.bindtextdomain('xdg-user-dirs', '/usr/share/locale');
}
_monitorDesktopDirChanges() {
this._xdgUserDirs = this._getXdgUserDirs();
this._monitorXdgUserDirs = this._xdgUserDirs.monitor_file(
Gio.FileMonitorFlags.WATCH_MOVES, null);
this._monitorXdgUserDirs.set_rate_limit(2000);
this._connectMonitor();
}
_connectMonitor() {
this._monitorID = this._monitorXdgUserDirs.connect(
'changed',
(obj, file, otherFile, event) => {
if (!(event === Gio.FileMonitorEvent.CHANGES_DONE_HINT ||
event === Gio.FileMonitorEvent.RENAMED))
return;
if (this._changingDesktopDirID)
GLib.source_remove(this._changingDesktopDirID);
this._changingDesktopDirID =
GLib.timeout_add(GLib.PRIORITY_LOW, 500, () => {
const newDesktopDir =
this.getDesktopDir();
this.onDesktopFolderChanged(newDesktopDir);
this._changingDesktopDirID = null;
return GLib.SOURCE_REMOVE;
});
}
);
}
_disconnectMonitor() {
if (this._monitorID)
this._monitorXdgUserDirs.disconnect(this._monitorID);
this._monitorID = 0;
}
_stopMonitoring() {
this._disconnectMonitor();
this._monitorXdgUserDirs?.cancel();
}
onDesktopFolderChanged(newDesktopDir) {
this._desktopDir = newDesktopDir;
}
changeDesktop() {
const dialog = new Gtk.FileDialog();
dialog.set_title(_('Choose Desktop Folder'));
dialog.set_accept_label(_('Choose'));
dialog.set_modal(true);
dialog.set_initial_folder(
Gio.File.new_for_commandline_arg(GLib.get_home_dir())
);
dialog.select_folder(
this.activeWindow,
null,
this._finishChooseDesktopFolder.bind(this)
);
}
restoreDefaultDesktop() {
const localizedDesktopName = this.getSystemLocalizedDesktopDir();
const defaultDesktop = GLib.build_filenamev([GLib.get_home_dir(),
localizedDesktopName]);
const desktopFolder = Gio.File.new_for_path(defaultDesktop);
try {
if (!desktopFolder.query_exists(null))
GLib.mkdir_with_parents(desktopFolder.get_path(), 0o755);
} catch (e) {
console.error(
`Unable to create Folder ${defaultDesktop}: ${e.message}`
);
}
this._setDesktopFolder(desktopFolder);
}
getDesktopDir() {
const glibDesktopPath =
GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP);
let xdgDesktopPath = null;
try {
const userDirsGioFile = this._getXdgUserDirs();
if (!userDirsGioFile.query_exists(null)) {
throw new Error(
'User configuration file user-dirs.users does not exist'
);
}
const decoder = new TextDecoder();
const contents =
decoder
.decode(GLib.file_get_contents(userDirsGioFile.get_path())[1])
.trim();
if (contents)
xdgDesktopPath = this._parseUserDirsFile(contents);
} catch (e) {
console.error(e, `XDG Desktop not set, ${e}`);
}
const desktopPath = xdgDesktopPath ? xdgDesktopPath : glibDesktopPath;
return Gio.File.new_for_commandline_arg(desktopPath);
}
getSystemLocalizedDesktopDir() {
const systemDesktopDirName =
this._getSystemDesktopDir() ?? this.Enums.DEFAULT_DESKTOP_NAME;
const localizedDesktopName =
gettext.dgettext('xdg-user-dirs', systemDesktopDirName);
return localizedDesktopName ?? systemDesktopDirName;
}
_getSystemDesktopDir() {
const systemDirsGioFile = this._getXdgSystemDirs();
if (!systemDirsGioFile) {
console.error('No system xdg user-dirs.default file');
return null;
}
let xdgSystemDesktopPath = null;
const decoder = new TextDecoder();
try {
const contents =
decoder
.decode(GLib.file_get_contents(systemDirsGioFile.get_path())[1])
.trim();
if (contents) {
const parseSystemconfig = true;
xdgSystemDesktopPath =
this._parseUserDirsFile(contents, parseSystemconfig);
}
} catch (e) {
console.error(e, `XDG Desktop not set in user-dirs.default, ${e}`);
}
return xdgSystemDesktopPath;
}
async _finishChooseDesktopFolder(dialog, asyncResult) {
let newFolder = null;
try {
newFolder = dialog.select_folder_finish(asyncResult);
} catch (e) {
if (e.matches(Gtk.DialogError, Gtk.DialogError.CANCELLED) ||
e.matches(Gtk.DialogError, Gtk.DialogError.DISMISSED))
return;
console.error(e, `Error selecting folder: ${e.message}`);
}
if (!newFolder)
return;
await this._setDesktopFolder(newFolder);
}
async _setDesktopFolder(newFolder) {
const isFolder =
newFolder.query_file_type(
Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
null
) === Gio.FileType.DIRECTORY;
if (!isFolder)
return;
if (newFolder.get_path() === this._desktopDir.get_path())
return;
await this._writeXdgUserDirsDesktopFile(newFolder.get_path());
}
_isDefaultDesktopFolder() {
const localizedDesktopName = this.getSystemLocalizedDesktopDir();
const defaultDesktop = GLib.build_filenamev([GLib.get_home_dir(),
localizedDesktopName]);
return this._desktopDir.get_path() === defaultDesktop;
}
_parseUserDirsFile(content, systemconfig = null) {
if (!content)
return null;
const serarchstring = systemconfig ? 'DESKTOP=' : 'XDG_DESKTOP_DIR=';
const lineArray = content.trim().split('\n');
const desktopline =
lineArray.filter(l => l.startsWith(serarchstring))[0];
let xdgDesktopPath = desktopline.split('=')[1].trim();
xdgDesktopPath = xdgDesktopPath.replace(/^"|"$/g, '');
xdgDesktopPath = xdgDesktopPath.replace('$HOME', GLib.get_home_dir());
return xdgDesktopPath;
}
async _writeXdgUserDirsDesktopFile(path) {
const userDirsGioFile = this._getXdgUserDirs();
if (path.startsWith(GLib.get_home_dir()))
path = path.replace(GLib.get_home_dir(), '$HOME');
const newline = `XDG_DESKTOP_DIR="${path}"`;
try {
const decoder = new TextDecoder();
const contents =
decoder
.decode(GLib.file_get_contents(userDirsGioFile.get_path())[1])
.trim();
const lineArray = contents.split('\n');
const newArray = lineArray.map(l => {
if (l.startsWith('XDG_DESKTOP_DIR='))
return newline;
return l;
});
const newContents = newArray.join('\n');
await this._replaceFileContentsAsync(userDirsGioFile, newContents);
} catch (e) {
console.error(e, `Failed to write XDG Desktop file with ${e}`);
}
}
async _replaceFileContentsAsync(destinationFile, contents) {
const textCoder = new TextEncoder();
const byteArray = new GLib.Bytes(textCoder.encode(contents));
try {
await destinationFile.replace_contents_async(
byteArray,
null,
false,
Gio.FileCreateFlags.REPLACE_DESTINATION,
null
);
} catch (e) {
if (e.matches(
Gio.IOErrorEnum,
Gio.IOErrorEnum.NOT_EMPTY
)) {
GLib.mkdir_with_parents(
GLib.path_get_dirname(destinationFile.get_path()),
0o700
);
this._replaceFileContentsAsync(destinationFile, contents);
return;
}
console.error(e, `Failed to write XDG Desktop file with ${e}`);
}
}
_getXdgUserDirs() {
const xdgUserDirspath =
Gio.File.new_build_filenamev(
[GLib.get_user_config_dir(), this.Enums.XDG_USER_DIRS]
);
return xdgUserDirspath;
}
_getXdgSystemDirs() {
const xdgSystemDirsArray = GLib.get_system_config_dirs();
for (let dir of xdgSystemDirsArray) {
const xdgSystemdir = Gio.File.new_build_filenamev(
[dir, this.Enums.XDG_SYSTEM_DIRS]
);
if (xdgSystemdir.query_exists(null))
return xdgSystemdir;
}
return null;
}
get activeWindow() {
if (this._activeWindow)
return this._activeWindow;
return Gio.Application.get_default().get_active_window();
}
set activeWindow(window) {
this._activeWindow = window;
}
get isDefaultDesktop() {
return this._isDefaultDesktopFolder();
}
};

File diff suppressed because it is too large Load Diff

230
ding/app/utils/gsConnect.js Normal file
View File

@@ -0,0 +1,230 @@
/* GsConnect Proxy
*
* Copyright (C) 2021, 2025 Sundeep Mediratta (smedius@gmail.com)
* Translation to javascript of python file with tweaks for DING
*
* Based on nautilus-gsconnect.py -
* A Nautilus extension for sending files via GSConnect by Andy Holmes
*fg
* A great deal of credit and appreciation is owed to the indicator-kdeconnect
* developers for the sister Python script 'kdeconnect-send-nautilus.py':
* 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, version 3 of the License.
*
* 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/>.
*/
import {Gio, GLib} from '../../dependencies/gi.js';
export {GsConnectSendFileOperationsManager};
var GsConnectSendFileOperationsManager = class {
constructor(GsConnectManager, mainApp) {
this._mainApp = mainApp;
this.gsConnectDevices = {};
this.devices = {};
this.gsConnectServiceName = 'org.gnome.Shell.Extensions.GSConnect';
this.gsConnectServicePath = '/org/gnome/Shell/Extensions/GSConnect';
this.GsConnectManager = GsConnectManager;
this.GsConnectManager.connect('changed-status', () => {
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, () => {
this._startGsConnectService();
return false;
});
});
this._startGsConnectService();
this._createSendAction();
}
_startGsConnectService() {
if (this.GsConnectManager.isAvailable) {
this.gsConnectProxy = this.GsConnectManager.proxy;
if (!this.gsConnectProxy) {
this._stopService();
return;
}
this.signalID =
this.gsConnectProxy.connect(
'g-signal',
this._on_g_signal.bind(this)
);
this._on_name_owner_changed();
} else {
this._stopService();
}
}
_stopService() {
this.gsConnectDevices = {};
this.devices = {};
if (this.signalID) {
this.gsConnectProxy.disconnect(this.signalID);
this.signalID = null;
}
}
_on_g_signal(proxy, senderName, signalName, parameters) {
// Wait until the service is ready
if (!this.gsConnectProxy.get_name_owner())
return;
let objects = parameters.recursiveUnpack();
if (signalName === 'InterfacesAdded') {
for (let [objectPath, props] of Object.entries(objects)) {
props = props['org.gnome.Shell.Extensions.GSConnect.Device'];
if (!props)
continue;
let action =
Gio.DBusActionGroup.get(
this.gsConnectProxy.get_connection(),
this.gsConnectServiceName,
objectPath
);
this.gsConnectDevices[objectPath] = [props['Name'], action];
}
} else if (signalName === 'InterfacesRemoved') {
for (const objectPath of Object.keys(objects)) {
try {
delete this.gsConnectDevices[objectPath];
} catch (e) {}
}
}
this._update_devices();
}
_on_name_owner_changed() {
// Wait until the service is ready
if (!this.gsConnectProxy.get_name_owner()) {
this.gsConnectDevices = {};
} else {
this.gsConnectProxy.call(
'GetManagedObjects',
null,
Gio.DBusCallFlags.NO_AUTO_START,
-1,
null,
this._get_managed_objects.bind(this)
);
}
}
_get_managed_objects(proxy, res) {
let objects = this.gsConnectProxy.call_finish(res).recursiveUnpack()[0];
if (objects) {
for (let [objectPath, props] of Object.entries(objects)) {
props = props['org.gnome.Shell.Extensions.GSConnect.Device'];
if (!Object.keys(props).length > 0)
continue;
let action =
Gio.DBusActionGroup.get(
this.gsConnectProxy.get_connection(),
this.gsConnectServiceName,
objectPath
);
this.gsConnectDevices[objectPath] = [props['Name'], action];
}
this._update_devices();
}
}
_createSendAction() {
let sendfiles = new Gio.SimpleAction({
name: 'sendfiles',
parameter_type: new GLib.VariantType('s'),
});
sendfiles.connect('activate', (action, parameter) => {
let device = parameter.recursiveUnpack();
this._send_files(device);
});
this._mainApp.add_action(sendfiles);
}
_send_files(device) {
// send files to shareFile action in actiongroup
// for the devices actiongroup
let actionGroup = this.devices[device];
for (let file of this.sendablefiles) {
let variant = GLib.Variant.new('(sb)', [file.get_uri(), false]);
actionGroup.activate_action('shareFile', variant);
}
}
_update_devices() {
this.devices = {};
for (let [name, actionGroup] of Object.values(this.gsConnectDevices)) {
if (actionGroup.get_action_enabled('shareFile'))
this.devices[name] = actionGroup;
}
}
_get_devices() {
// No capable devices
if (!Object.keys(this.devices).length > 0)
return null;
else
return true;
}
_sendable_file_items(files) {
// Return a list of select files to be sent
// Only accept regular files
for (let f of files) {
if (f.get_uri_scheme() !== 'file')
return null;
}
return true;
}
create_gsconnect_menu(files) {
this.sendablefiles = files.map(f => f.file);
this._update_devices();
if (
this._sendable_file_items(this.sendablefiles) &&
this._get_devices()
) {
this._menu = new Gio.Menu();
for (let device of Object.keys(this.devices)) {
let menuitem = Gio.MenuItem.new(`${device}`, null);
menuitem.set_action_and_target_value(
'app.sendfiles',
GLib.Variant.new('s', `${device}`)
);
this._menu.append_item(menuitem);
}
return this._menu;
} else {
return null;
}
}
};

161
ding/app/volumeIcon.js Normal file
View File

@@ -0,0 +1,161 @@
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Adw-DING Copyright (C) 2022, 2025 Sundeep Mediratta (smedius@gmail.com)
* Based on code original (C) Carlos Soriano and (c) Sergio Costas
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {Gtk, Gio} from '../dependencies/gi.js';
import {FileItemIcon} from '../dependencies/localFiles.js';
import {_} from '../dependencies/gettext.js';
export {VolumeIcon};
const VolumeIcon = class extends FileItemIcon {
constructor(desktopManager, file, fileInfo, fileExtra, gioMount) {
super(desktopManager, file, fileInfo, fileExtra, gioMount);
if (this._gioMount) {
/* gjs doesn't handle some virtual implementations well*/
Gio._promisify(this._gioMount.constructor.prototype,
'eject_with_operation');
Gio._promisify(this._gioMount.constructor.prototype,
'unmount_with_operation');
}
}
_destroy() {
super._destroy();
if (this._umountCancellable)
this._umountCancellable.cancel();
if (this._ejectCancellable)
this._ejectCancellable.cancel();
}
_getVisibleName() {
if (this._fileTypeEnum === this.Enums.FileType.EXTERNAL_DRIVE)
return this._gioMount.get_name();
return super._getVisibleName();
}
_setAccesibilityName() {
const visibleName = this._getVisibleName();
const driveName = _('Drive');
if (this._fileTypeEnum === this.Enums.FileType.EXTERNAL_DRIVE) {
/** TRANSLATORS: when using a screen reader, this is the text
* read when an external drive is selected.
* Example: if a USB stick named "my_portable"
* is selected, it will say "my_portable Drive" */
this.container.update_property(
[Gtk.AccessibleProperty.LABEL],
[`${visibleName} ${driveName}`]
);
}
}
_getDefaultIcon() {
if (this._fileTypeEnum === this.Enums.FileType.EXTERNAL_DRIVE)
return this._gioMount.get_icon();
return super._getDefaultIcon();
}
async eject(atWidget) {
if (!this._gioMount || this._ejectCancellable)
return;
const parentWidget = atWidget ?? this._grid._window;
const mountOp = new Gtk.MountOperation();
mountOp.set_parent(parentWidget);
this._ejectCancellable = new Gio.Cancellable();
try {
await this._gioMount.eject_with_operation(
Gio.MountUnmountFlags.NONE,
mountOp,
this._ejectCancellable
);
} catch (e) {
// I cannot find the exact Gio Enum, Gio.MountOperationResult
// does not work. Shortcut :)
// logError(e, `Mount failed: ${e.domain} ${e.code}`);
if (!(e.domain === 195 && e.code === 30)) {
console.error(
e,
`Exception ejecting Volume ${
this._getVisibleName()
? this._getVisibleName()
: 'Volume icon'
}: ${e.message}`
);
}
} finally {
this._ejectCancellable = null;
}
}
async unmount(atWidget) {
if (!this._gioMount || this._umountCancellable)
return;
const parentWidget = atWidget ?? this._grid._window;
const mountOp = new Gtk.MountOperation();
mountOp.set_parent(parentWidget);
this._umountCancellable = new Gio.Cancellable();
try {
await this._gioMount.unmount_with_operation(
Gio.MountUnmountFlags.NONE,
mountOp,
this._umountCancellable
);
} catch (e) {
// I cannot find the exact Gio Enum, Gio.MountOperationResult
// does not work. Shortcut :)
// logError(e, `Mount failed: ${e.domain} ${e.code}`);
if (!(e.domain === 195 && e.code === 30)) {
console.error(
e,
`Exception unmounting Volume ${
this._getVisibleName()
? this._getVisibleName()
: 'Volume icon'
}: ${e.message}`
);
}
} finally {
this._umountCancellable = null;
}
}
get canEject() {
if (this._gioMount)
return this._gioMount.can_eject();
else
return false;
}
get canUnmount() {
if (this._gioMount)
return this._gioMount.can_unmount();
else
return false;
}
};

644
ding/app/widgetApi.js Normal file
View File

@@ -0,0 +1,644 @@
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Gtk4 Port Copyright (C) 2022 - 2025 Sundeep Mediratta (smedius@gmail.com)
*
* 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, version 3 of the License.
*
* 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/>.
*/
// widget-js-api.js
// Exports the script that gets injected into each WebView.
const transparencyCSS = `
/* Keep the page transparent without nuking widget element backgrounds */
html, body {
background: transparent !important;
background-color: transparent !important;
background-image: none !important;
}
`;
export const CSP_STRICT = `
default-src 'none';
base-uri 'none';
object-src 'none';
frame-ancestors 'none';
form-action 'none';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data:;
media-src 'self' blob:;
connect-src 'self' https: ;
navigate-to 'self';
block-all-mixed-content;
worker-src 'none';
frame-src 'none';
`;
export const CSP_DEV = `
default-src 'none';
base-uri 'none';
object-src 'none';
frame-ancestors 'none';
form-action 'none';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data:;
media-src 'self' blob:;
connect-src
'self'
https:
http:
http://localhost:*
ws://localhost:*;
worker-src 'none';
frame-src 'none';
`;
export const CSP_RELAXED = `
default-src 'none';
base-uri 'self';
object-src 'none';
frame-ancestors 'none';
script-src
'self'
'unsafe-inline'
https:;
style-src
'self'
'unsafe-inline'
https:;
img-src 'self' data: blob: https:;
font-src 'self' data: https:;
media-src 'self' blob: https:;
connect-src
'self'
https:
http:
ws:
wss:;
worker-src blob:;
frame-src https:;
`;
export const WIDGET_UNAVAILABLE_HTML = `
<!doctype html>
<html>
<head><meta charset="utf-8"></head>
<body style="margin:0;padding:0;display:flex;align-items:center;justify-content:center;font:14px system-ui;background:rgba(0,0,0,0.05);color:#555;">
<div>
<div style="font-weight:600;">Widget unavailable</div>
<div style="font-size:12px;opacity:0.8;">__REASON__</div>
</div>
</body>
</html>`;
export const WIDGET_API =
`(function() {
'use strict';
// Avoid re-injecting if the page has already been initialized
if (window.ding)
return;
try {
const style = document.createElement('style');
style.id = 'ding-widget-background';
style.textContent = \`${transparencyCSS}\`;
document.documentElement.insertAdjacentElement('afterbegin', style);
} catch (e) {
// Failing to inject style is non-fatal.
console.error('ding: failed to inject default style', e);
}
// ---------------------------------------------------------------------
// Upward channel: widget -> host (via WebKit messageHandler)
// ---------------------------------------------------------------------
function post(message) {
try {
if (!window.webkit ||
!window.webkit.messageHandlers ||
!window.webkit.messageHandlers.dingWidget)
return;
window.webkit.messageHandlers.dingWidget.postMessage(
JSON.stringify(message)
);
} catch (e) {
try {
console.error('ding: post failed', e);
} catch (_ignored) {
// ignore logging failures
}
}
}
// ---------------------------------------------------------------------
// Small helper: query parameter parsing
// ---------------------------------------------------------------------
function getQueryParam(qs, key) {
if (!qs || qs.length <= 1)
return null;
if (qs.charAt(0) === '?')
qs = qs.substring(1);
var parts = qs.split('&');
for (var i = 0; i < parts.length; i++) {
var kv = parts[i].split('=');
if (kv.length === 2 && kv[0] === key) {
try {
return decodeURIComponent(kv[1]);
} catch (e) {
return kv[1];
}
}
}
return null;
}
// ---------------------------------------------------------------------
// Config cache + configChanged (downward push)
// ---------------------------------------------------------------------
var _configCache = {};
var _configListeners = new Set();
function _cloneObject(obj) {
if (!obj || typeof obj !== 'object')
return {};
var out = {};
for (var k in obj) {
if (Object.prototype.hasOwnProperty.call(obj, k))
out[k] = obj[k];
}
return out;
}
function _notifyConfigListeners(meta) {
var snapshot = _cloneObject(_configCache);
_configListeners.forEach(function(cb) {
try {
cb(snapshot, meta || null);
} catch (e) {
try {
console.error('ding: configChanged listener failed', e);
} catch (_ignored) {}
}
});
}
function _setConfigCache(nextConfig, meta) {
if (!nextConfig || typeof nextConfig !== 'object')
nextConfig = {};
_configCache = _cloneObject(nextConfig);
_notifyConfigListeners(meta);
}
function getConfigCached() {
return _cloneObject(_configCache);
}
// ---------------------------------------------------------------------
// getConfig request/response plumbing
// ---------------------------------------------------------------------
var pending = new Map();
var msgCounter = 1;
var _backendPending = new Map();
var _backendListeners = new Set();
// Host responds to getConfig by running:
// window.postMessage({ _dingInternal: true, requestId, config }, '*');
window.addEventListener('message', function(event) {
var data = event.data;
if (!data || data._dingInternal !== true)
return;
var type = data.type || null;
// --- Backend first ---
if (type === 'backendEvent') {
_backendListeners.forEach(function(cb) {
try {
cb(data.name, data.payload);
} catch (_e) {}
});
return;
}
if (type === 'backendReply') {
var requestId = data.requestId;
if (requestId === undefined || requestId === null)
return;
var pendingReq = _backendPending.get(requestId);
if (!pendingReq)
return;
_backendPending.delete(requestId);
if (data.ok)
pendingReq.resolve(data.result);
else
pendingReq.reject(data.error || {
code: 'E_BACKEND',
message: 'Backend request failed',
});
return;
}
// --- Config plumbing (only for config message types) ---
var requestId = data.requestId || null;
var config = data.config;
if (config && typeof config === 'object') {
_setConfigCache(config, {
reason: type === 'configChanged'
? (data.reason || 'configChanged')
: 'getConfigReply',
sourceMode: data.sourceMode || null,
});
}
if (type === 'configChanged')
return;
if (!requestId)
return;
var resolver = pending.get(requestId);
if (!resolver)
return;
pending.delete(requestId);
try {
resolver(config || {});
} catch (e) {
try {
console.error('ding: getConfig resolver failed', e);
} catch (_ignored) {}
}
});
// ---------------------------------------------------------------------
// Host state (downward channel)
// ---------------------------------------------------------------------
var _hostState = {
editMode: false,
selected: false,
theme: 'light',
reducedMotion: false,
direction: 'ltr',
locale: (typeof navigator !== 'undefined' && navigator.language) ?
navigator.language :
'en_US',
};
var _hostStateListeners = new Set();
// ---------------------------------------------------------------------
// Page-side debug flag:
// Host can flip this with:
// webView.evaluate_javascript("window.DING_DEBUG_HOST_STATE = true;", ...)
// ---------------------------------------------------------------------
window.DING_DEBUG_HOST_STATE = false;
function _debugHostState(msg, data) {
if (!window.DING_DEBUG_HOST_STATE)
return;
try {
console.log('ding[host-state]', msg, data);
} catch (_e) {
// ignore
}
}
function _applyHostStateToDom() {
try {
var docEl = document.documentElement;
var body = document.body;
if (!docEl || !body)
return;
// Direction
docEl.dir = _hostState.direction || 'ltr';
// Theme
body.dataset.theme = _hostState.theme || 'light';
// Edit mode & selection
body.classList.toggle('ding-edit-mode', !!_hostState.editMode);
body.classList.toggle('ding-selected', !!_hostState.selected);
// Reduced motion:
body.classList.toggle('ding-reduced-motion', !!_hostState.reducedMotion);
} catch (_e) {
// Don't let DOM sync failures break host-state updates
}
}
function _cloneHostState() {
var out = {};
for (var k in _hostState) {
if (Object.prototype.hasOwnProperty.call(_hostState, k))
out[k] = _hostState[k];
}
return out;
}
function _notifyHostStateListeners() {
var snapshot = _cloneHostState();
_debugHostState('notify', snapshot);
_applyHostStateToDom()
_hostStateListeners.forEach(function(cb) {
try {
cb(snapshot);
} catch (e) {
try {
console.error('ding: hostState listener failed', e);
} catch (_ignored) {}
}
});
}
function _setHostState(patch) {
if (!patch || typeof patch !== 'object')
return;
_debugHostState('patch', patch);
var changed = false;
for (var key in patch) {
if (!Object.prototype.hasOwnProperty.call(patch, key))
continue;
var value = patch[key];
if (_hostState[key] !== value) {
_hostState[key] = value;
changed = true;
}
}
if (changed)
_notifyHostStateListeners();
}
// ---------------------------------------------------------------------
// Instance ID derivation from URL query parameters
// ---------------------------------------------------------------------
var initialInstanceId = null;
var initialMode = 'widget'; // default
try {
var search = (window.location && window.location.search) || '';
var qmode = getQueryParam(search, 'dingMode');
if (qmode === 'prefs' || qmode === 'widget')
initialMode = qmode;
var qid =
getQueryParam(search, 'dingInstanceId') ||
getQueryParam(search, 'widgetInstanceId') ||
getQueryParam(search, 'instanceId');
if (qid)
initialInstanceId = qid;
} catch (e) {
// ignore
}
// ---------------------------------------------------------------------
// Public API: window.ding
// ---------------------------------------------------------------------
window.ding = {
apiVersion: 1,
instanceId: initialInstanceId,
mode: initialMode,
// Expose raw post() if widgets want it
post: post,
getInstanceId: function() {
return this.instanceId;
},
// -----------------------------
// Widget -> host helpers
// -----------------------------
log: function(message) {
post({
type: 'log',
instanceId: this.instanceId,
message: String(message),
});
},
saveConfig: function(config) {
if (!this.instanceId)
return;
post({
type: 'updateConfig',
instanceId: this.instanceId,
config: config || {},
});
},
getConfig: function() {
if (!this.instanceId)
return Promise.resolve(null);
var requestId = msgCounter++;
return new Promise(function(resolve) {
pending.set(requestId, resolve);
post({
type: 'getConfig',
instanceId: window.ding.instanceId,
requestId: requestId,
});
});
},
// Can return {} if the cache is not initialized yet
getConfigSync: function() {
return getConfigCached();
},
// -----------------------------
// Host -> widget helpers
// -----------------------------
/**
* Returns a shallow copy of the current host state:
* {
* editMode, selected, theme, visible,
* reducedMotion, direction, locale
* }
*/
getHostState: function() {
return _cloneHostState();
},
/**
* Subscribe to host state changes.
* Returns an unsubscribe() function.
*/
onHostStateChanged: function(cb) {
if (typeof cb !== 'function')
return function() {};
_hostStateListeners.add(cb);
// Immediately deliver current snapshot
try {
cb(_cloneHostState());
} catch (e) {
try {
console.error('ding: hostState listener failed (initial)', e);
} catch (_ignored) {}
}
return function() {
_hostStateListeners.delete(cb);
};
},
onConfigChanged: function(cb) {
if (typeof cb !== 'function')
return function() {};
_configListeners.add(cb);
try {
cb(
_cloneObject(_configCache),
{ reason: 'initial', sourceMode: null }
);
} catch (_e) {}
// Return unsubscribe
return function() {
_configListeners.delete(cb);
};
},
backendRequest: function(method, params) {
if (!this.instanceId)
return Promise.reject(new Error('No instanceId'));
var requestId = msgCounter++;
return new Promise(function(resolve, reject) {
_backendPending.set(requestId, { resolve, reject });
post({
type: 'backendRequest',
instanceId: window.ding.instanceId,
requestId: requestId,
method: method,
params: params || {},
});
});
},
backendSend: function(name, payload) {
if (!this.instanceId)
return;
post({
type: 'backendSend',
instanceId: window.ding.instanceId,
name: name,
payload: payload || {},
});
},
onBackendEvent: function(cb) {
if (typeof cb !== 'function')
return function() {};
_backendListeners.add(cb);
return function() {
_backendListeners.delete(cb);
};
},
// ---------------------------------------------------------------------
// INTERNAL: host-only entrypoint. The GJS side calls this via
// evaluate_javascript() to push patches into _hostState.
// ---------------------------------------------------------------------
_setHostState: _setHostState,
};
(function() {
function doInitialDomSync() {
try {
_debugHostState('initial-dom-sync', _cloneHostState());
_notifyHostStateListeners();
} catch (_e) {
// Ignore; we don't want this to break the widget
}
}
if (document.readyState === 'loading') {
// Body not ready yet; wait for DOMContentLoaded
document.addEventListener('DOMContentLoaded', function onReady() {
document.removeEventListener('DOMContentLoaded', onReady);
doInitialDomSync();
});
} else {
// DOM is already ready (e.g. script injected later)
doInitialDomSync();
}
})();
// Notify the host that the widget API is ready and has an instanceId
try {
post({
type: 'hostReady',
instanceId: initialInstanceId || null,
});
} catch (_e) {
// ignore
}
})();`;

2260
ding/app/widgetManager.js Normal file

File diff suppressed because it is too large Load Diff

573
ding/app/widgetRegistry.js Normal file
View File

@@ -0,0 +1,573 @@
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Gtk4 Port Copyright (C) 2022 - 2025 Sundeep Mediratta (smedius@gmail.com)
*
* 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, version 3 of the License.
*
* 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/>.
*/
/*
* Layout:
* $XDG_DATA_HOME/<app-id>/widgets/<widgetId>/widget.json
* $XDG_DATA_DIRS/.../<app-id>/widgets/<widgetId>/widget.json
*/
import {Gio, GLib} from '../dependencies/gi.js';
export {WidgetRegistry};
const WidgetRegistry = class {
constructor(desktopIconsUtil) {
this._util = desktopIconsUtil;
this._appId = null;
this._userRoot = null;
this._systemRoots = [];
// id -> descriptor
this._widgets = new Map();
this._loaded = false;
this._loadingPromise = null;
this._initPaths();
this.preload();
}
// ---------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------
preload() {
this._ensureLoadedAsync().catch(e => {
console.error('WidgetRegistry.preload():', e);
});
}
/**
* Get a snapshot of all known widgets.
*
* @returns {Promise<Array<object>>}
*/
async listWidgets() {
await this._ensureLoadedAsync();
return Array.from(this._widgets.values());
}
/**
* Look up a widget descriptor by ID.
*
* Descriptor shape:
* {
* id: string,
* name: string,
* kind: 'html' | 'gtk',
* dir: Gio.File,
* manifestFile: Gio.File,
* isUser: boolean,
* defaultWidth: number | null,
* defaultHeight: number | null,
* defaultConfig: object | null,
* "name_localized": {
* "fr": "Horloge analogique"
* },
* description: "A simple analog clock widget.",
* "description_localized": {
* "fr": "Un widget d'horloge analogique simple."
* },
* category: "time",
* author: "Sundeep Mediratta",
* version: "1.0",
* homepage: "https://…",
* license: "GPL-3.0-or-later"
* }
*
* @param {string} id Widget identifier
* @returns {Promise<object|null>}
*/
async getDescriptor(id) {
if (!id)
return null;
await this._ensureLoadedAsync();
return this._widgets.get(id) ?? null;
}
/*
* Get widget kind ('html' | 'gtk')
*
* @param {string} id Widget identifier
* @returns {Promise<'html'|'gtk'>}
*/
async getKind(id) {
const desc = await this.getDescriptor(id);
return desc?.kind === 'gtk' ? 'gtk' : 'html';
}
/**
* For HTML widgets, return the entry file (always index.html).
*
* @param {string} id Widget identifier
* @returns {Promise<Gio.File|null>}
*/
async getHtmlEntryFile(id) {
const desc = await this.getDescriptor(id);
if (!desc || desc.kind !== 'html')
return null;
const file = desc.dir.get_child('index.html');
try {
const info = file.query_info(
'standard::type',
Gio.FileQueryInfoFlags.NONE,
null
);
if (info.get_file_type() === Gio.FileType.REGULAR)
return file;
} catch (e) {
console.error(
'WidgetRegistry: getHtmlEntryFile failed for',
id,
e
);
}
return null;
}
reload() {
this._loaded = false;
this._loadingPromise = null;
this._widgets.clear();
this.preload();
}
// ---------------------------------------------------------------------
// Internal
// ---------------------------------------------------------------------
_initPaths() {
const mainApp = this._util.mainApp;
const appId = mainApp?.get_application_id
? mainApp.get_application_id()
: null;
if (!appId) {
console.error(
'WidgetRegistry: application-id not available; ' +
'widget paths will not be resolved.'
);
return;
}
this._appId = appId;
// User root: $XDG_DATA_HOME/<app-id>/widgets
try {
const appDataDir = this._util.getAppUserDataDir();
this._userRoot = appDataDir.get_child('widgets');
} catch (e) {
console.warn('WidgetRegistry: failed to resolve user root:', e);
}
// System roots: for each XDG data dir, <dir>/<app-id>/widgets
try {
const systemBaseDirs = GLib.get_system_data_dirs();
this._systemRoots = systemBaseDirs.map(base => {
return Gio.File.new_build_filenamev([
base,
this._appId,
'widgets',
]);
});
} catch (e) {
console.warn('WidgetRegistry: failed to build system roots:', e);
this._systemRoots = [];
}
}
async _ensureLoadedAsync() {
if (this._loaded)
return;
// Idempotent
if (this._loadingPromise) {
await this._loadingPromise;
return;
}
const loadPromise = this._loadOnceAsync();
this._loadingPromise = loadPromise;
try {
await loadPromise;
} finally {
this._loadingPromise = null;
}
}
async _loadOnceAsync() {
const newMap = new Map();
this._loggedDuplicateIds = new Set();
if (this._userRoot)
await this._scanRootAsync(this._userRoot, true, newMap);
for (const root of this._systemRoots)
// eslint-disable-next-line no-await-in-loop
await this._scanRootAsync(root, false, newMap);
this._widgets = newMap;
this._loaded = true;
}
/**
* Async scan of a single root dir:
* <root>/<widgetId>/widget.json
*
* @param {Gio.File} root
* @param {boolean} isUser
* @param {Map<string,object>} outMap
*/
async _scanRootAsync(root, isUser, outMap) {
let info;
try {
info = root.query_info(
'standard::type',
Gio.FileQueryInfoFlags.NONE,
null
);
} catch (e) {
return;
}
if (info.get_file_type() !== Gio.FileType.DIRECTORY)
return;
let enumerator;
try {
enumerator = root.enumerate_children(
'standard::name,standard::type',
Gio.FileQueryInfoFlags.NONE,
null
);
} catch (e) {
console.error(
'WidgetRegistry: enumerate_children failed for',
root.get_path?.(),
e
);
return;
}
try {
while (true) {
// eslint-disable-next-line no-await-in-loop
const files = await this._nextFilesAsync(enumerator);
if (!files.length)
break;
for (const finfo of files) {
if (finfo.get_file_type() !== Gio.FileType.DIRECTORY)
continue;
const name = finfo.get_name();
const widgetDir = root.get_child(name);
const manifestFile =
widgetDir.get_child('widget.json');
let manifestInfo;
try {
manifestInfo = manifestFile.query_info(
'standard::type',
Gio.FileQueryInfoFlags.NONE,
null
);
} catch (e) {
continue;
}
if (manifestInfo.get_file_type() !== Gio.FileType.REGULAR)
continue;
const manifest =
// eslint-disable-next-line no-await-in-loop
await this._util.readJsonFile(manifestFile);
if (!manifest)
continue;
// ID must come from widget.json and must be valid.
const idRaw =
typeof manifest.id === 'string'
? manifest.id.trim() : '';
const idOk =
!!idRaw && /^[A-Za-z0-9._-]+$/.test(idRaw);
if (!idOk) {
const mf =
manifestFile.get_path?.() ??
manifestFile.get_uri?.() ??
String(manifestFile);
console.warn(
`WidgetRegistry:
rejecting widget with invalid id in ${mf}`
);
continue;
}
const id = idRaw;
const kind = manifest.kind === 'gtk' ? 'gtk' : 'html';
const displayName = manifest.name || id;
const description = manifest.description || '';
const author = manifest.author || '';
const version = manifest.version || '';
const icon = manifest.icon || '';
const defaultWidth =
Number.isFinite(manifest.defaultWidth)
? Math.max(1, Math.floor(manifest.defaultWidth))
: 260;
const defaultHeight =
Number.isFinite(manifest.defaultHeight)
? Math.max(1, Math.floor(manifest.defaultHeight))
: 160;
const defaultConfig =
this._isObject(manifest.defaultConfig)
? manifest.defaultConfig
: {};
const prefs =
typeof manifest.prefs === 'string'
? manifest.prefs
: null;
const backend =
this._isObject(manifest.backend)
? manifest.backend
: null;
const desc = {
id,
kind,
dir: widgetDir,
manifestFile,
isUser,
displayName,
description,
author,
version,
icon,
defaultWidth,
defaultHeight,
defaultConfig,
prefs,
backend,
hasBackend: !!backend,
};
// Resolve duplicates deterministically;
// log each duplicated id only once.
const existing = outMap.get(id);
if (existing) {
const replace = isUser && !existing.isUser;
this._logDuplicateIds(
id, existing, replace, widgetDir, isUser
);
if (replace)
outMap.set(id, desc);
} else {
outMap.set(id, desc);
}
}
}
} catch (e) {
console.error(
'WidgetRegistry: error scanning root',
root.get_path?.(),
e
);
} finally {
try {
enumerator.close(null);
} catch (e) {
console.error(
'WidgetRegistry: error closing enumerator',
e
);
}
}
}
_isObject(object) {
const isObject =
object !== null &&
typeof object === 'object' &&
!Array.isArray(object) &&
Object.getPrototypeOf(object) === Object.prototype;
return isObject;
}
_logDuplicateIds(id, existing, replace, widgetDir, isUser) {
if (this._loggedDuplicateIds.has(id))
return;
const toPathString = file =>
file?.get_path?.() ??
file?.get_uri?.() ??
String(file);
const existingPath = toPathString(existing?.dir);
const newPath = toPathString(widgetDir);
const existingScope = existing?.isUser ? 'user' : 'system';
const newScope = isUser ? 'user' : 'system';
const action = replace ? 'using user override' : 'keeping first';
console.warn(
`WidgetRegistry: duplicate widget id "${id}": ` +
`${existingPath} (${existingScope}) vs ` +
`${newPath} (${newScope}) — ${action}`
);
this._loggedDuplicateIds.add(id);
}
_nextFilesAsync(enumerator) {
const batchSize = 32;
const cancelable = null;
return new Promise((resolve, reject) => {
enumerator.next_files_async(
batchSize,
GLib.PRIORITY_DEFAULT,
cancelable,
(src, res) => {
try {
const files = src.next_files_finish(res);
resolve(files ?? []);
} catch (e) {
reject(e);
}
}
);
});
}
normalizeBackendSpec(desc, inst) {
return this._normalizeBackendSpec(desc, inst);
}
_normalizeBackendSpec(desc, inst) {
const b = desc?.backend;
if (!b || typeof b !== 'object')
return null;
const dirFile = desc.dir;
const dirPath = dirFile?.get_path?.();
if (!dirFile || !dirPath)
return null;
const argv = this._buildBackendArgv(b, dirFile);
if (!argv?.length) {
console.error(
'WidgetRegistry: backend argv missing/invalid for widget',
desc?.id ?? '<unknown>'
);
return null;
}
const cwd = this._resolveBackendCwd(b, dirFile, dirPath);
const envOverrides =
b.env && typeof b.env === 'object' ? b.env : null;
const env = {
DING_WIDGET_ID: String(inst?.widgetId ?? ''),
DING_INSTANCE_ID: String(inst?.instanceId ?? ''),
};
if (envOverrides) {
for (const [key, value] of Object.entries(envOverrides)) {
if (typeof key !== 'string')
continue;
if (value === undefined)
continue;
env[key] = typeof value === 'string'
? value
: String(value);
}
}
return {
argv,
cwd,
env,
};
}
_buildBackendArgv(backend, dirFile) {
const cmd = backend?.command;
if (typeof cmd !== 'string' || cmd.length === 0)
return null;
// eslint-disable-next-line no-nested-ternary
const args = Array.isArray(backend.args)
? backend.args.filter(a => typeof a === 'string')
: Array.isArray(backend.argv)
? backend.argv.filter(a => typeof a === 'string')
: [];
// Resolve argv[0] to an absolute path if it isn't already.
let argv0 = null;
if (cmd.startsWith('/')) {
argv0 = cmd;
} else if (cmd.includes('/')) {
const f = dirFile.resolve_relative_path
? dirFile.resolve_relative_path(cmd)
: dirFile.get_child(cmd);
argv0 = f?.get_path?.() ?? null;
} else {
argv0 = GLib.find_program_in_path(cmd);
if (!argv0) {
const f = dirFile.resolve_relative_path
? dirFile.resolve_relative_path(cmd)
: dirFile.get_child(cmd);
argv0 = f?.get_path?.() ?? null;
}
}
if (!argv0)
return null;
return [argv0, ...args];
}
_resolveBackendCwd(backend, dirFile, fallbackDirPath) {
const cwdRel =
typeof backend?.cwd === 'string' && backend.cwd.length
? backend.cwd
: '.';
const cwdFile = dirFile.resolve_relative_path
? dirFile.resolve_relative_path(cwdRel)
: dirFile.get_child(cwdRel);
return cwdFile?.get_path?.() ?? fallbackDirPath;
}
};

1042
ding/app/widgetWebContext.js Normal file

File diff suppressed because it is too large Load Diff

607
ding/app/windowManager.js Normal file
View File

@@ -0,0 +1,607 @@
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Adw/Gtk4 Port Copyright (C) 2025 Sundeep Mediratta (smedius@gmail.com)
*
* 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, version 3 of the License.
*
* 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/>.
*/
import {
DesktopGrid
} from '../dependencies/localFiles.js';
import {Gio, GLib, DesktopWidgetCapability} from '../dependencies/gi.js';
export {WindowManager};
const WindowManager = class {
constructor(desktopManager, desktopList, asDesktop, primaryIndex) {
this._desktopManager = desktopManager;
this._Prefs = desktopManager.Prefs;
this.mainApp = desktopManager.mainApp;
this._desktopList = desktopList;
this._primaryIndex = primaryIndex;
if (primaryIndex < desktopList.length)
this._primaryScreen = desktopList[primaryIndex];
else
this._primaryScreen = null;
this._priorDesktopList = [];
this._desktops = [];
this._asDesktop = asDesktop;
this._zoom = 1;
this._primaryMonitorIndex = null;
this._priorPrimaryIndex = null;
this._priorPrimaryMonitorIndex = null;
this._differentZooms = false;
this._hidden = false;
this._gridWindowsUpdateInProgress = false;
this._pendingDesktopList = null;
this._registerWidgetLayerAction();
this._dbusAdvertiseUpdate();
}
_dbusAdvertiseUpdate() {
const updateGridWindows = new Gio.SimpleAction({
name: 'updateGridWindows',
parameter_type: new GLib.VariantType('av'),
});
updateGridWindows.connect('activate', (action, parameter) => {
this.updateGridWindows(parameter.recursiveUnpack())
.catch(e => logError(e));
});
this.mainApp.add_action(updateGridWindows);
const busObjectPath = this.mainApp.get_dbus_object_path();
const busName = this.mainApp.get_application_id();
const connection = Gio.DBus.session;
const signalName = 'updategeometry';
const signalXml = `
<node>
<interface name="${busName}">
<signal name="${signalName}">
<arg name="type" type="s"/>
<arg name="value" type="b"/>
</signal>
</interface>
</node>`;
this._dbusGeometryIface =
Gio.DBusExportedObject.wrapJSObject(signalXml, this);
this._dbusGeometryIface.export(
connection,
busObjectPath
);
}
requestGeometryUpdate() {
const variant = new GLib.Variant('(sb)', ['updategeometry', true]);
const busObjectPath = this.mainApp.get_dbus_object_path();
const busName = this.mainApp.get_application_id();
const connection = Gio.DBus.session;
const signalName = 'updategeometry';
connection.emit_signal(
null,
busObjectPath,
busName,
signalName,
variant
);
}
async updateGridWindows(newdesktoplist) {
if (this._gridWindowsUpdateInProgress) {
this._pendingDesktopList = newdesktoplist;
return;
}
const changeInfo =
this._computeDesktopChangeInfo(newdesktoplist);
const {
firstDesktop,
monitorCountChanged,
monitorschangedList,
gridschangedList,
monitorschanged,
gridschanged,
redisplay,
} = changeInfo;
// Allow initial startup if no desktops defined on initiation
if (firstDesktop) {
await this._handleFirstDesktop();
return;
}
// If any new monitors plugged in or removed
// by creating new desktops
if (monitorCountChanged) {
await this._handleMonitorCountChange();
return;
}
if (redisplay) {
await this._handleRedisplay({
monitorschangedList,
gridschangedList,
monitorschanged,
gridschanged,
redisplay,
});
}
}
async _handleFirstDesktop() {
this._desktopManager.clearAllLayersFromGrids();
await this.createGridWindows();
// sanity checks and icons placement on grid will be done by
// desktopManager in sync startup
}
async _handleMonitorCountChange() {
this._gridWindowsUpdateInProgress = true;
// monitor has been plugged in or removed.
this._desktopManager.clearAllLayersFromGrids();
await this.createGridWindows();
await this._desktopManager.applyDesktopLayoutChange({
redisplay: true,
monitorschanged: true,
gridschanged: true,
});
this._gridWindowsUpdateInProgress = false;
await this._drainPendingUpdates();
}
async _drainPendingUpdates() {
if (this._gridWindowsUpdateInProgress)
return;
const next = this._pendingDesktopList;
this._pendingDesktopList = null;
if (next != null)
await this.updateGridWindows(next);
this.queue_draw();
}
async _handleRedisplay({
monitorschangedList,
gridschangedList,
monitorschanged,
gridschanged,
redisplay,
}) {
if (!redisplay)
return;
this._gridWindowsUpdateInProgress = true;
await this._displayDesktopSnapShots();
this._desktopManager.clearAllLayersFromGrids();
this._desktops.forEach((desktop, index) => {
desktop.updateGridDescription(this._desktopList[index]);
if (monitorschangedList.includes(index)) {
desktop.resizeWindow();
desktop.resizeGrid();
} else if (gridschangedList.includes(index)) {
desktop.resizeGrid();
}
});
// There is a subtle difference here, all information is needed
//
// gridschanged implies prior grid information is available.
// Therefore write mode is 'PRESERVE' initially
//
// monitors changed implies that all coordintes are rewritten to the
// new monitor relative coordinates with a write mode of 'OVERWRITE'
//
// redisplay re-arranges all the icons on the new desktop monitor,
// essential for proper sorting/stacking of icons and arranging of
// icons
//
// For keep arranged new coordinates are automatically written to
// grid. However for stacked co-ordinates- we will neeed to redo the
// old coordinates seperately in do stacks with nonitorschanged info
await this._desktopManager.applyDesktopLayoutChange({
redisplay,
monitorschanged,
gridschanged,
});
// animate to the new margins and positions
// force a queue draw of all windows now that we have drawn the desktop,
// and poke mutter to map the meta window.
this._displayAnimationToLive();
this._gridWindowsUpdateInProgress = false;
await this._drainPendingUpdates();
}
_updatePrimaryStateAndZoomInfo(newdesktoplist) {
// Save prior primary state
this._priorPrimaryIndex = this._primaryIndex ?? null;
this._priorPrimaryMonitorIndex = this._primaryMonitorIndex ?? 0;
// Compute new primary index
let newPrimaryIndex;
if (newdesktoplist.length > 0 &&
('primaryMonitor' in newdesktoplist[0]))
newPrimaryIndex = newdesktoplist[0].primaryMonitor ?? null;
// Update primary index if changed
if (newPrimaryIndex !== this._priorPrimaryIndex)
this._primaryIndex = newPrimaryIndex;
// Find the new primary monitor
this._primaryScreen = this._desktopList[this._primaryIndex] ?? null;
this._primaryMonitorIndex = this._primaryScreen.monitorIndex ?? null;
// See if there are different zooms in the desktops
this._differentZooms = this._desktopList.some((d, index) => {
const nextd = this._desktopList[index + 1];
if (nextd != null)
return d.zoom !== nextd.zoom;
return false;
});
}
_computeDesktopChangeInfo(newDesktopList) {
const priorDesktopList = this._desktopList;
this._priorDesktopList = priorDesktopList;
this._desktopList = newDesktopList;
this._updatePrimaryStateAndZoomInfo(newDesktopList);
// Allow initial startup if no desktops defined on initiation
const firstDesktop =
priorDesktopList.some(d => typeof d !== 'object' || d == null) ||
priorDesktopList.length === 0;
const monitorCountChanged =
priorDesktopList.length !== newDesktopList.length;
const monitorschangedList = [];
const gridschangedList = [];
// If this is the first desktop, we don't need finer diffing;
if (firstDesktop) {
return {
firstDesktop,
monitorCountChanged,
monitorschangedList,
gridschangedList,
monitorschanged: false,
gridschanged: false,
redisplay: false,
};
}
// if no change in monitors, check if any change in monitor geometry
// or if any change in grid geometry
newDesktopList.forEach((area, index) => {
const area2 = priorDesktopList[index];
if (!area || !area2) {
// Monitor count changed; mark this index as changed and skip diff
monitorschangedList.push(index);
gridschangedList.push(index);
return;
}
if ((area.x !== area2.x) ||
(area.y !== area2.y) ||
(area.width !== area2.width) ||
(area.height !== area2.height) ||
(area.zoom !== area2.zoom) ||
(area.monitorIndex !== area2.monitorIndex)
) {
monitorschangedList.push(index);
gridschangedList.push(index);
return;
}
if ((area.marginTop !== area2.marginTop) ||
(area.marginBottom !== area2.marginBottom) ||
(area.marginLeft !== area2.marginLeft) ||
(area.marginRight !== area2.marginRight)
) {
if (!gridschangedList.includes(index))
gridschangedList.push(index);
}
});
const indexChanged =
this._priorPrimaryMonitorIndex !== this._primaryMonitorIndex;
// indexChanged implies monitors have changed
// monitors changed or index changed implies grids have changed
// as there may be other actors on the new monitor edge
const monitorschanged = !!monitorschangedList.length || indexChanged;
// only the grids have changed, no monitor changes
const gridschanged = gridschangedList.length
? gridschangedList.some(i => !monitorschangedList.includes(i))
: false;
// redisplay is needed for sorting and stacking. Icons
// need to be redisplayed if anything changes - the actual fileList
// has not changed
const redisplay = monitorschanged || gridschanged;
return {
firstDesktop,
monitorCountChanged,
monitorschangedList,
gridschangedList,
monitorschanged,
gridschanged,
redisplay,
};
}
async _displayDesktopSnapShots() {
const array = this._desktops.map(
d => d.displaySnapshot()
);
await Promise.all(array).catch(e => logError(e));
}
_displayAnimationToLive() {
this._desktops.forEach(d => d.requestAnimatedRelayout());
}
async createGridWindows() {
// Allow startup with no desktops from constructor
// even if no desktops are defined when started by the extension
// desktops can be defined later from updateGridWindows(), dbus
// activation
if (!this._desktopList.length ||
this._desktopList.some(d => {
return typeof d !== 'object' || d == null;
}))
return;
this._desktops.forEach(desktop => desktop.destroy());
this._desktops = [];
this._desktopList.forEach((desktop, desktopIndex) => {
const desktopName =
this._asDesktop
? `@!${desktop.x},${desktop.y};BDHF`
: `DING ${desktopIndex}`;
this._desktops.push(
new DesktopGrid.DesktopGrid({
desktopManager: this._desktopManager,
desktopName,
desktopDescription: desktop,
asDesktop: this._asDesktop,
hidden: this._hidden,
desktopIndex,
})
);
});
const displayPromises =
this._desktops.map(desktop => desktop.ensureMapped());
const allocatedPromises =
this._desktops.map(d => d.ensureAllocationComplete());
let safegaurd;
try {
safegaurd = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 2000,
() => {
throw new Error(
'Timeout while waiting for desktop windows to map'
);
}
);
await Promise.all(displayPromises);
await Promise.all(allocatedPromises);
} catch (e) {
logError(e);
// if the windows fail to map, we should still proceed
// and poke the desktop windows later.
this.show();
}
if (safegaurd)
GLib.source_remove(safegaurd);
if (this._desktopManager.windowsPromiseResolve)
this._desktopManager.windowsPromiseResolve(true);
}
hide() {
this._desktops.forEach(desktop => desktop.hide());
this._hidden = true;
}
show() {
this._hidden = false;
this._desktops.forEach(desktop => {
desktop.show();
desktop.set_visible(true);
});
}
queue_draw() {
this._desktops.forEach(desktop => desktop.queue_draw());
}
toggleVisibility() {
if (this._hidden)
this.show();
else
this.hide();
}
toggleWidgetLayers() {
if (!this._Prefs.showDesktopWidgets)
return;
this._desktops.forEach(desktop => desktop.toggleWidgetLayer());
}
lowerWidgetLayers() {
this._desktops.forEach(desktop => desktop.lowerWidgetContainer());
}
raiseWidgetLayers() {
this._desktops.forEach(desktop => desktop.raiseWidgetContainer());
}
_registerWidgetLayerAction() {
const action = new Gio.SimpleAction({name: 'toggleWidgetLayer'});
action.connect('activate', () => {
this.toggleWidgetLayers();
});
action.set_enabled(DesktopWidgetCapability);
this._desktopManager.mainApp.add_action(action);
const lowerAction = new Gio.SimpleAction({name: 'lowerWidgetLayer'});
lowerAction.connect('activate', () => {
this.lowerWidgetLayers();
});
lowerAction.set_enabled(DesktopWidgetCapability);
this._desktopManager.mainApp.add_action(lowerAction);
const raiseAction = new Gio.SimpleAction({name: 'raiseWidgetLayer'});
raiseAction.connect('activate', () => {
this.raiseWidgetLayers();
});
raiseAction.set_enabled(DesktopWidgetCapability);
this._desktopManager.mainApp.add_action(raiseAction);
}
_getPreferredDisplayDesktop() {
if (!this._desktops.length)
return null;
if (this._desktops.length === 1)
return this._desktops[0];
if (!this._Prefs.showOnSecondaryMonitor &&
this._primaryMonitorIndex !== null) {
return this._desktops.filter(d => {
return d.monitorIndex === this._primaryMonitorIndex;
})[0];
}
const tempDesktops = this._desktops.filter((desktop, index) =>
index !== this._primaryMonitorIndex
);
if (this._desktops.length > 1) {
if (tempDesktops.length === 1)
return tempDesktops[0];
// Positional algorithms here depending on new geomertry
// of the placed monitors, -FIX ME- currently rudimentary
// only going by position in the index, not by placement geometry.
if (tempDesktops.length <= this._primaryMonitorIndex)
return tempDesktops[0];
else
return tempDesktops[tempDesktops.length - 1];
}
// Catch All if everything fails
return this._desktops[0];
}
destroyDesktops() {
this._desktops.forEach(desktop => desktop.destroy());
this._desktops = [];
}
onMutterSettingsChanged() {
for (let desktop of this._desktops)
desktop._premultiplied = this._premultiplied;
this.requestGeometryUpdate();
}
getClosestDesktop(itempositionX) {
let closestDesktop = null;
let closestDistance = 100000000000;
for (let desktop of this._desktops) {
if (!desktop.isAvailable())
continue;
const distance = desktop.getDistance(itempositionX);
if (distance < closestDistance) {
closestDesktop = desktop;
closestDistance = distance;
}
}
return closestDesktop;
}
get desktops() {
return this._desktops;
}
get desktopList() {
return this._desktopList;
}
get primaryMonitorIndex() {
return this._primaryMonitorIndex;
}
get primaryMonitor() {
return this._primaryScreen;
}
get primaryIndex() {
return this._primaryIndex;
}
get priorDesktopList() {
return this._priorDesktopList;
}
get priorPrimaryMonitorIndex() {
return this._priorPrimaryMonitorIndex;
}
get differentZooms() {
return this._differentZooms;
}
get preferredDisplayDesktop() {
return this._getPreferredDisplayDesktop();
}
};

View File

@@ -0,0 +1,12 @@
# This profile allows everything and only exists to give the
# application a name instead of having the label "unconfined"
abi <abi/4.0>,
include <tunables/global>
profile gtk4-ding /usr/share/gnome-shell/extensions/vesperos-taskbar@oxmc.me/ding/app/adw-ding.js flags=(unconfined) {
userns,
# Site-specific additions and overrides. See local/README for details.
include if exists <local/gtk4-ding>
}

View File

@@ -0,0 +1,12 @@
# This profile allows everything and only exists to give the
# application a name instead of having the label "unconfined"
abi <abi/4.0>,
include <tunables/global>
profile gtk4-ding @PREFIX@/share/gnome-shell/extensions/vesperos-taskbar@oxmc.me/ding/app/adw-ding.js flags=(unconfined) {
userns,
# Site-specific additions and overrides. See local/README for details.
include if exists <local/gtk4-ding>
}

View File

@@ -0,0 +1,8 @@
if prefix.startswith('/usr')
configure_file(input: 'gtk4-desktop-icons.in',
output: 'gtk4-desktop-icons',
configuration: {'PREFIX': prefix},
install: true,
install_dir: '/etc/apparmor.d')
endif

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/com/desktop/ding">
<file compressed="true" alias='icons/symbolic/actions/list-add-symbolic.svg'>icons/list-add-symbolic.svg</file>
<file compressed="true" alias='icons/symbolic/actions/view-grid-symbolic.svg'>icons/view-grid-symbolic.svg</file>
<file compressed="true" alias='icons/symbolic/emblems/emblem-system-symbolic.svg'>icons/emblem-system-symbolic.svg</file>
<file compressed="true" alias='icons/symbolic/ui/window-close-symbolic.svg'>icons/window-close-symbolic.svg</file>
<file compressed="true" alias='icons/symbolic/apps/prefs-desktop-symbolic.svg'>icons/prefs-desktop-symbolic.svg</file>
<file compressed="true" alias='icons/symbolic/apps/prefs-files-symbolic.svg'>icons/prefs-files-symbolic.svg</file>
<file compressed="true" alias='icons/symbolic/apps/prefs-files.svg'>icons/prefs-files.svg</file>
<file compressed="true" alias='icons/symbolic/apps/prefs-more-symbolic.svg'>icons/prefs-more-symbolic.svg</file>
<file compressed="true" alias='icons/symbolic/apps/prefs-desktop.svg'>icons/prefs-desktop.svg</file>
<file compressed="true" alias='icons/symbolic/apps/prefs-more.svg'>icons/prefs-more.svg</file>
<file compressed="true" alias='icons/symbolic/apps/prefs-tweaks-symbolic.svg'>icons/prefs-tweaks-symbolic.svg</file>
<file compressed="true" alias='icons/symbolic/apps/icon-emblem-locked.svg'>icons/emblem-readonly-symbolic.svg</file>
<file compressed="true" alias='icons/symbolic/apps/icon-emblem-symbolic-link.svg'>icons/emblem-symbolic-link-symbolic.svg</file>
<file compressed="true" alias='icons/symbolic/apps/icon-emblem-unreadable.svg'>icons/emblem-unwriteable-symbolic.svg</file>
<file compressed="true" alias='icons/symbolic/apps/icon-emblem-stack.svg'>icons/stack.svg</file>
<file compressed="true" alias='icons/symbolic/apps/ding-edit-delete-symbolic.svg'>icons/edit-delete-symbolic.svg</file>
<file compressed="true" alias='icons/symbolic/apps/edit-undo-symbolic.svg'>icons/edit-undo-symbolic.svg</file>
<file compressed="true" alias='icons/symbolic/apps/xapp-edit-symbolic.svg'>icons/xapp-edit-symbolic.svg</file>
<file compressed="true" alias='icons/symbolic/apps/window-pop-out-symbolic.svg'>icons/window-pop-out-symbolic.svg</file>
<file compressed="true" alias='icons/hicolor/scalable/apps/com.desktop.ding.svg'>icons/com.desktop.ding.svg</file>
<file compressed="true">stylesheet.css</file>
<file compressed="true">com.desktop.ding.desktop</file>
<file compressed="true" preprocess="xml-stripblanks">ui/ding-app-chooser.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/ding-widget-chooser.ui</file>
</gresource>
</gresources>

View File

@@ -0,0 +1,500 @@
[Desktop Entry]
Type=Application
Name[ar]=حاسوب مكتبي
Name[az]=Axtarış
Name[be]=Значкі працоўнага стала
Name[bg]=Икони на работния плот
Name[bn]=ডেস্কটপ আইকন
Name[ca]=Icones de l' escriptori
Name[cs]=Ikony plochy
Name[da]=Desktopikoner
Name[de]=Desktop-Symbole
Name[el]=Εικονίδια επιφάνειας εργασίας
Name[eo]=Desktop Icons
Name[es]=Iconos de escritorio
Name[et]=Töölaua ikoonid
Name[eu]=Mahaigaineko ikonoak
Name[fa]=Desktop Icons
Name[fi]=Työpöydän kuvakkeet
Name[fr]=Icônes de bureau
Name[ga]=Seirbhís do Chustaiméirí
Name[gl]=Desktop Icons
Name[he]=סמלים בשולחן העבודה
Name[hi]=डेस्कटॉप प्रतीक
Name[hr]=ikona na radnoj površini
Name[hu]=Asztali ikonok
Name[id]=Ikon Desktop
Name[it]=Icone sulla scrivania
Name[ja]=デスクトップアイコン
Name[ka]=სამუშაო დაფის ხატულა
Name[ko]=데스크탑 아이콘
Name[ky]=Десктоптун иконалары
Name[lv]=Darbvirsmas ikonas
Name[lt]=Darbastalio ženkliukai
Name[ms]=Ikon Desktop Ikon
Name[nb]=Skrivebordsikon
Name[nl]=Bureaubladpictogrammen
Name[pl]=Ikony pulpitu
Name[pt_BR]=Ícones da área de trabalho
Name[pt]=Ícones da área de trabalho
Name[ro]=Pictograme de birou
Name[ru]=Desktop Icons
Name[sk]=Ikony plochy
Name[sl]=Ikone namizja
Name[sq]=Ikonat e Desktopit
Name[sv]=Desktop Icons
Name[ta]=டெச்க்டாப் சின்னங்கள் அளவு
Name[tl]=Desktop Mga Icon
Name[tr]=Masaüstü Icons
Name[th]=ไอคอนของพื้นที่ทํางาน
Name[uk]=Настільні іконки
Name[ur]=فائلز
Name[zh-Hans]=桌面图标
Name[zh-Hant]=桌面圖示
Name=Desktop Icons
GenericName[ar]=حاسوب مكتبي
GenericName[az]=Axtarış
GenericName[be]=Значкі працоўнага стала
GenericName[bg]=Икони на работния плот
GenericName[bn]=ডেস্কটপ আইকন
GenericName[ca]=Icones de l' escriptori
GenericName[cs]=Ikony plochy
GenericName[da]=Desktopikoner
GenericName[de]=Desktop-Symbole
GenericName[el]=Εικονίδια επιφάνειας εργασίας
GenericName[eo]=Desktop Icons
GenericName[es]=Iconos de escritorio
GenericName[et]=Töölaua ikoonid
GenericName[eu]=Mahaigaineko ikonoak
GenericName[fa]=Desktop Icons
GenericName[fi]=Työpöydän kuvakkeet
GenericName[fr]=Icônes de bureau
GenericName[ga]=Seirbhís do Chustaiméirí
GenericName[gl]=Desktop Icons
GenericName[he]=סמלים בשולחן העבודה
GenericName[hi]=डेस्कटॉप प्रतीक
GenericName[hr]=ikona na radnoj površini
GenericName[hu]=Asztali ikonok
GenericName[id]=Ikon Desktop
GenericName[it]=Icone sulla scrivania
GenericName[ja]=デスクトップアイコン
GenericName[ka]=სამუშაო დაფის ხატულა
GenericName[ko]=데스크탑 아이콘
GenericName[ky]=Десктоптун иконалары
GenericName[lv]=Darbvirsmas ikonas
GenericName[lt]=Darbastalio ženkliukai
GenericName[ms]=Ikon Desktop Ikon
GenericName[nb]=Skrivebordsikon
GenericName[nl]=Bureaubladpictogrammen
GenericName[pl]=Ikony pulpitu
GenericName[pt_BR]=Ícones da área de trabalho
GenericName[pt]=Ícones da área de trabalho
GenericName[ro]=Pictograme de birou
GenericName[ru]=Desktop Icons
GenericName[sk]=Ikony plochy
GenericName[sl]=Ikone namizja
GenericName[sq]=Ikonat e Desktopit
GenericName[sv]=Desktop Icons
GenericName[ta]=டெச்க்டாப் சின்னங்கள் அளவு
GenericName[tl]=Desktop Mga Icon
GenericName[tr]=Masaüstü Icons
GenericName[th]=ไอคอนของพื้นที่ทํางาน
GenericName[uk]=Настільні іконки
GenericName[ur]=فائلز
GenericName[zh-Hans]=桌面图标
GenericName[zh-Hant]=桌面圖示
GenericName=Desktop Icons
Comment[ar]=جهاز تصوير على الحاسوب المكتبي
Comment[az]=GNOME masa üstü ekran icons
Comment[be]=Паказваць значкі на працоўным стале GNOME
Comment[bg]=Показване на иконите на работния плот на GNOME
Comment[bn]=ডেস্কটপে আইকন প্রদর্শন করা হবে
Comment[ca]=Mostra icones a l'escriptori GNOME
Comment[cs]=Zobrazovat ikony na ploše GNOME
Comment[da]=Vis ikoner på GNOME skrivebordet
Comment[de]=Symbole auf dem Desktop anzeigen
Comment[el]=Εμφάνιση εικονιδίων στην επιφάνεια εργασίας του GNOME
Comment[eo]=Apartaj ikonoj sur la GNOME-tablo
Comment[es]=Mostrar iconos en el escritorio GNOME
Comment[et]=Ikoonide näitamine GNOME töölaual
Comment[eu]=Bistaratu ikonoak GNOME mahaigainean
Comment[fa]=نمایش آیکون ها در دسکتاپ GNOME
Comment[fi]=Näytä kuvakkeet Gnomen työpöydällä
Comment[fr]=Affiche les icônes sur le bureau GNOME
Comment[ga]=Taispeáin deilbhíní ar an GNOME deisce
Comment[gl]=Mostrar iconas no escritorio GNOME
Comment[he]=תגיות: GNOME Desktop
Comment[hi]=GNOME डेस्कटॉप पर आइकन प्रदर्शित करें
Comment[hu]=Ikonok megjelenítése a GNOME asztalon
Comment[id]=Tampilkan ikon pada desktop GNOME
Comment[it]=Visualizza icone sulla scrivania
Comment[ja]=GNOMEデスクトップにアイコンを表示
Comment[ko]=GNOME 데스크톱에서 아이콘 표시
Comment[ky]=GNOME үстөлүндө иконаларды көрсөтүү
Comment[lv]=Rādīt GNOME darbvirsmas ikonas
Comment[lt]=GNOME darbastalyje rodyti ženkliukus
Comment[ms]=Ikon ikon pada desktop GNOME
Comment[nb]=Vis ikoner på GNOME-skrivebordet
Comment[nl]=Pictogrammen op het GNOME-bureaublad tonen
Comment[pl]=Wyświetlaj ikony na pulpicie GNOME
Comment[pt_BR]=Exibir ícones na área de trabalho do GNOME
Comment[pt]=Exibir ícones na área de trabalho do GNOME
Comment[ro]=Afișează pictograme pe biroul GNOME
Comment[ru]=Показывать значки на рабочем столе GNOME
Comment[sk]=Zobraziť ikony na pracovnej ploche GNOME
Comment[sl]=Prikaži ikone na namizju GNOME
Comment[sq]=Shfaq ikonat në desktopin GNOME
Comment[sv]=Visa ikoner på GNOME-skrivbordet
Comment[tl]=Ipakita ang mga larawan sa desktop ngrkton
Comment[tr]=GNOME masaüstü ikonları
Comment[th]=แสดงไอคอนบนพื้นที่ทํางานของ GNOME
Comment[uk]=Показати іконки на робочому столі GNOME
Comment[ur]=گنوم ڈیسک ٹاپ پر تصاویر دکھائیں
Comment[zh-Hans]=在 GNOME 桌面上显示图标
Comment[zh-Hant]=在 GNOME 桌面上顯示圖示
Comment=Display icons on the GNOME desktop
Keywords[ar]=حواسيب مكتبية؛ وأجهزة تصفية؛ وملفات؛ ومجلات؛ ومجلات؛ وزراعة؛ وهيد؛ والعرض؛ وقطع الطرق؛;
Keywords[az]=masa üstü; vasit;files;folders;manager;arrange;hide;show;starter;kıqraflı;
Keywords[be]=desktop;icons;files;folders;manager;arrange;hide;show;launcher;shortcuts;працоўны стол;файлы;папкі;ярлыкі;значкі;лаўнчар;
Keywords[bg]=настолен компютър;икони;файлове;папки;мениджър;аранжимент;скрий;шоу;прозорец;къси клавиши;
Keywords[bn]=ডেস্কটপ;আইকন;সম্প্রদায়;মনন;অবলন;হর্‍;হর্‌;ব্রম;ব্‌স;ব্‌;ব্‌স;ব্‌স;
Keywords[ca]=desktop; icons; files; folders;manager; adevinament; show;launcher; shortcuts;
Keywords[cs]=desktop; ikony; soubory; složky; manažer; zařídit; skrýt; show; spouštěč; zkratky;
Keywords[da]=desktop; ikoner; filer; mapper; manager; arrangere; skjule; show; lancerer; genveje;
Keywords[de]=desktop;icons;files;folders;manager;arrange;hide;show;launcher;shortcuts;
Keywords[el]=desktop; icons;files; folders; manager; arrange; hide; show; launcher; shortcuts;
Keywords[eo]=tablo; fasoj; falantoj; managro;arrange;hide; spektaklo;lanĉilo; mallongigoj;
Keywords[es]=escritorio; icons;files;folders;manager;arrange;hide;show;launcher;shortcuts;
Keywords[et]=töölaud; ikoonid; failid; kataloogid; haldaja; korralda; peita; näidata;launcher; lühilõiked;
Keywords[eu]=mahaigaina;ikonoak;karpetak; kudeatzailea;arrange;hide;show;launcher;shortcuts;
Keywords[fa]=دانلود بازی های رومیزی؛ فایل ها؛ پوشه ها؛manager;arrange;coat;show; launcher;
Keywords[fi]=työpöytä; ikonit; tiedostot; kansiot; manager;arrange;piilota;show;laukaisin; oikosulut;
Keywords[fr]=bureau;icônes;fichiers;dossiers;manager;dispositif;show;lanceur;shortcuts;
Keywords[ga]=deisce;icons; comhaid; fillteáin; bainisteoir; raon; hide; seó; seoladh;gearrtha;
Keywords[gl]=|data de nacemento = [[5 de setembro]] de [[1638]];
Keywords[he]=שולחן עבודה; מטבעות; פיות; מנדר; ;hide; show;launcher; shortcuts;
Keywords[hi]=डेस्कटॉप; आइकन; फ़ाइल्स; फ़ोल्डर्स; प्रबंधक; व्यवस्था; छिपाना; शो; लांचर; छोटा;
Keywords[hu]=asztali, ikonok, akták, mappák, kezelők, szervezők, bújócskák, bemutatók, kilövők, rövidítések;
Keywords[id]=desktop; ikon; berkas; folder; manajer; atur; sembunyikan; tampilkan; peluncur; jalan pintas;
Keywords[it]=desktop;icone;file;cartelle;gestore;ordine;nascondi;mostra;lanciatore;scorciatoie;
Keywords[ja]=デスクトップ;アイコン;ファイル;フォルダ;マネージャー;範囲;非表示;ショー;ランチャー;ショートカット;
Keywords[ko]=데스크탑; 아이콘; 파일; 폴더; 관리자; 배열; 숨기기; 쇼; 런처; 단축키;
Keywords[ky]=desktop;icons;files; папкалар; башкаруучу; жабуу; шоу; ишке киргизүүчү; кыска жолдор;
Keywords[lv]=darbvirsma;icons;files;folders;manager;arange;hide;show;palaišana;shortcuts;
Keywords[lt]=darbastalis; piktogramos; failai; aplankai; vadovas; organizuoti; slėpti; rodyti; paleidimo; spartieji;
Keywords[ms]=desktop;icons;files;folder;manager;arrange;hide;show;launcher;shortcuts;
Keywords[nb]=desktop;icons;files;folders;manager;arrange;hide;show;lancer;shortcuts;
Keywords[nl]=desktop;icons;files;folders;manager;arrange;hide;show;launcher;shortcuts;
Keywords[pl]=desktop;icons;files;folders;manager;arrange;hide;show;launcher;shortcuts;
Keywords[pt_BR]=desktop; icons; arquivos; pastas; gerenciador; organizar; esconder; mostrar; launcher; atalhos;
Keywords[pt]=desktop; icons; arquivos; pastas; gerenciador; organizar; esconder; mostrar; launcher; atalhos;
Keywords[ro]=desktop;icoane;file;dosare;manager;aranjat;ascunde;arată;lansare;scurtături;
Keywords[ru]=desktop;icons;files;folders;manager;arrange;hide;show;launcher;shortcuts;рабочий стол;файлы;значки;комбинации клавиш;папки;
Keywords[sk]=plocha;ikony;súbory; priečinky;správca;usporiadať;skryť;zobraziť;spúšťač;skratky;
Keywords[sl]=namizne; ikone; datoteke; mape; manager; arrange; skrinje; prikaži; zaganjalnik; kratke ure;
Keywords[sq]=j;
Keywords[sv]=desktop;icons;files;folders;manager;arrange;hide;show;launcher;shortcuts;
Keywords[tl]=mga desktop;icon; profile; pepper; manager;arrange;hide;show;launcher; shortcuts;
Keywords[tr]=masaüstü;icons;files;folders;manager;arrange;hide;show; başlatıcı; linecuts;
Keywords[th]=desktop; icons;files; files; manager; aarrange; hyd; show; launcher; sshuts;
Keywords[uk]=робочий стіл;кони;файли; мангери; ланч; ходовий; шоу; лущильник;коротки;
Keywords[ur]=ڈیسک ٹاپ؛ نقل و حمل؛ نقل و حمل؛ manager؛ arrange؛ hide؛ ظاہر؛ کھولاؤ؛;
Keywords[zh-Hans]=桌面; icons; 文件; 文件夹; 管理器; 排列; 隐藏; 显示; 发射器; 短剪;
Keywords[zh-Hant]=桌面; icons; 文件; 收件; 管理; 排列; 隱藏; 顯示; 發射; 短剪;
Keywords=desktop;icons;files;folders;manager;arrange;hide;show;launcher;shortcuts;
Icon=com.desktop.ding
Exec=gdbus call --session --dest org.gnome.Shell --object-path /org/gnome/Shell --method org.gnome.Shell.Extensions.EnableExtension gtk4-ding@smedius.gitlab.com
TryExec=/usr/bin/gdbus
Terminal=false
StartupNotify=false
OnlyShowIn=GNOME;
Categories=Utility;DesktopSettings;Settings;
X-Ding-SingleMainWindow=false
Actions=togglevisibility;enable;showshortcuts;preferences;disable;
[Desktop Action togglevisibility]
Name[ar]=Show<Hide
Name[az]=\sShow <>Hide
Name[be]=Паказаць<>Схаваць
Name[bg]=Показване < > Скриване
Name[bn]=লুকিয়ে রাখুন
Name[ca]=Mostra <> Amaga
Name[cs]=Zobrazit < > Skrýt
Name[da]=Vis < > Skjul
Name[de]=Gebräuchliche Bezeichnung
Name[el]=Εμφάνιση <>Απόκρυψη
Name[eo]=La jenaj paĝoj ligas al
Name[es]=Mostrar =
Name[et]=Näita <> Peida
Name[eu]=Erakutsi<>Ezkutatu
Name[fa]=<Hide
Name[fi]=Näytä <> Piilota
Name[fr]=Afficher <>Cacher
Name[ga]=Taispeáin níos mó
Name[gl]=Mostrar <Hide
Name[he]=תגית: Hide
Name[hi]=प्रदर्शन
Name[hr]=Prikaži / Sakrij
Name[hu]=< > Elrejtés
Name[id]=Tampilkan < > Sembunyikan
Name[it]=Mostra<>Nascondi
Name[ja]=ショー<>
Name[ka]=ჩვენება / დამალვა
Name[ko]=쇼<>머리
Name[ky]=Көрсөтүңүз <> Жашыруун
Name[lv]=Rādīt 'slēpt'
Name[lt]=Rodyti < > Slėpti
Name[ms]=Show<>Sembunyikan
Name[nb]=Vis<> Skjul
Name[nl]=Toon <>Verbergen
Name[oc]=Afichar/Amagar
Name[pl]=Pokaż<>Ukryj
Name[pt_BR]=Mostrar <>Esconder
Name[pt]=Mostrar <>Esconder
Name[ro]=Arată < > Ascunde
Name[ru]=Показать<>Скрыть
Name[sk]=Zobraziť<>Skryť
Name[sl]=Prikaži <>Skrij
Name[sq]=Shfaq<>fshi
Name[sv]=Visa <>Hide
Name[ta]=காட்டு/மறைக்க
Name[tl]=Ipakita<>Hide
Name[tr]=Show<>Hide
Name[th]=แสดง </ Hide
Name[uk]=Показати <>Приховати
Name[ur]=طےشدہ
Name[zh-Hans]=显示 {} 隐藏
Name[zh-Hant]=顯示 {} 隱藏
Name=Show<>Hide
Exec=gapplication action com.desktop.ding toggleVisibility
[Desktop Action enable]
Name[ar]=التمكين
Name[az]=Qeydiyyat
Name[be]=Уключыць
Name[bg]=Включване
Name[bn]=সক্রিয় করুন
Name[ca]=Habilita
Name[cs]=Povolit
Name[da]=Aktivér
Name[de]=Ermöglichen
Name[el]=Ενεργοποίηση
Name[eo]=Enable
Name[es]=Habilitación
Name[et]=Luba
Name[eu]=Gaitu
Name[fa]=گزینه Enable
Name[fi]=Käytä
Name[fr]=Activer
Name[fur]=Abilite
Name[ga]=Cumasaigh
Name[gl]=Habilitar
Name[he]=הפעלה
Name[hi]=सक्षम
Name[hr]=Omogući
Name[hu]=Engedélyezés
Name[id]=Aktifkan
Name[it]=Abilita
Name[ja]=アクセス
Name[ka]=ჩართვა
Name[ko]=이름 *
Name[ky]=Мүмкүн
Name[lv]=Ieslēgt
Name[lt]=Įjungti
Name[ms]=Hidupkan
Name[nb]=Slå på
Name[nl]=Inschakelen
Name[oc]=Activar
Name[pl]=Włącz
Name[pt_BR]=Activar
Name[pt]=Activar
Name[ro]=Activează
Name[ru]=Включить
Name[sk]=Zapnúť
Name[sl]=Omogoči
Name[sq]=Aktivo
Name[sv]=Aktivera
Name[ta]=இயக்கு
Name[tl]=Kaibig - ibig
Name[tr]=Enable
Name[th]=เปิด
Name[uk]=Увімкнути
Name[ur]=فعال کریں
Name[zh-Hans]=启用
Name[zh-Hant]=開啟
Name=Enable
Exec=gdbus call --session --dest org.gnome.Shell --object-path /org/gnome/Shell --method org.gnome.Shell.Extensions.EnableExtension gtk4-ding@smedius.gitlab.com
[Desktop Action showshortcuts]
Name[ar]=الطرق القصيرة
Name[az]=Tarix
Name[be]=Спалучэнні клавіш
Name[bg]=Бързи пътища
Name[bn]=শর্ট- কাট
Name[ca]=Dreceres
Name[cs]=Zkratky
Name[da]=Genveje
Name[de]=Tastenkürzel
Name[el]=Συντομεύσεις
Name[eo]=Mallongigoj
Name[es]=Accesos directos
Name[et]=Otseteed
Name[eu]=Lasterbideak
Name[fa]=میانبرها
Name[fi]=Pikanäppäimet
Name[fr]=Raccourcis
Name[fur]=Scurtis
Name[ga]=Gearáin agus Cur i bhFeidhm
Name[gl]=Shortcuts
Name[he]=קיצורי דרך
Name[hi]=शॉर्टकट
Name[hr]=Prečice
Name[hu]=Gyorsbillentyűk
Name[id]=Pintas
Name[it]=Scorciatoie
Name[ja]=ショートカット
Name[ka]=მალსახმობები
Name[ko]=바로가기
Name[ky]=Кыска жолдор
Name[lv]=Saīsnes
Name[lt]=Spartieji klavišai
Name[ms]=Pintasan
Name[nb]=Snarveier
Name[nl]=Sneltoetsen
Name[oc]=Acorchis
Name[pl]=Skróty klawiszowe
Name[pt_BR]=Atalhos
Name[pt]=Atalhos
Name[ro]=Scurtături
Name[ru]=Комбинации клавиш
Name[sk]=Skratky
Name[sl]=Bližnjice
Name[sq]=Kombinim përshpejtues
Name[sv]=Genvägar
Name[ta]=குறுக்குவழிகள்
Name[tl]=Mga Maikling Pagputol
Name[tr]=Kısayollar
Name[th]=ปุ่มพิมพ์ลัด
Name[uk]=Шорти
Name[ur]=مختصر
Name[zh-Hans]=快捷键
Name[zh-Hant]=捷徑
Name=Shortcuts
Exec=gapplication action com.desktop.ding showShortcutViewer
[Desktop Action preferences]
Name[ar]=الأفضليات
Name[az]=Qeydiyyat
Name[be]=Параметры
Name[bg]=Преференция
Name[bn]=পছন্দ
Name[ca]=Preferències
Name[cs]=Předvolby
Name[da]=Indstillinger
Name[de]=Vorlieben
Name[el]=Προτιμήσεις
Name[eo]=Preferoj
Name[es]=Preferencias
Name[et]=Eelistused
Name[eu]=Hobespenak
Name[fa]=ترجیحات
Name[fi]=Asetukset
Name[fr]=Préférences
Name[fur]=Preferencis
Name[ga]=Tosaíochtaí
Name[gl]=Preferencias
Name[he]=העדפות
Name[hi]=प्राथमिकता
Name[hr]=Postavke
Name[hu]=Előirányzatok
Name[id]=Preferensi
Name[it]=Preferenze
Name[ja]=リファレンス
Name[ka]=მორგება
Name[ko]=옵션 정보
Name[ky]=Преференциялар
Name[lv]=Iestatījumi
Name[lt]=Nustatymai
Name[ms]=Keutamaan Anjuta
Name[nb]=Innstillinger
Name[nl]=Voorkeuren
Name[oc]=Preferéncias
Name[pl]=Preferencje
Name[pt_BR]=Preferências
Name[pt]=Preferências
Name[ro]=Preferințe
Name[ru]=Параметры
Name[sk]=Predvoľby
Name[sl]=Lastnosti
Name[sq]=Preferimet
Name[sv]=Föreställningar
Name[ta]=விருப்பத்தேர்வுகள்
Name[tl]=Mga kagustuhan
Name[tr]=Tercihler
Name[th]=ปรับแต่ง
Name[uk]=Налаштування
Name[ur]=ترجیحات
Name[zh-Hans]=首选项
Name[zh-Hant]=首选项
Name=Preferences
Exec=gapplication action com.desktop.ding changeDesktopIconSettings
[Desktop Action disable]
Name[ar]=العجز
Name[az]=Qeydiyyat
Name[be]=Адключыць
Name[bg]=Изключване
Name[bn]=নিষ্ক্রিয়
Name[ca]=Deshabilita
Name[cs]=Zakázat
Name[da]=Deaktivér
Name[de]=Nicht verfügbar
Name[el]=Απενεργοποίηση
Name[eo]=Distingebla
Name[es]=Inhabilitación
Name[et]=Keela
Name[eu]=Desgaitu
Name[fa]=Disable
Name[fi]=Poista käytöstä
Name[fr]=Désactiver
Name[fur]=Disabilitât
Name[ga]=Díroghnaigh gach rud
Name[gl]=Disable
Name[he]=אכזבה
Name[hi]=अक्षम
Name[hr]=Deaktiviraj
Name[hu]=Kikapcsolás
Name[id]=Matikan
Name[it]=Disattiva
Name[ja]=免責事項
Name[ka]=გამორთვა
Name[ko]=기타 제품
Name[ky]=Майыптар
Name[lv]=Atslēgt
Name[lt]=Išjungti
Name[ms]=Matikan
Name[nb]=Slå av
Name[nl]=Uitschakelen
Name[oc]=Disable
Name[pl]=Wyłącz
Name[pt_BR]=Desactivar
Name[pt]=Desactivar
Name[ro]=Dezactivează
Name[ru]=Выключить
Name[sk]=Vypnúť
Name[sl]=Onemogoči
Name[sq]=Jo aktiv
Name[sv]=Inaktivera
Name[ta]=முடக்கு
Name[tl]=Hindi Kaya
Name[tr]=Engelliler
Name[th]=ปิดการใช้งาน
Name[uk]=Вимкнути
Name[ur]=منسوخ کریں
Name[zh-Hans]=禁用
Name[zh-Hant]=禁用
Name=Disable
Exec=gdbus call --session --dest org.gnome.Shell --object-path /org/gnome/Shell --method org.gnome.Shell.Extensions.DisableExtension gtk4-ding@smedius.gitlab.com

View File

@@ -0,0 +1,38 @@
[Desktop Entry]
Type=Application
Name=Desktop Icons
GenericName=Desktop Icons
Comment=Display icons on the GNOME desktop
Keywords=desktop;icons;files;folders;manager;arrange;hide;show;launcher;shortcuts;
Icon=com.desktop.ding
Exec=gdbus call --session --dest org.gnome.Shell --object-path /org/gnome/Shell --method org.gnome.Shell.Extensions.EnableExtension gtk4-ding@smedius.gitlab.com
TryExec=/usr/bin/gdbus
Terminal=false
StartupNotify=false
OnlyShowIn=GNOME;
Categories=Utility;DesktopSettings;Settings;
X-Ding-SingleMainWindow=false
Actions=togglevisibility;enable;showshortcuts;preferences;disable;
[Desktop Action togglevisibility]
Name=Show<>Hide
Exec=gapplication action com.desktop.ding toggleVisibility
[Desktop Action enable]
Name=Enable
Exec=gdbus call --session --dest org.gnome.Shell --object-path /org/gnome/Shell --method org.gnome.Shell.Extensions.EnableExtension gtk4-ding@smedius.gitlab.com
[Desktop Action showshortcuts]
Name=Shortcuts
Exec=gapplication action com.desktop.ding showShortcutViewer
[Desktop Action preferences]
Name=Preferences
Exec=gapplication action com.desktop.ding changeDesktopIconSettings
[Desktop Action disable]
Name=Disable
Exec=gdbus call --session --dest org.gnome.Shell --object-path /org/gnome/Shell --method org.gnome.Shell.Extensions.DisableExtension gtk4-ding@smedius.gitlab.com

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="128px" viewBox="0 0 128 128" width="128px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<clipPath id="a">
<path d="m 41 70 h 46 v 52 h -46 z m 0 0"/>
</clipPath>
<clipPath id="b">
<path d="m 40.402344 54.339844 h 47.421875 v 68.484375 h -47.421875 z m 35.910156 42.734375 c 0 -6.742188 -5.453125 -12.210938 -12.183594 -12.210938 c -6.726562 0 -12.183594 5.46875 -12.183594 12.210938 c 0 6.742187 5.457032 12.210937 12.183594 12.210937 c 6.730469 0 12.183594 -5.46875 12.183594 -12.210937 z m 0 0"/>
</clipPath>
<linearGradient id="c" gradientTransform="matrix(0.164687 0 0 0.165054 -25.111408 -9.834853)" gradientUnits="userSpaceOnUse" x1="403.496033" x2="678.908813" y1="793.565552" y2="793.565552">
<stop offset="0" stop-color="#9a9996"/>
<stop offset="0.0414257" stop-color="#c0bfbc"/>
<stop offset="0.0815191" stop-color="#9a9996"/>
<stop offset="0.899024" stop-color="#77767b"/>
<stop offset="0.952865" stop-color="#c0bfbc"/>
<stop offset="1" stop-color="#77767b"/>
</linearGradient>
<clipPath id="d">
<path d="m 41 68 h 46 v 52 h -46 z m 0 0"/>
</clipPath>
<clipPath id="e">
<path d="m 40.402344 54.339844 h 47.421875 v 68.484375 h -47.421875 z m 35.910156 42.734375 c 0 -6.742188 -5.453125 -12.210938 -12.183594 -12.210938 c -6.726562 0 -12.183594 5.46875 -12.183594 12.210938 c 0 6.742187 5.457032 12.210937 12.183594 12.210937 c 6.730469 0 12.183594 -5.46875 12.183594 -12.210937 z m 0 0"/>
</clipPath>
<clipPath id="f">
<path d="m 41 55 h 46 v 54 h -46 z m 0 0"/>
</clipPath>
<clipPath id="g">
<path d="m 40.402344 54.339844 h 47.421875 v 68.484375 h -47.421875 z m 35.910156 42.734375 c 0 -6.742188 -5.453125 -12.210938 -12.183594 -12.210938 c -6.726562 0 -12.183594 5.46875 -12.183594 12.210938 c 0 6.742187 5.457032 12.210937 12.183594 12.210937 c 6.730469 0 12.183594 -5.46875 12.183594 -12.210937 z m 0 0"/>
</clipPath>
<linearGradient id="h" gradientTransform="matrix(0.299808 0 0 0.300475 -270.58579 35.155126)" gradientUnits="userSpaceOnUse" x1="928.741516" x2="1302.490479" y1="216.638611" y2="216.638611">
<stop offset="0" stop-color="#3d3846"/>
<stop offset="0.0279595" stop-color="#79718e"/>
<stop offset="0.0654033" stop-color="#4e475a"/>
<stop offset="0.938181" stop-color="#716881"/>
<stop offset="0.971878" stop-color="#847a96"/>
<stop offset="1" stop-color="#3d3846"/>
</linearGradient>
<linearGradient id="i" gradientTransform="matrix(0.45451 0 0 0.455522 -1210.292114 616.172607)" gradientUnits="userSpaceOnUse" x1="2704.463135" x2="2868.168457" y1="-1148.187378" y2="-1311.529175">
<stop offset="0" stop-color="#1c71d8"/>
<stop offset="1" stop-color="#62a0ea"/>
</linearGradient>
<g clip-path="url(#a)">
<g clip-path="url(#b)">
<path d="m 44.664062 70.726562 h 38.898438 c 1.902344 0 3.4375 1.523438 3.4375 3.414063 v 44.445313 c 0 1.890624 -1.535156 3.414062 -3.4375 3.414062 h -38.898438 c -1.902343 0 -3.4375 -1.523438 -3.4375 -3.414062 v -44.445313 c 0 -1.890625 1.535157 -3.414063 3.4375 -3.414063 z m 0 0" fill="url(#c)"/>
</g>
</g>
<g clip-path="url(#d)">
<g clip-path="url(#e)">
<path d="m 44.664062 68.726562 h 38.898438 c 1.902344 0 3.4375 1.523438 3.4375 3.414063 v 44.445313 c 0 1.890624 -1.535156 3.414062 -3.4375 3.414062 h -38.898438 c -1.902343 0 -3.4375 -1.523438 -3.4375 -3.414062 v -44.445313 c 0 -1.890625 1.535157 -3.414063 3.4375 -3.414063 z m 0 0" fill="#77767b"/>
</g>
</g>
<g clip-path="url(#f)">
<g clip-path="url(#g)">
<path d="m 41.226562 55.164062 h 45.773438 v 53.414063 h -45.773438 z m 0 0" fill="#434348" fill-opacity="0.509804"/>
</g>
</g>
<path d="m 120 21.613281 v 72.773438 c 0 5.308593 -4.296875 9.613281 -9.59375 9.613281 h -92.8125 c -5.296875 0 -9.59375 -4.304688 -9.59375 -9.613281 v -72.773438 c 0 -5.308593 4.296875 -9.613281 9.59375 -9.613281 h 92.8125 c 5.296875 0 9.59375 4.304688 9.59375 9.613281 z m 0 0" fill="url(#h)"/>
<path d="m 120 21.613281 v 68.773438 c 0 5.308593 -4.296875 9.613281 -9.59375 9.613281 h -92.8125 c -5.296875 0 -9.59375 -4.304688 -9.59375 -9.613281 v -68.773438 c 0 -5.308593 4.296875 -9.613281 9.59375 -9.613281 h 92.8125 c 5.296875 0 9.59375 4.304688 9.59375 9.613281 z m 0 0" fill="#241f31"/>
<path d="m 17.378906 14 h 93.242188 c 4.074218 0 7.378906 3.3125 7.378906 7.394531 v 69.210938 c 0 4.082031 -3.304688 7.394531 -7.378906 7.394531 h -93.242188 c -4.074218 0 -7.378906 -3.3125 -7.378906 -7.394531 v -69.210938 c 0 -4.082031 3.304688 -7.394531 7.378906 -7.394531 z m 0 0" fill="url(#i)"/>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 8 0 c -4.410156 0 -8 3.589844 -8 8 s 3.589844 8 8 8 s 8 -3.589844 8 -8 s -3.589844 -8 -8 -8 z m 0 2 c 3.332031 0 6 2.667969 6 6 s -2.667969 6 -6 6 s -6 -2.667969 -6 -6 s 2.667969 -6 6 -6 z m -2.03125 2.96875 c -0.265625 0 -0.519531 0.105469 -0.707031 0.292969 c -0.390625 0.390625 -0.390625 1.023437 0 1.414062 l 1.292969 1.292969 l -1.292969 1.292969 c -0.390625 0.390625 -0.390625 1.023437 0 1.414062 s 1.023437 0.390625 1.414062 0 l 1.292969 -1.292969 l 1.292969 1.292969 c 0.390625 0.390625 1.023437 0.390625 1.414062 0 s 0.390625 -1.023437 0 -1.414062 l -1.292969 -1.292969 l 1.292969 -1.292969 c 0.390625 -0.390625 0.390625 -1.023437 0 -1.414062 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 s -0.519531 0.105469 -0.707031 0.292969 l -1.292969 1.292969 l -1.292969 -1.292969 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 z m 0 0" fill="#2e3436"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 5 2 c -0.265625 0 -0.519531 0.105469 -0.707031 0.292969 l -4 4 c -0.3906252 0.390625 -0.3906252 1.023437 0 1.414062 l 4 4 c 0.390625 0.390625 1.023437 0.390625 1.414062 0 s 0.390625 -1.023437 0 -1.414062 l -2.292969 -2.292969 h 8.585938 c 1.117188 0 2 0.882812 2 2 s -0.882812 2 -2 2 c -0.550781 0 -1 0.449219 -1 1 s 0.449219 1 1 1 c 2.199219 0 4 -1.800781 4 -4 s -1.800781 -4 -4 -4 h -8.585938 l 2.292969 -2.292969 c 0.390625 -0.390625 0.390625 -1.023437 0 -1.414062 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 z m 0 0" fill="#2e3436"/>
</svg>

After

Width:  |  Height:  |  Size: 701 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 3 -0.0117188 c -1.660156 0 -3 1.3398438 -3 2.9999998 v 10 c 0 1.664063 1.339844 3 3 3 h 10 c 1.660156 0 3 -1.335937 3 -3 v -10 c 0 -1.660156 -1.339844 -2.9999998 -3 -2.9999998 z m 4.964844 2.9999998 h 0.164062 c 1.617188 0 2.917969 1.300781 2.917969 2.917969 v 1.082031 c 0.5625 0.007813 1.007813 0.472657 1 1.03125 v 2.9375 c 0 0.570313 -0.460937 1.03125 -1.03125 1.03125 h -5.9375 c -0.570313 0 -1.03125 -0.460937 -1.03125 -1.03125 v -2.9375 c -0.007813 -0.558593 0.4375 -1.023437 1 -1.03125 v -1.082031 c 0 -1.617188 1.300781 -2.917969 2.917969 -2.917969 z m 0.082031 2 c -0.554687 0 -1 0.445313 -1 1 v 1 h 2 v -1 c 0 -0.554687 -0.445313 -1 -1 -1 z m 0 0" fill="#222222"/>
</svg>

After

Width:  |  Height:  |  Size: 825 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 3 -0.0117188 c -1.660156 0 -3 1.3398438 -3 2.9999998 v 10 c 0 1.664063 1.339844 3 3 3 h 10 c 1.660156 0 3 -1.335937 3 -3 v -10 c 0 -1.660156 -1.339844 -2.9999998 -3 -2.9999998 z m 4.046875 2.9999998 h 5 c 0.027344 0.003907 0.058594 0.007813 0.085937 0.011719 c 0.039063 0.003906 0.082032 0.011719 0.121094 0.019531 c 0.054688 0.011719 0.105469 0.027344 0.15625 0.046875 c 0.039063 0.015625 0.078125 0.03125 0.113282 0.050782 c 0.066406 0.035156 0.128906 0.078124 0.183593 0.128906 c 0.015625 0.007812 0.03125 0.019531 0.046875 0.035156 c 0.0625 0.0625 0.113282 0.132812 0.15625 0.207031 c 0.046875 0.078125 0.078125 0.15625 0.101563 0.242188 c 0.023437 0.085937 0.035156 0.171875 0.035156 0.257812 v 5 c 0 0.554688 -0.449219 1 -1 1 s -1 -0.445312 -1 -1 v -2.585937 l -6.292969 6.292968 c -0.390625 0.390626 -1.023437 0.390626 -1.414062 0 c -0.390625 -0.390624 -0.390625 -1.023437 0 -1.414062 l 6.292968 -6.292969 h -2.585937 c -0.550781 0 -1 -0.449219 -1 -1 s 0.449219 -1 1 -1 z m 0 0" fill="#222222"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" version="1.1">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text { color:#444444; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 6.25,1 6.1,2.84 A 5.5,5.5 0 0 0 4.49,3.77 L 2.81,2.98 1.06,6.02 2.58,7.07 A 5.5,5.5 0 0 0 2.5,8 5.5,5.5 0 0 0 2.58,8.93 L 1.06,9.98 2.81,13.02 4.48,12.23 A 5.5,5.5 0 0 0 6.1,13.15 L 6.25,15 H 9.75 L 9.9,13.16 A 5.5,5.5 0 0 0 11.51,12.23 L 13.19,13.02 14.94,9.98 13.42,8.93 A 5.5,5.5 0 0 0 13.5,8 5.5,5.5 0 0 0 13.42,7.07 L 14.94,6.02 13.19,2.98 11.52,3.77 A 5.5,5.5 0 0 0 9.9,2.85 L 9.75,1 Z M 8,6 A 2,2 0 0 1 10,8 2,2 0 0 1 8,10 2,2 0 0 1 6,8 2,2 0 0 1 8,6 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 907 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 3 -0.0117188 c -1.660156 0 -3 1.3398438 -3 2.9999998 v 10 c 0 1.664063 1.339844 3 3 3 h 10 c 1.660156 0 3 -1.335937 3 -3 v -10 c 0 -1.660156 -1.339844 -2.9999998 -3 -2.9999998 z m 1 2.9999998 c 0.265625 0 0.519531 0.105469 0.707031 0.292969 l 3.292969 3.292969 l 3.292969 -3.292969 c 0.1875 -0.1875 0.441406 -0.292969 0.707031 -0.292969 s 0.519531 0.105469 0.707031 0.292969 c 0.390625 0.390625 0.390625 1.023438 0 1.414062 l -3.292969 3.292969 l 3.292969 3.292969 c 0.390625 0.390625 0.390625 1.023438 0 1.414062 c -0.390625 0.390626 -1.023437 0.390626 -1.414062 0 l -3.292969 -3.292968 l -3.292969 3.292968 c -0.390625 0.390626 -1.023437 0.390626 -1.414062 0 c -0.390625 -0.390624 -0.390625 -1.023437 0 -1.414062 l 3.292969 -3.292969 l -3.292969 -3.292969 c -0.390625 -0.390624 -0.390625 -1.023437 0 -1.414062 c 0.1875 -0.1875 0.441406 -0.292969 0.707031 -0.292969 z m 0 0" fill="#222222"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 7 1 v 6 h -6 v 2 h 6 v 6 h 2 v -6 h 6 v -2 h -6 v -6 z m 0 0" fill="#2e3436"/>
</svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@@ -0,0 +1,11 @@
icons_dir = join_paths(datadir, 'icons', 'hicolor', 'scalable', 'apps')
install_data([
'com.desktop.ding.svg',
],
install_dir: icons_dir
)
gnome.post_install(
gtk_update_icon_cache: true,
)

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" version="1.1">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text { color:#444444; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 1.8003904,2 C 1.3571904,2 0.9996092,2.3464547 0.9996092,2.7773437 V 4 H 15.000391 V 2.7773437 C 15.000391,2.3464548 14.64281,2 14.199609,2 Z M 0.9996092,5 v 8.222656 C 0.9996092,13.653545 1.3571904,14 1.8003904,14 H 3.9999999 V 12.794922 C 3.9999999,12.354315 4.3575811,12 4.8007811,12 H 11.199219 C 11.642419,12 12,12.354315 12,12.794922 V 14 h 2.199609 c 0.443201,0 0.754351,-0.348964 0.800782,-0.777344 V 5 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 859 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" version="1.1">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text { color:#444444; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 1.8003904,2 C 1.3571904,2 0.9996092,2.3464547 0.9996092,2.7773437 V 4 H 15.000391 V 2.7773437 C 15.000391,2.3464548 14.64281,2 14.199609,2 Z M 0.9996092,5 v 8.222656 C 0.9996092,13.653545 1.3571904,14 1.8003904,14 H 3.9999999 V 12.794922 C 3.9999999,12.354315 4.3575811,12 4.8007811,12 H 11.199219 C 11.642419,12 12,12.354315 12,12.794922 V 14 h 2.199609 c 0.443201,0 0.754351,-0.348964 0.800782,-0.777344 V 5 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 859 B

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
version="1.1"
id="svg7"
sodipodi:docname="prefs-files-smbolic.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview9"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="58.4375"
inkscape:cx="5.2278075"
inkscape:cy="8.0513369"
inkscape:window-width="2192"
inkscape:window-height="1164"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg7" />
<defs
id="defs3">
<style
id="Current Color Scheme"
type="text/css">
.ColorScheme-Text { color:#444444; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
</style>
</defs>
<path
style="fill:#000000;fill-opacity:1"
class="ColorScheme-Text"
d="m 1,2 v 11 c 0,0 0,1 1,1 h 12 c 0,0 1,0 1,-1 V 4 C 15,3 14,3 14,3 H 9 L 7,1 H 2 C 2,1 1,1 1,2 Z"
id="path5"
inkscape:transform-center-x="-0.0012032086"
inkscape:transform-center-y="0.10835561" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text { color:#444444; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="m 1,2 0,11 c 0,0 0,1 1,1 l 12,0 c 0,0 1,0 1,-1 L 15,4 C 15,3 14,3 14,3 L 9,3 7,1 2,1 C 2,1 1,1 1,2 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 532 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" version="1.1">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text { color:#444444; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 9,12.5 A 1.5,1.5 0 0 1 7.5,14 1.5,1.5 0 0 1 6,12.5 1.5,1.5 0 0 1 7.5,11 1.5,1.5 0 0 1 9,12.5 Z M 9,8.5 A 1.5,1.5 0 0 1 7.5,10 1.5,1.5 0 0 1 6,8.5 1.5,1.5 0 0 1 7.5,7 1.5,1.5 0 0 1 9,8.5 Z M 9,4.5 A 1.5,1.5 0 0 1 7.5,6 1.5,1.5 0 0 1 6,4.5 1.5,1.5 0 0 1 7.5,3 1.5,1.5 0 0 1 9,4.5 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 727 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" version="1.1">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text { color:#444444; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 9,12.5 A 1.5,1.5 0 0 1 7.5,14 1.5,1.5 0 0 1 6,12.5 1.5,1.5 0 0 1 7.5,11 1.5,1.5 0 0 1 9,12.5 Z M 9,8.5 A 1.5,1.5 0 0 1 7.5,10 1.5,1.5 0 0 1 6,8.5 1.5,1.5 0 0 1 7.5,7 1.5,1.5 0 0 1 9,8.5 Z M 9,4.5 A 1.5,1.5 0 0 1 7.5,6 1.5,1.5 0 0 1 6,4.5 1.5,1.5 0 0 1 7.5,3 1.5,1.5 0 0 1 9,4.5 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 727 B

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
enable-background="new"
version="1.1"
id="svg16"
sodipodi:docname="general-symbolic.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs20" />
<sodipodi:namedview
id="namedview18"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="24.174578"
inkscape:cx="13.98163"
inkscape:cy="8.3765683"
inkscape:window-width="1500"
inkscape:window-height="963"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g14" />
<g
fill="#363636"
id="g14"
transform="rotate(90,7.75,8.25)">
<path
d="m 2.9500144,1 c -0.277,0 -0.5,0.223 -0.5,0.5 v 6.5547 a 2.5,2.5 0 0 1 0.5,-0.054688 2.5,2.5 0 0 1 0.5,0.050781 v -6.5508 c 0,-0.277 -0.223,-0.5 -0.5,-0.5 z m 0.5,11.945 a 2.5,2.5 0 0 1 -0.5,0.05469 2.5,2.5 0 0 1 -0.5,-0.05078 v 1.5508 c 0,0.277 0.223,0.5 0.5,0.5 0.277,0 0.5,-0.223 0.5,-0.5 v -1.5547 z"
id="path2" />
<path
d="M 7.5,1 C 7.223,1 7,1.223 7,1.5 V 3.0547 A 2.5,2.5 0 0 1 7.5,3.000012 2.5,2.5 0 0 1 8,3.050793 v -1.5508 c 0,-0.277 -0.223,-0.5 -0.5,-0.5 z M 8,7.9453 A 2.5,2.5 0 0 1 7.5,7.999988 2.5,2.5 0 0 1 7,7.949207 v 6.5508 c 0,0.277 0.223,0.5 0.5,0.5 0.277,0 0.5,-0.223 0.5,-0.5 v -6.5547 z"
id="path4" />
<path
d="m 12.049986,1.0001482 c -0.277,0 -0.5,0.223 -0.5,0.5 v 6.5547 a 2.5,2.5 0 0 1 0.5,-0.054688 2.5,2.5 0 0 1 0.5,0.050781 v -6.5508 c 0,-0.277 -0.223,-0.5 -0.5,-0.5 z m 0.5,11.9450008 a 2.5,2.5 0 0 1 -0.5,0.05469 2.5,2.5 0 0 1 -0.5,-0.05078 v 1.5508 c 0,0.276999 0.223,0.5 0.5,0.5 0.277,0 0.5,-0.223001 0.5,-0.5 v -1.5547 z"
id="path6" />
<circle
cx="2.9500144"
cy="10.5"
id="circle8"
r="1.5" />
<circle
cx="7.5"
cy="5.5"
r="1.5"
id="circle10" />
<circle
cx="12.049986"
cy="10.500149"
id="circle12"
r="1.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

49
ding/data/icons/stack.svg Normal file
View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16"
height="16"
viewBox="0 0 16 16"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1">
<rect
style="fill:#000000;stroke:#000000;stroke-width:0.968838;stroke-dasharray:none;stroke-opacity:1"
id="rect19"
width="15.058167"
height="15.00266"
x="0.49173185"
y="0.49173179" />
<g
id="g20"
transform="translate(0.01387684,-0.15264527)">
<rect
style="fill:#ffffff;stroke:#000000;stroke-width:0.929;stroke-dasharray:none;stroke-opacity:1"
id="rect20-6"
width="13.946227"
height="3.6773634"
x="1.1037022"
y="11.354352" />
<rect
style="fill:#ffffff;stroke:#000000;stroke-width:0.929;stroke-dasharray:none;stroke-opacity:1"
id="rect20-7"
width="13.946227"
height="3.6773634"
x="1.1453327"
y="6.2988448" />
<rect
style="fill:#ffffff;stroke:#000000;stroke-width:0.929;stroke-dasharray:none;stroke-opacity:1"
id="rect20"
width="13.946227"
height="3.6773634"
x="1.0962706"
y="1.4431916" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1771270234822" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1578" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M426.666667 170.666667v170.666666h170.666666V170.666667h-170.666666m256 0v170.666666h170.666666V170.666667h-170.666666m0 256v170.666666h170.666666v-170.666666h-170.666666m0 256v170.666666h170.666666v-170.666666h-170.666666m-85.333334 170.666666v-170.666666h-170.666666v170.666666h170.666666m-256 0v-170.666666H170.666667v170.666666h170.666666m0-256v-170.666666H170.666667v170.666666h170.666666m0-256V170.666667H170.666667v170.666666h170.666666m85.333334 256h170.666666v-170.666666h-170.666666v170.666666M170.666667 85.333333h682.666666a85.333333 85.333333 0 0 1 85.333334 85.333334v682.666666a85.333333 85.333333 0 0 1-85.333334 85.333334H170.666667c-46.08 0-85.333333-38.4-85.333334-85.333334V170.666667a85.333333 85.333333 0 0 1 85.333334-85.333334z" fill="#2E3436" p-id="1579"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 4 4 h 1 h 0.03125 c 0.253906 0.011719 0.511719 0.128906 0.6875 0.3125 l 2.28125 2.28125 l 2.3125 -2.28125 c 0.265625 -0.230469 0.445312 -0.304688 0.6875 -0.3125 h 1 v 1 c 0 0.285156 -0.035156 0.550781 -0.25 0.75 l -2.28125 2.28125 l 2.25 2.25 c 0.1875 0.1875 0.28125 0.453125 0.28125 0.71875 v 1 h -1 c -0.265625 0 -0.53125 -0.09375 -0.71875 -0.28125 l -2.28125 -2.28125 l -2.28125 2.28125 c -0.1875 0.1875 -0.453125 0.28125 -0.71875 0.28125 h -1 v -1 c 0 -0.265625 0.09375 -0.53125 0.28125 -0.71875 l 2.28125 -2.25 l -2.28125 -2.28125 c -0.210938 -0.195312 -0.304688 -0.46875 -0.28125 -0.75 z m 0 0" fill="#2e3436"/>
</svg>

After

Width:  |  Height:  |  Size: 767 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" version="1.1">
<defs>
<style id="current-color-scheme" type="text/css">
.ColorScheme-Text { color:#444444; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
</style>
</defs>
<path style="fill:currentColor" class="ColorScheme-Text" d="M 8.8,14.62 13,10.41 V 14 h 2 V 7 H 8 v 2 h 3.59 L 7.38,13.2 C 7.15,13.39 7,13.698 7,14 v 1 h 1 c 0.3037,0 0.61,-0.14 0.8,-0.38 z"/>
</svg>

After

Width:  |  Height:  |  Size: 575 B

View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg7384"
height="16"
width="16"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="list-edit-symbolic.svg">
<defs
id="defs9" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1888"
inkscape:window-height="999"
id="namedview7"
showgrid="true"
showguides="false"
inkscape:zoom="29.5"
inkscape:cx="1.791146"
inkscape:cy="12.696566"
inkscape:window-x="32"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="g4156"
inkscape:snap-global="true"
inkscape:snap-bbox="true"
inkscape:bbox-nodes="true"
inkscape:snap-object-midpoints="true"
inkscape:object-nodes="true">
<inkscape:grid
type="xygrid"
id="grid4142" />
</sodipodi:namedview>
<metadata
id="metadata90">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>Gnome Symbolic Icon Theme</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<title
id="title9167">Gnome Symbolic Icon Theme</title>
<g
id="layer12"
transform="translate(-40 -726)">
<g
id="g4141"
transform="matrix(1,0,0,1.49985,0.14644561,-367.46435)">
<g
id="g4146"
transform="translate(-0.15531039,-0.2500338)">
<g
id="g4151"
transform="matrix(0.70710678,-0.47145167,1.0605541,0.70710678,-765.05661,237.80289)">
<g
id="g4156"
transform="matrix(0.70710679,0.47145167,-1.0605541,0.70710678,793.33489,192.28516)">
<path
inkscape:connector-curvature="0"
d="m 41.16418,738.21673 9,-6.0006 c 1,0 2,0.66674 2,1.33347 l -9,6.0006 -2,0 z"
id="path2273-6-2"
sodipodi:nodetypes="cccccc"
style="fill:#bebebe;fill-opacity:1;fill-rule:evenodd;stroke:none" />
<path
inkscape:connector-curvature="0"
d="m 51.16418,731.5494 c 1,0 2,0.66674 2,1.33347 l 2,-1.33347 c 0,-0.66674 -0.75185,-1.33347 -2,-1.33347 z"
id="path4113-1-6-3"
sodipodi:nodetypes="ccccc"
style="display:inline;overflow:visible;visibility:visible;fill:#bebebe;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;enable-background:new" />
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

27
ding/data/meson.build Normal file
View File

@@ -0,0 +1,27 @@
desktop_file_dir = join_paths(datadir, 'applications')
desktop_merged = i18n.merge_file(
input: 'com.desktop.ding.desktop.in',
output: 'com.desktop.ding.desktop',
po_dir: join_paths(meson.project_source_root(), 'po'),
type: 'desktop',
install: true,
install_dir: desktop_file_dir,
)
gnome.post_install(
update_desktop_database: true,
)
gnome.compile_resources(
app_id + '.data',
app_id + '.data.gresource.xml',
source_dir: meson.current_source_dir(),
extra_args: ['--sourcedir=' + meson.current_build_dir()],
dependencies: [ desktop_merged ],
gresource_bundle: true,
install: true,
install_dir: join_paths(extensions_dir, app_dir),
)
subdir('icons')

241
ding/data/stylesheet.css Normal file
View File

@@ -0,0 +1,241 @@
/*
Local file for to specify styles for desktop icons
@define-color desktop_icons_bg_color @theme_selected_accent_color; // Adwaita
@define-color desktop_icons_fg_color @theme_selected_fg_color;
The above colors are set using javascript code in DesktopManager
with sane fallbacks if theme_slected_colors do not exist,
and then injected into Gdk.Display css before this file is read in and added.
*/
box > label.file-label {
margin-top: 0px;
margin-left: 5px;
margin-right: 5px;
text-shadow: 0.6px 0.7px 1px black, 0.1em 0.1em 0.1em black;
color: white;
}
box > label.file-label-dark {
margin-top: 0px;
margin-left: 5px;
margin-right: 5px;
text-shadow: 0.6px 0.7px 1px white, 0.1em 0.1em 0.1em white;
color: black;
}
box > label.file-label-vertical {
/* Twice the padding set in box #file-item below to keep spacing between
icon and label constant with shape change */
margin-top: 4px;
}
box > #file-item {
padding: 2px;
border-radius: 5px;
}
box > #file-item:hover {
background-color: alpha(@desktop_icons_fg_color, 0.3);
}
box > #file-item.mimic-hovered {
background-color: alpha(@desktop_icons_fg_color, 0.3);
}
box > #file-item.desktop-icons-selected {
background-color: alpha(@desktop_icons_bg_color, 0.3);
}
box.desktop-icon-container.keyboard-selected {
/* Draw a visible keyboard focus rectangle inside the container */
box-shadow: inset 0 0 0 2px alpha(@desktop_icons_fg_color, 0.6);
background-color: alpha(@desktop_icons_fg_color, 0.3);
border-radius: 6px;
animation: shadow-descend 150ms ease-out;
}
box.desktop-icon-container.keyboard-selected.desktop-icons-selected {
background-color: alpha(@desktop_icons_bg_color, 0.3);
}
/* Avoid double hover shading when keyboard-selected */
box.desktop-icon-container.keyboard-selected > #file-item:hover,
box.desktop-icon-container.keyboard-selected > #file-item.mimic-hovered,
box.desktop-icon-container.keyboard-selected > #file-item.desktop-icons-selected {
background-color: transparent;
}
@keyframes shadow-descend {
from {
box-shadow: inset 0 0 0 6px alpha(@desktop_icons_fg_color, 0.0);
}
to {
box-shadow: inset 0 0 0 2px alpha(@desktop_icons_fg_color, 0.6);
}
}
.not-found {
color: rgb(255, 0, 0);
}
#desktopwindow.background {
background-blend-mode: normal;
background-clip: border-box;
background-color: transparent;
background-image: none;
background-origin: padding-box;
background-position: left top;
background-repeat: repeat;
background-size: auto;
border-radius: 0px;
}
#errorstate.background {
background-blend-mode: normal;
background-clip: border-box;
background-color: alpha(red, 0.3);
background-image: none;
background-origin: padding-box;
background-position: left top;
background-repeat: repeat;
background-size: auto;
border-radius: 0px;
}
#testwindow.background {
background-blend-mode: normal;
background-clip: border-box;
background-color: black;
background-image: none;
background-origin: padding-box;
background-position: left top;
background-repeat: repeat;
background-size: auto;
border-radius: 0px;
}
.unhighlightdroptarget:drop(active) {
box-shadow: none;
}
#DingAppChooser treeview {
min-height: 36px;
-gtk-icon-size: 32px;
}
#ding-widget.dragging {
outline: 2px solid rgba(0, 120, 255, 0.8);
background-color: rgba(0, 120, 255, 0.15);
border-radius: 8px;
transform: scale(1.04);
}
#ding-widget.ding-widget-selected {
outline: none;
box-shadow: 0 0 0 2px alpha(@desktop_icons_bg_color, 0.9);
background-color: alpha(@desktop_icons_bg_color, 0.12);
border-radius: 8px;
}
#ding-widget {
border-radius: 8px;
background-color: transparent;
}
#ding-widget-close-button {
min-width: 28px;
min-height: 28px;
padding: 0;
border-radius: 9999px;
background-color: rgba(0,0,0,0.65);
color: white;
box-shadow: 0 1px 3px rgba(0,0,0,0.18);
transition: opacity 0.15s, background-color 0.15s;
-gtk-icon-size: 18px; /* symbolic icon size */
}
#ding-widget-close-button:hover {
background-color: rgba(0,0,0,0.8);
}
#ding-widget-close-button:disabled {
background-color: rgba(0,0,0,0.3);
color: rgba(255,255,255,0.5);
}
#ding-widget-add-button {
min-width: 48px;
min-height: 48px;
padding: 0;
border-radius: 9999px;
background-color: rgba(0, 0, 0, 0.7);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
color: #fff;
-gtk-icon-size: 24px;
}
#ding-widget-add-button:hover {
background-color: rgba(255, 255, 255, 0.25);
}
#ding-widget-grid-toggle-button {
min-width: 48px;
min-height: 48px;
padding: 0;
border-radius: 9999px;
background-color: rgba(0, 0, 0, 0.7);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
color: #fff;
-gtk-icon-size: 24px;
}
#ding-widget-grid-toggle-button:hover {
background-color: rgba(255, 255, 255, 0.25);
}
#widget-container {
background-color: transparent;
}
#widget-container.widgets-on-top {
background-color: rgba(255, 255, 255, 0.25);
}
#ding-widget-webview {
margin: 0;
padding: 0;
background-color: transparent;
background-image: none;
border: none;
box-shadow: none;
border-radius: 0px;
}
#ding-widget-prefs-button {
min-width: 28px;
min-height: 28px;
padding: 0;
border-radius: 9999px;
background-color: rgba(0,0,0,0.65);
color: white;
box-shadow: 0 1px 3px rgba(0,0,0,0.18);
transition: opacity 0.15s, background-color 0.15s;
-gtk-icon-size: 18px; /* match close button */
}
#ding-widget-prefs-button:hover {
background-color: rgba(0,0,0,0.8);
}
#ding-widget-prefs-button:disabled {
background-color: rgba(0,0,0,0.3);
color: rgba(255,255,255,0.5);
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<object class="GtkDialog" id="DingAppChooser">
<property name="use_header_bar">1</property>
<property name="title" translatable="yes">Open File</property>
<property name="focusable">False</property>
<property name="destroy-with-parent">True</property>
<property name="modal">True</property>
<property name="default-width">420</property>
<property name="default-height">560</property>
<child internal-child="content_area">
<object class="GtkBox" id="content_area">
<property name="orientation">vertical</property>
<child>
<object class="GtkStack">
<property name="hexpand">True</property>
<child>
<object class="GtkStackPage">
<property name="name">list</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow">
<property name="hscrollbar-policy">never</property>
<property name="vscrollbar-policy">never</property>
<property name="vexpand">true</property>
<style>
<class name="background"/>
</style>
<property name="child">
<object class="AdwClamp">
<property name="margin-top">18</property>
<property name="margin-bottom">18</property>
<property name="margin-start">18</property>
<property name="margin-end">18</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel" id="label_description">
<property name="wrap">True</property>
<property name="wrap-mode">PANGO_WRAP_WORD_CHAR</property>
<property name="justify">center</property>
<property name="label" translatable="yes">Choose an app to open the selected files.</property>
</object>
</child>
<child>
<object class="GtkBox" id="app_chooser_widget_box">
<property name="halign">center</property>
<property name="vexpand">True</property>
</object>
</child>
</object>
</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkBox" id="set_default_box">
<property name="orientation">vertical</property>
<style>
<class name="background"/>
</style>
<child>
<object class="GtkSeparator"/>
</child>
<child>
<object class="GtkListBox">
<style>
<class name="background"/>
</style>
<child>
<object class="AdwActionRow" id="set_default_row">
<property name="hexpand">true</property>
<property name="selectable">false</property>
<property name="activatable-widget">set_as_default_switch</property>
<property name="title" translatable="yes">Always use for this file type</property>
<child>
<object class="GtkSwitch" id="set_as_default_switch">
<property name="halign">end</property>
<property name="valign">center</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child type="action">
<object class="GtkButton" id="cancel_button">
<property name="label" translatable="yes">_Cancel</property>
<property name="use-underline">True</property>
</object>
</child>
<child type="action">
<object class="GtkButton" id="ok_button">
<property name="label" translatable="yes">_Open</property>
<property name="use-underline">True</property>
<property name="sensitive">False</property>
</object>
</child>
<action-widgets>
<action-widget response="ok" default="true">ok_button</action-widget>
<action-widget response="cancel">cancel_button</action-widget>
</action-widgets>
</object>
</interface>

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<requires lib="adwaita" version="1.0"/>
<object class="AdwWindow" id="widget_picker_window">
<property name="default-width">420</property>
<property name="default-height">320</property>
<property name="modal">true</property>
<property name="title" translatable="yes">Add Widget</property>
<property name="decorated">false</property>
<child>
<object class="AdwToolbarView" id="toolbar_view">
<!-- Header bar -->
<child type="top">
<object class="AdwHeaderBar" id="header_bar">
<property name="show-start-title-buttons">false</property>
<property name="show-end-title-buttons">false</property>
<child type="title">
<object class="GtkLabel" id="title_label">
<property name="label" translatable="yes">Add Widget</property>
<property name="xalign">0.5</property>
</object>
</child>
<child type="start">
<object class="GtkButton" id="cancel_button">
<property name="label" translatable="yes">Cancel</property>
</object>
</child>
<child type="end">
<object class="GtkButton" id="add_button">
<property name="label" translatable="yes">Add</property>
<style>
<class name="suggested-action"/>
</style>
</object>
</child>
</object>
</child>
<!-- Main content -->
<child>
<object class="GtkBox" id="outer_box">
<property name="orientation">vertical</property>
<property name="spacing">0</property>
<property name="margin-top">6</property>
<property name="margin-bottom">6</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<child>
<object class="GtkScrolledWindow" id="scrolled">
<property name="hexpand">true</property>
<property name="vexpand">true</property>
<child>
<object class="GtkListBox" id="widget_list">
<property name="selection-mode">single</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>

View File

@@ -0,0 +1,7 @@
import {domain as gettextDomain} from 'gettext';
const Gettext = gettextDomain('gtk4-ding');
const _ = Gettext.gettext;
export {Gettext, _};

90
ding/dependencies/gi.js Normal file
View File

@@ -0,0 +1,90 @@
import Adw from 'gi://Adw';
import Gdk from 'gi://Gdk?version=4.0';
import GdkPixbuf from 'gi://GdkPixbuf';
import GdkWayland from 'gi://GdkWayland?version=4.0';
import GdkX11 from 'gi://GdkX11?version=4.0';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
let GLibUnix;
GLibUnix = await import('gi://GLibUnix').then(module => module.default).catch(_e => {
console.log('GLibUnix not found.');
});
if (!GLibUnix) {
console.log('Falling back to GLib...');
GLibUnix = {
'signal_add_full': GLib.unix_signal_add,
};
}
import GnomeDesktop from 'gi://GnomeDesktop?version=4.0';
const GnomeAutoar = await import('gi://GnomeAutoar')
.then(module => module.default)
.catch(e => console.error(e));
import GObject from 'gi://GObject';
import Graphene from 'gi://Graphene';
import Gsk from 'gi://Gsk';
import Gtk from 'gi://Gtk';
import Pango from 'gi://Pango';
const Poppler = await import('gi://Poppler')
.then(module => module.default)
.catch(e => console.error(`Install Poppler for proper fallback pdf thumbnailing \n ${e}`));
const Cairo = await import('gi://cairo')
.then(module => module.default)
.catch(e => console.error(`Install Cairo for proper fallback pdf thumbnailing \n ${e}`));
import gettext from 'gettext';
const GioUnix = await import('gi://GioUnix?version=2.0')
.then(module => module.default)
.catch(_e => {
console.log('GioUnix not found, falling back to Gio...');
});
var DesktopAppInfo;
// Prefer GioUnix if available (newer GLib ≥ 2.80)
if (GioUnix?.DesktopAppInfo)
DesktopAppInfo = GioUnix.DesktopAppInfo;
else if (Gio?.DesktopAppInfo)
DesktopAppInfo = Gio.DesktopAppInfo;
if (!DesktopAppInfo)
console.error('DesktopAppInfo is not available on this system!');
const WebKit = await import('gi://WebKit?version=6.0')
.then(m => m.default)
.catch(e => {
console.log(`WebKit GI not found; desktop widgets disabled\n${e}`);
return null;
});
const Soup = await import('gi://Soup?version=3.0')
.then(m => m.default)
.catch(e => {
console.log(`Soup GI not found; desktop widgets disabled\n${e}`);
return null;
});
const DesktopWidgetCapability = !!WebKit && !!Soup;
export {
Adw,
Cairo,
DesktopAppInfo,
DesktopWidgetCapability,
Gdk,
GdkPixbuf,
GdkX11,
GdkWayland,
gettext,
GLib,
GLibUnix,
GnomeDesktop,
GnomeAutoar,
GObject,
Gio,
Graphene,
Gsk,
Gtk,
Pango,
Poppler,
Soup,
WebKit
};

View File

@@ -0,0 +1,41 @@
export {DesktopFolderUtils} from '../app/utils/desktopFolderUtils.js';
export * as AdwPreferencesWindow from '../app/adwPreferencesWindow.js';
export * as AppChooser from '../app/appChooser.js';
export * as AskRenamePopup from '../app/askRenamePopup.js';
export * as AutoAr from '../app/autoAr.js';
export * as DBusInterfaces from '../app/utils/dbusInterfaces.js';
export * as DBusUtils from '../app/utils/dbusUtils.js';
export * as DesktopGrid from '../app/desktopGrid.js';
export * as DesktopIconsUtil from '../app/utils/desktopIconsUtil.js';
export * as DesktopManager from '../app/desktopManager.js';
export * as DesktopMonitor from '../app/desktopFolderMonitor.js';
export * as Enums from '../app/enums.js';
export * as FileItemMenu from '../app/fileItemMenu.js';
export * as FileUtils from '../utils/fileUtils.js';
export * as Preferences from '../app/preferences.js';
export * as GnomeShellDragDrop from '../app/gnomeShellDragDrop.js';
export * as GsConnect from '../app/utils/gsConnect.js';
export * as ShowErrorPopup from '../app/showErrorPopup.js';
export * as StackItem from '../app/stackItem.js';
export * as TemplatesScriptsManager from '../app/templatesScriptsManager.js';
export * as Thumbnails from '../app/thumbnails.js';
export * as WindowManager from '../app/windowManager.js';
export * as DesktopMenu from '../app/desktopMenu.js';
export * as DragManager from '../app/dragManager.js';
export {IconCreator} from '../app/desktopIconFactory.js';
export {FileItemIcon} from '../app/fileItemIcon.js';
export {DesktopIconItem} from '../app/desktopIconItem.js';
export {VolumeIcon} from '../app/volumeIcon.js';
export {DesktopFileIcon} from '../app/desktopFileIcon.js';
export {AppImageFileIcon} from '../app/appImageFileItem.js';
export {SymLinkIcon} from '../app/symLinkIcon.js';
export {SpecialFolderIcon} from '../app/specialFolderIcon.js';
export {ShortcutManager} from '../app/shortcutManager.js';
export {DefaultShortcuts} from '../app/shortcuts.js';
export {GlobalShortcuts} from '../app/shortcuts.js';
export * as WidgetManager from '../app/widgetManager.js';
export {WidgetRegistry} from '../app/widgetRegistry.js';
export {HtmlWidgetHost} from '../app/htmlWidgetHost.js';
export {HtmlWidgetHostWithBackend} from '../app/htmlWidgetHostWithBackend.js';
export * as WidgetApi from '../app/widgetApi.js';
export {WebWidgetContext} from '../app/widgetWebContext.js';

1093
ding/dingManager.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,839 @@
/* Emulate X11WindowType
*
* Copyright (C) 2022 Sundeep Mediratta (smedius@gmail.com)
* Copyright (C) 2020 Sergio Costas (rastersoft@gmail.com)
*
* 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, version 3 of the License.
*
* 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/>.
*/
/* global global */
/* exported EmulateX11WindowType */
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Clutter from 'gi://Clutter';
import Meta from 'gi://Meta';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as DND from 'resource:///org/gnome/shell/ui/dnd.js';
import * as AppFavorites from 'resource:///org/gnome/shell/ui/appFavorites.js';
import * as Utils from 'resource:///org/gnome/shell/misc/util.js';
export {EmulateX11WindowType};
const appID = 'com.desktop.ding';
const appPath = GLib.build_filenamev(['/', ...appID.split('.')]);
class ManageWindow {
/* This class is added to each managed window, and it's used to make it
behave like an X11 Desktop window.
Trusted windows will set in the title the characters @!, followed by
the coordinates where to put the window separated by a colon, and
ended in a semicolon. After that, it can have one or more of these
letters:
* B : put and always keep this window at the bottom of the stack of
windows on screen
* T : put and always keep this window at the top of the stack of
windows on the screen
* D : show this window in all desktops
* H : hide this window from the window list
Using the title is generally not a problem because the desktop windows
do not have a title. But some other windows may have and still need to
set a title and use this class, so adding a single blank space at the
end of the title is equivalent to @!H, and having two blank spaces at
the end of the title is equivalent to @!HTD. This allows use of these
flags for decorated or titled windows.
*/
constructor(window, waylandClient, changedStatusCB) {
this.isWayland = typeof Meta.is_wayland_compositor === 'function'
? Meta.is_wayland_compositor()
: true;
this._isX11 = !this.isWayland;
this._waylandClient = waylandClient;
this._window = window;
this._signalIDs = [];
this._onIdleChangedStatusCallback = changedStatusCB;
this._titleID = this._window.connect('notify::title', () => {
this.refreshProperties();
});
this._parseTitle();
this._attachControllers();
}
disconnect() {
this._disconnetSignalsAndTimeouts();
if (this._titleID)
this._window.disconnect(this._titleID);
this._titleID = 0;
if (this._keepAtTop)
this._window.unmake_above();
this._window = null;
this._waylandClient = null;
}
_disconnetSignalsAndTimeouts() {
for (let signalID of this._signalIDs) {
if (signalID)
this._window.disconnect(signalID);
}
this._signalIDs = [];
if (this._checkOnAllWorkspacesID)
GLib.source_remove(this._checkOnAllWorkspacesID);
this._checkOnAllWorkspacesID = 0;
if (this._moveIntoPlaceID)
GLib.source_remove(this._moveIntoPlaceID);
this._moveIntoPlaceID = 0;
if (this._restackedBottomID)
global.display.disconnect(this._restackedBottomID);
this._restackedBottomID = 0;
if (this._showDesktopID)
global.workspace_manager.disconnect(this._showDesktopID);
this._showDesktopID = 0;
if (this._restackedTopID)
global.display.disconnect(this._restackedTopID);
this._restackedTopID = 0;
}
set_wayland_client(client) {
this._waylandClient = client;
}
_parseTitle() {
this._x = null;
this._y = null;
this._keepAtBottom = false;
this._keepAtTop = false;
this._showInAllDesktops = false;
this._hideFromWindowList = false;
this._fixed = false;
this._desktopWindow = false;
let title = this._window.get_title();
if (!title && !!this._window.get_transient_for()) {
// Transient dialog window
// Does not have title, hide from windowlist
title = '@!H';
}
if (title !== null) {
if ((title.length > 0) && (title[title.length - 1] === ' ')) {
if ((title.length > 1) && (title[title.length - 2] === ' '))
title = '@!HTD';
else
title = '@!H';
}
let pos = title.search('@!');
if (pos !== -1) {
let pos2 = title.search(';', pos);
let coords;
if (pos2 !== -1)
coords = title.substring(pos + 2, pos2).trim().split(',');
else
coords = title.substring(pos + 2).trim().split(',');
try {
this._x = parseInt(coords[0]);
this._y = parseInt(coords[1]);
} catch (e) {
global.log(`Exception ${e.message}.\n${e.stack}`);
}
try {
let extraChars =
title.substring(pos + 2).trim().toUpperCase();
for (let char of extraChars) {
switch (char) {
case 'B':
this._keepAtBottom = true;
this._keepAtTop = false;
break;
case 'T':
this._keepAtTop = true;
this._keepAtBottom = false;
break;
case 'D':
this._showInAllDesktops = true;
break;
case 'H':
this._hideFromWindowList = true;
break;
case 'F':
this._fixed = true;
break;
}
}
this._desktopWindow =
this._keepAtBottom &&
!this._keepAtTop &&
this._showInAllDesktops &&
this._hideFromWindowList;
} catch (e) {
global.log(`Exception ${e.message}.\n${e.stack}`);
}
}
}
}
_attachControllers() {
if (this._fixed)
this._keepFixedWindowPosition();
if (this._hideFromWindowList)
this._keepWindowHidden();
else
this._unhideWindow();
if (this._keepAtTop)
this._keepWindowOnTop();
else if (this._window.above)
this._window.unmake_above();
if (this._keepAtBottom & !this._desktopWindow)
this._keepWindowAtBottom();
if (this._showInAllDesktops & !this._desktopWindow)
this._showWindowOnAllDesktops();
else if (this._window.on_all_workspaces)
this._window.unstick();
if (this._desktopWindow) {
if (typeof this._window.set_type === 'function') {
this._window.set_type(Meta.WindowType.DESKTOP);
console.log('Setting window type to desktop with Gnome 49 API');
// In future, Meta.WaylandClient.make_desktop(window) will not
// be necessary.
} else {
this._makeWindowTypeDesktop();
}
}
}
_keepFixedWindowPosition() {
this._signalIDs.push(
this._window.connect(
'position-changed',
() => {
if (this._fixed &&
(this._x !== null) &&
(this._y !== null)
) {
this._window.move_frame(true, this._x, this._y);
if (this._window.fullscreen)
this._window.unmake_fullscreen();
}
}
)
);
this._signalIDs.push(
this._window.connect('notify::minimized', () => {
this._window.unminimize();
})
);
this._signalIDs.push(
this._window.connect('notify::maximized-vertically',
() => {
if (typeof this._window.is_maximized === 'function' &&
!this._window.is_maximized()
)
this._window.maximize();
else if (!this._window.maximized_vertically)
this._window.maximize(Meta.MaximizeFlags.VERTICAL);
this._moveIntoPlace();
}
)
);
this._signalIDs.push(
this._window.connect('notify::maximized-horizontally',
() => {
if (typeof this._window.is_maximized === 'function' &&
!this._window.is_maximized()
)
this._window.maximize();
else if (!this._window.maximized_horizontally)
this._window.maximize(Meta.MaximizeFlags.HORIZONTAL);
this._moveIntoPlace();
}
)
);
if ((this._x !== null) && (this._y !== null))
this._window.move_frame(true, this._x, this._y);
}
_moveIntoPlace() {
if (this._moveIntoPlaceID)
GLib.source_remove(this._moveIntoPlaceID);
this._moveIntoPlaceID =
GLib.timeout_add(GLib.PRIORITY_LOW, 250, () => {
if (this._fixed && (this._x !== null) && (this._y !== null))
this._window.move_frame(true, this._x, this._y);
this._moveIntoPlaceID = 0;
return GLib.SOURCE_REMOVE;
});
}
_keepWindowHidden() {
if (!this._isX11 && this._waylandClient) {
this._waylandClient.hide_from_window_list(this._window);
} else {
const xid = this._window.xwindow;
this._setX11windowSkipTaskbar(xid);
}
}
_unhideWindow() {
if (!this._isX11 && this._waylandClient) {
this._waylandClient.show_in_window_list(this._window);
} else {
const xid = this._window.xwindow;
this._unSetX11windowSkipTaskbar(xid);
}
}
_keepWindowAtBottom() {
this._signalIDs.push(
this._window.connect(
'notify::above',
() => {
if (this._keepAtBottom && this._window.above)
this._window.unmake_above();
}
)
);
this._signalIDs.push(
this._window.connect_after(
'raised',
() => {
if (this._keepAtBottom)
this._window.lower();
}
)
);
/* If a window is lowered below us with shortcuts,
detect and fix DING window */
this._restackedBottomID = global.display.connect('restacked',
this._syncToBottomOfStack.bind(this)
);
/* If the desktop is shown with keyboard gnome shortcuts, detect and put
DING window back.
Seems to be needed for X11, works without on Wayland.
*/
if (this._isX11) {
this._showDesktopID =
global.workspace_manager.connect(
'showing-desktop-changed',
this._activateDesktopWindow.bind(this)
);
}
if (this._window.above)
this._window.unmake_above();
this._window.lower();
}
_keepWindowUnFullScreen() {
this._signalIDs.push(
this._window.connect(
'notify::fullscreen',
() => {
if (this._window.fullscreen)
this._window.unmake_fullscreen();
}
)
);
if (this._window.fullscreen)
this._window.unmake_fullscreen();
}
_activateDesktopWindow() {
if (this._desktopWindow)
this._window.activate(Meta.CURRENT_TIME);
}
_syncToBottomOfStack() {
let windows =
global.display
.get_tab_list(
Meta.TabList.NORMAL_ALL,
global.workspace_manager.get_active_workspace()
);
windows = global.display.sort_windows_by_stacking(windows);
if (windows.length > 1 && !windows[0].customJS_ding)
this._moveDesktopWindowToBottom();
}
_moveDesktopWindowToBottom() {
if (this._window.fullscreen)
this._window.unmake_fullscreen();
if (this._keepAtBottom)
this._window.lower();
}
_keepWindowOnTop() {
this._restackedTopID = global.display.connect('restacked', () => {
if (!this._window.above)
this._window.make_above();
});
if (!this._window.above)
this._window.make_above();
}
_showWindowOnAllDesktops() {
this._signalIDs.push(this._window.connect('notify::on-all-workspaces',
this._checkOnAllWorkspaces.bind(this)
));
this._signalIDs.push(this._window.connect('workspace-changed',
this._checkOnAllWorkspaces.bind(this)
));
this._window.stick();
}
_checkOnAllWorkspaces() {
if (this._checkOnAllWorkspacesID)
GLib.source_remove(this._checkOnAllWorkspacesID);
this._checkOnAllWorkspacesID =
GLib.idle_add(
GLib.PRIORITY_LOW,
() => {
if (this._showInAllDesktops &&
!this._window.on_all_workspaces
) {
this._window.stick();
this._onIdleActivateTopWindowOnActiveWorkspace();
}
this._checkOnAllWorkspacesID = null;
return GLib.SOURCE_REMOVE;
}
);
}
_makeWindowTypeDesktop() {
if (!this._isX11 && this._waylandClient) {
const desktopWindowTypeSetOnWindow =
this._waylandClient.make_desktop_window(this._window);
if (!desktopWindowTypeSetOnWindow) {
this._emulateDesktopWindow();
return;
}
} else {
const xid = this._window.xwindow;
try {
this._setX11windowTypeDesktop(xid);
} catch (e) {
logError(e);
this._emulateDesktopWindow();
return;
}
}
// Window manager bug - it treats request to resize window
// to monitor size as a fullscreen window request as well and makes
// the window fullscreen, more so for legacy X11 apps.
// This makes intellihide for docks/panels hide from desktop window
this._keepWindowUnFullScreen();
const activateTopWindowOnWorkspace = true;
this._onIdleChangedStatusCallback({activateTopWindowOnWorkspace});
}
_emulateDesktopWindow() {
console.log('Emulating window type Desktop');
this._window.get_window_type = function () {
return Meta.WindowType.DESKTOP;
};
this._keepWindowAtBottom();
this._showWindowOnAllDesktops();
const moveDesktopWindowToBottom = true;
const activateTopWindowOnWorkspace = true;
this._onIdleChangedStatusCallback(
{moveDesktopWindowToBottom, activateTopWindowOnWorkspace}
);
}
_onIdleActivateTopWindowOnActiveWorkspace() {
const activateTopWindowOnWorkspace = true;
this._onIdleChangedStatusCallback({activateTopWindowOnWorkspace});
}
_setX11windowSkipTaskbar(xid) {
// Unfortunately xprop can set only one of the properties in the state,
// not multiple.
// Stick to setting only skip-taskbar, we can otherwirse also set
// the property for pager, _NET_WM_STATE_SKIP_PAGER
const commandline = `xprop -id ${xid}` +
' -f _NET_WM_STATE 32a' +
' -set _NET_WM_STATE' +
' _NET_WM_STATE_SKIP_TASKBAR';
console.log('Making X11 windowtype type skip-taskbar');
Utils.spawnCommandLine(commandline);
}
_unSetX11windowSkipTaskbar(xid) {
const commandline = `xprop -id ${xid}` +
' -f _NET_WM_STATE 32a' +
' -remove _NET_WM_STATE' +
' _NET_WM_STATE_SKIP_TASKBAR';
console.log('Making X11 windowtype type NOT skip-taskbar');
Utils.spawnCommandLine(commandline);
}
_setX11windowTypeDesktop(xid) {
const commandline = `xprop -id ${xid}` +
' -f _NET_WM_WINDOW_TYPE 32a' +
' -set _NET_WM_WINDOW_TYPE' +
' _NET_WM_WINDOW_TYPE_DESKTOP';
console.log('Making X11 windowtype type Desktop');
Utils.trySpawnCommandLine(commandline);
}
refreshProperties() {
this._disconnetSignalsAndTimeouts();
this._parseTitle();
this._attachControllers();
}
get hideFromWindowList() {
return this._hideFromWindowList;
}
get keepAtBottom() {
return this._keepAtBottom;
}
get desktopWindow() {
return this._desktopWindow;
}
}
var EmulateX11WindowType = class {
/*
This class does all the heavy lifting for emulating WindowType.
Just make one instance of it, call enable(), and whenever a window
that you want to give "superpowers" is mapped, add it with the
"addWindowManagedCustomJS_ding" method. That's all.
*/
constructor() {
this._windowList = new Set();
this._overviewHiding = true;
this._waylandClient = null;
this.isWayland = typeof Meta.is_wayland_compositor === 'function'
? Meta.is_wayland_compositor()
: true;
this._isX11 = !this.isWayland;
}
set_wayland_client(client) {
this._waylandClient = client;
for (let window of this._windowList) {
if (window.customJS_ding)
window.customJS_ding.set_wayland_client(this._waylandClient);
}
}
enable() {
this._idMap =
global.window_manager.connect_after(
'map',
(obj, windowActor) => {
const window = windowActor.get_meta_window();
if (window.get_window_type() > Meta.WindowType.MODAL_DIALOG)
return;
const appid = window.get_gtk_application_id();
if (appid !== appID)
return;
const windowpid = window.get_pid();
const mypid = this._waylandClient
? parseInt(this._waylandClient.query_pid_of_program())
: null;
if (this._waylandClient &&
this._waylandClient.query_window_belongs_to(window)
) {
this._addWindowManagedCustomJS_ding(
window,
windowActor
);
return;
}
if (mypid !== null && windowpid === mypid) {
this._addWindowManagedCustomJS_ding(
window,
windowActor
);
}
}
);
/* But in Overview mode it is paramount to not change the workspace to
emulate "stick", or the windows will appear
*/
this._showingId = Main.overview.connect('showing', () => {
this._overviewHiding = false;
});
this._hidingId = Main.overview.connect('hiding', () => {
this._overviewHiding = true;
});
}
disable() {
if (this._activate_window_ID) {
GLib.source_remove(this._activate_window_ID);
this._activate_window_ID = null;
}
for (let window of this._windowList)
this._clearWindow(window);
this._windowList.clear();
// disconnect signals
if (this._idMap) {
global.window_manager.disconnect(this._idMap);
this._idMap = null;
}
if (this._idDestroy) {
global.window_manager.disconnect(this._idDestroy);
this._idDestroy = null;
}
if (this._showingId) {
Main.overview.disconnect(this._showingId);
this._showingId = null;
}
if (this._hidingId) {
Main.overview.disconnect(this._hidingId);
this._hidingId = null;
}
}
_addWindowManagedCustomJS_ding(window, windowActor) {
if (window.get_meta_window) { // it is a MetaWindowActor
window = window.get_meta_window();
}
if (this._windowList.has(window))
return;
window.customJS_ding =
new ManageWindow(
window,
this._waylandClient,
this.onIdleReStackActivteWindows.bind(this)
);
window.actor = windowActor;
windowActor._delegate = new HandleDragActors(windowActor);
this._windowList.add(window);
window.customJS_ding.unmanagedID =
window.connect(
'unmanaging',
win => {
this._clearWindow(win);
this._windowList.delete(window);
}
);
}
_clearWindow(window) {
window.disconnect(window.customJS_ding.unmanagedID);
window.customJS_ding.disconnect();
window.customJS_ding = null;
window.actor._delegate = null;
window.actor = null;
}
_activateTopWindowOnActiveWorkspace() {
let windows =
global.display
.get_tab_list(
Meta.TabList.NORMAL,
global.workspace_manager.get_active_workspace()
);
windows = global.display.sort_windows_by_stacking(windows);
if (windows.length) {
const topWindow = windows[windows.length - 1];
topWindow.focus(Clutter.CURRENT_TIME);
}
}
_moveDesktopWindowToBottom() {
for (let window of this._windowList)
window.customJS_ding._moveDesktopWindowToBottom();
}
onIdleReStackActivteWindows(action = {activateTopWindowOnWorkspace: true}) {
if (!this._activate_window_ID) {
this._activate_window_ID =
GLib.idle_add(
GLib.PRIORITY_LOW,
() => {
if (this._overviewHiding) {
if (action.moveDesktopWindowToBottom)
this._moveDesktopWindowToBottom();
if (action.activateTopWindowOnWorkspace)
this._activateTopWindowOnActiveWorkspace();
}
this._activate_window_ID = null;
return GLib.SOURCE_REMOVE;
}
);
}
}
// After shell unlock, window seems to lose stick property,
// refresh window properties
refreshWindows() {
for (let window of this._windowList)
window.customJS_ding.refreshProperties();
}
};
// Since Gnome Shell 48 the enumeration of the cursor is different
// the name has changed, althugh the value is the same;
// We use our own enumeration names to avoid problems with the version
// of the Gnome Shell, the enumeration integer points to the correct
// value in the Gnome Shell 48 and Meta 48 Enum and earlier.
// Using the wrong Enum Name seems to crash mutter
const ShellDropCursor = {
DEFAULT: 2, // META_CURSOR_DEFAULT Meta.Cursor.DEFAULT
NODROP: 15, // META_CURSOR_NO_DROP Meta.Cursor.DND_UNSUPPORTED_TARGET
COPY: 13, // META_CURSOR_COPY Meta.Cursor.DND_COPY
MOVE: 14, // META_CURSOR_MOVE Meta.Cursor.DND_MOVE
};
class HandleDragActors {
/* This class is added to each managed windowActor, and it's used to
make it behave like a shell Actor that can accept drops from
Gnome Shell dnd.
*/
constructor(windowActor) {
this.windowActor = windowActor;
this.remoteDingActions = Gio.DBusActionGroup.get(
Gio.DBus.session,
appID,
appPath
);
}
_getModifierKeys() {
let [, , state] = global.get_pointer();
state &= Clutter.ModifierType.MODIFIER_MASK;
this.isControl = (state & Clutter.ModifierType.CONTROL_MASK) !== 0;
this.isShift = (state & Clutter.ModifierType.SHIFT_MASK) !== 0;
}
handleDragOver(source) {
if ((source.app ?? null) === null)
return DND.DragMotionResult.NO_DROP;
this._getModifierKeys();
if (this.isShift) {
global.display.set_cursor(ShellDropCursor.COPY);
return DND.DragMotionResult.COPY_DROP;
}
if (this.isControl) {
global.display.set_cursor(ShellDropCursor.MOVE);
return DND.DragMotionResult.MOVE_DROP;
}
return DND.DragMotionResult.CONTINUE;
}
acceptDrop(source, actor, x, y) {
if ((source.app ?? null) === null)
return false;
let appFavorites = AppFavorites.getAppFavorites();
let sourceAppId = source.app.get_id();
let sourceAppPath = source.app.appInfo.get_filename();
let appIsFavorite = appFavorites.isFavorite(sourceAppId);
this._getModifierKeys();
if (appIsFavorite && !this.isShift)
appFavorites.removeFavorite(sourceAppId);
if (sourceAppPath && (this.isControl || this.isShift)) {
this.remoteDingActions.activate_action('createDesktopShortcut',
new GLib.Variant('a{sv}', {
uri: GLib.Variant.new_string(`file://${sourceAppPath}`),
X: new GLib.Variant('i', parseInt(x)),
Y: new GLib.Variant('i', parseInt(y)),
})
);
}
appFavorites.emit('changed');
return true;
}
}

190
ding/gnomeShellOverride.js Normal file
View File

@@ -0,0 +1,190 @@
/* eslint-disable no-invalid-this */
/* eslint-disable no-undef */
/* The above is for use of global in this file as Shell.global */
/* Gnome Shell Override
*
* Copyright (C) 2023 Sundeep Mediratta (smedius@gmail.com)
*
* 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, version 3 of the License.
*
* 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/>.
*/
/* exported GnomeShellOverride */
const {Meta, Clutter, GObject} = imports.gi;
// Show desktop windows on workspace thumbnails
const SHOW_ON_WORKSPACE_THUMBNAILS = true;
const SHOW_ICONS_ON_OVERVIEW = false;
const ANIMATION_MULTIPLE = 1;
import {WorkspaceBackground} from 'resource:///org/gnome/shell/ui/workspace.js';
import {InjectionManager} from
'resource:///org/gnome/shell/extensions/extension.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as Util from 'resource:///org/gnome/shell/misc/util.js';
export {GnomeShellOverride};
var GnomeShellOverride = class {
constructor() {
this._injectionManager = new InjectionManager();
}
enable() {
const Background = WorkspaceBackground;
this._injectionManager.overrideMethod(Background.prototype, '_init',
this._newBackgroundInit.bind(this));
}
disable() {
this._injectionManager.clear();
}
_newBackgroundInit(origninalMethod) {
return function (...args) {
origninalMethod.call(this, ...args);
/** @enum {number} */
const ControlsState = {
HIDDEN: 0,
WINDOW_PICKER: 1,
APP_GRID: 2,
};
const opaque = 255;
const transparent = 0;
const adjustment =
Main.overview._overview._controls._stateAdjustment
function _windowIsOnThisMonitor(metawindow, monitorIndex) {
const geometry =
global.display.get_monitor_geometry(monitorIndex);
const [intersects] =
metawindow.get_frame_rect().intersect(geometry);
return intersects;
}
function _modifyTransparency(value) {
const {initialState, finalState, } =
adjustment.getStateTransitionParams();
if ((initialState == ControlsState.HIDDEN ||
finalState == ControlsState.HIDDEN) &&
(Math.abs(initialState - finalState) == 1))
return _setTransparency(value);
return transparent;
}
function _setTransparency(value) {
if (SHOW_ICONS_ON_OVERVIEW)
return opaque;
return Util.lerp(opaque, transparent,
Math.min(ANIMATION_MULTIPLE * value, 1.0));
}
const desktopWindows = global.get_window_actors().filter(a =>
a.meta_window.get_window_type() === Meta.WindowType.DESKTOP &&
_windowIsOnThisMonitor(a.meta_window, this._monitorIndex));
if (desktopWindows.length) {
const desktopLayer = new Clutter.Actor({
layout_manager: new DesktopLayout(),
clip_to_allocation: true,
});
for (let windowActor of desktopWindows) {
const clone = new Clutter.Clone({
source: windowActor,
});
desktopLayer.add_child(clone);
windowActor.connectObject('destroy', () => {
clone.destroy();
}, this);
}
const offset = 0;
const syncAll = Clutter.BindConstraint.new(
this._bgManager.backgroundActor,
Clutter.BindCoordinate.ALL,
offset);
desktopLayer.add_constraint(syncAll);
desktopLayer.opacity = _setTransparency(opaque);
this._stateAdjustment.connectObject('notify::value',
(stAdjustment) => {
if (SHOW_ON_WORKSPACE_THUMBNAILS)
desktopLayer.opacity =
_setTransparency(this._stateAdjustment.value);
else
desktopLayer.opacity =
_modifyTransparency(stAdjustment.value);
},
this
);
this._backgroundGroup.insert_child_above(
desktopLayer,
this._bgManager.backgroundActor
);
}
};
}
};
class DesktopLayout extends Clutter.LayoutManager {
static {
GObject.registerClass(this);
}
vfunc_get_preferred_width() {
return [0, 0];
}
vfunc_get_preferred_height() {
return [0, 0];
}
vfunc_allocate(container, box) {
const monitorIndex = Main.layoutManager.findIndexForActor(container);
const monitor = Main.layoutManager.monitors[monitorIndex];
const hscale = box.get_width() / monitor.width;
const vscale = box.get_height() / monitor.height;
for (const child of container) {
const childBox = new Clutter.ActorBox();
const frameRect = child.get_source()?.metaWindow.get_frame_rect();
childBox.set_size(
Math.round(frameRect.width * hscale),
Math.round(frameRect.height * vscale)
);
childBox.set_origin(
Math.round((frameRect.x - monitor.x) * hscale),
Math.round((frameRect.y - monitor.y) * vscale)
);
child.allocate(childBox);
}
}
}

56
ding/po/LINGUAS Normal file
View File

@@ -0,0 +1,56 @@
ar
az
be
bg
bn
ca
cs
da
de
el
eo
es
et
eu
fa
fi
fr
fur
ga
gl
he
hi
hr
hu
id
it
ja
ka
kk
ko
ky
lv
lt
ms
nb
nl
oc
pl
pt_BR
pt
ro
ru
sk
sl
sq
sv
ta
tl
tr
th
uk
ur
zh_CN
zh_TW
zh-Hans
zh-Hant

46
ding/po/POTFILES.in Normal file
View File

@@ -0,0 +1,46 @@
app/adw-ding.js
app/adwPreferencesWindow.js
app/appChooser.js
app/appImageFileItem.js
app/askRenamePopup.js
app/autoAr.js
app/desktopFileIcon.js
app/desktopFolderMonitor.js
app/desktopGrid.js
app/desktopIconFactory.js
app/desktopIconItem.js
app/desktopManager.js
app/desktopMenu.js
app/dragManager.js
app/enums.js
app/fileItemIcon.js
app/fileItemMenu.js
app/gnomeShellDragDrop.js
app/preferences.js
app/shortcutManager.js
app/shortcuts.js
app/showErrorPopup.js
app/specialFolderIcon.js
app/stackItem.js
app/symLinkIcon.js
app/templatesScriptsManager.js
app/thumbnails.js
app/volumeIcon.js
app/windowManager.js
app/widgetManager.js
app/widgetRegistry.js
app/widgetWebContext.js
app/htmlWidgetHost.js
app/htmlWidgetHostWithBackend.js
app/utils/dbusUtils.js
app/utils/desktopFolderUtils.js
app/utils/desktopIconsUtil.js
app/utils/gsConnect.js
data/ui/ding-app-chooser.ui
data/com.desktop.ding.desktop.in
desktopIconsIntegration.js
emulateX11WindowType.js
extension.js
prefs.js
visibleArea.js
schemas/org.gnome.shell.extensions.gtk4-ding.gschema.xml

2097
ding/po/ar.po Normal file

File diff suppressed because it is too large Load Diff

2065
ding/po/az.po Normal file

File diff suppressed because it is too large Load Diff

1887
ding/po/be.po Normal file

File diff suppressed because it is too large Load Diff

2116
ding/po/bg.po Normal file

File diff suppressed because it is too large Load Diff

2064
ding/po/bn.po Normal file

File diff suppressed because it is too large Load Diff

2020
ding/po/ca.po Normal file

File diff suppressed because it is too large Load Diff

2004
ding/po/cs.po Normal file

File diff suppressed because it is too large Load Diff

2041
ding/po/da.po Normal file

File diff suppressed because it is too large Load Diff

1886
ding/po/de.po Normal file

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More