Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 69343573af | |||
| de270cf9f5 | |||
| 0ff76b8c88 | |||
| b63f158459 | |||
|
|
8b34d5f144 | ||
|
|
a40d94c14b | ||
|
|
541f5f20da | ||
|
|
250d988473 | ||
|
|
41f92619bf | ||
|
|
388febf2fd |
4
.gitignore
vendored
@@ -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/
|
||||
|
||||
23
Makefile
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
624
debian/changelog
vendored
@@ -1,566 +1,58 @@
|
||||
gnome-shell-extension-zorin-taskbar (68.8) noble; urgency=medium
|
||||
|
||||
* Fixed app grid icon styling and rebased on upstream commit
|
||||
4787d12180462f0c1c90d3f38ece5921e7e31b7a
|
||||
|
||||
-- Artyom Zorin <azorin@zoringroup.com> Fri, 05 Sep 2025 17:44:41 +0100
|
||||
|
||||
gnome-shell-extension-zorin-taskbar (68.7) noble; urgency=medium
|
||||
|
||||
* Adjusted window preview margin and rounding
|
||||
|
||||
-- Artyom Zorin <azorin@zoringroup.com> Thu, 04 Sep 2025 14:22:11 +0100
|
||||
|
||||
gnome-shell-extension-zorin-taskbar (68.6) noble; urgency=medium
|
||||
|
||||
* Introduced keep gnome shell dash option
|
||||
|
||||
-- Artyom Zorin <azorin@zoringroup.com> Sat, 23 Aug 2025 15:04:05 +0100
|
||||
|
||||
gnome-shell-extension-zorin-taskbar (68.5.3) noble; urgency=medium
|
||||
|
||||
* Added workaround for race condition
|
||||
|
||||
-- Artyom Zorin <azorin@zoringroup.com> Thu, 07 Aug 2025 00:09:07 +0100
|
||||
|
||||
gnome-shell-extension-zorin-taskbar (68.5.2) noble; urgency=medium
|
||||
|
||||
* Fixed settings cache issues
|
||||
|
||||
-- Artyom Zorin <azorin@zoringroup.com> Wed, 06 Aug 2025 23:45:25 +0100
|
||||
|
||||
gnome-shell-extension-zorin-taskbar (68.5.1) noble; urgency=medium
|
||||
|
||||
* Minor code cleanups
|
||||
|
||||
-- Artyom Zorin <azorin@zoringroup.com> Tue, 05 Aug 2025 22:47:20 +0100
|
||||
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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));
|
||||
900
ding/app/adwPreferencesWindow.js
Normal 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
@@ -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('&', '&'));
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
};
|
||||
136
ding/app/appImageFileItem.js
Normal 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
@@ -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
@@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
BIN
ding/app/com.desktop.ding.data.gresource
Normal file
305
ding/app/desktopFileIcon.js
Normal 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(
|
||||
`Couldn’t 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;
|
||||
}
|
||||
};
|
||||
660
ding/app/desktopFolderMonitor.js
Normal 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
81
ding/app/desktopIconFactory.js
Normal 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
1874
ding/app/desktopManager.js
Normal file
1191
ding/app/desktopMenu.js
Normal file
987
ding/app/dragManager.js
Normal 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
@@ -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
@@ -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
368
ding/app/gnomeShellDragDrop.js
Normal 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
@@ -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();
|
||||
}
|
||||
});
|
||||
599
ding/app/htmlWidgetHostWithBackend.js
Normal 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
732
ding/app/shortcutManager.js
Normal 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
@@ -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
@@ -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);
|
||||
}
|
||||
};
|
||||
174
ding/app/specialFolderIcon.js
Normal 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
@@ -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
@@ -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: Can’t 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
|
||||
);
|
||||
}
|
||||
};
|
||||
308
ding/app/templatesScriptsManager.js
Normal 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
@@ -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;
|
||||
}
|
||||
};
|
||||
243
ding/app/utils/dbusInterfaces.js
Normal 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
350
ding/app/utils/desktopFolderUtils.js
Normal 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();
|
||||
}
|
||||
};
|
||||
1025
ding/app/utils/desktopIconsUtil.js
Normal file
230
ding/app/utils/gsConnect.js
Normal 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
@@ -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
@@ -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
573
ding/app/widgetRegistry.js
Normal 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
607
ding/app/windowManager.js
Normal 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();
|
||||
}
|
||||
};
|
||||
12
ding/apparmor/gtk4-desktop-icons
Normal 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>
|
||||
}
|
||||
12
ding/apparmor/gtk4-desktop-icons.in
Normal 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>
|
||||
}
|
||||
8
ding/apparmor/meson.build
Normal 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
|
||||
29
ding/data/com.desktop.ding.data.gresource.xml
Normal 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>
|
||||
500
ding/data/com.desktop.ding.desktop
Normal 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
|
||||
38
ding/data/com.desktop.ding.desktop.in
Normal 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
|
||||
59
ding/data/icons/com.desktop.ding.svg
Normal 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 |
4
ding/data/icons/edit-delete-symbolic.svg
Normal 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 |
4
ding/data/icons/edit-undo-symbolic.svg
Normal 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 |
4
ding/data/icons/emblem-readonly-symbolic.svg
Normal 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 |
4
ding/data/icons/emblem-symbolic-link-symbolic.svg
Normal 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 |
8
ding/data/icons/emblem-system-symbolic.svg
Normal 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 |
4
ding/data/icons/emblem-unwriteable-symbolic.svg
Normal 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 |
4
ding/data/icons/list-add-symbolic.svg
Normal 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 |
11
ding/data/icons/meson.build
Normal 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,
|
||||
)
|
||||
8
ding/data/icons/prefs-desktop-symbolic.svg
Normal 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 |
8
ding/data/icons/prefs-desktop.svg
Normal 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 |
47
ding/data/icons/prefs-files-symbolic.svg
Normal 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 |
8
ding/data/icons/prefs-files.svg
Normal 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 |
8
ding/data/icons/prefs-more-symbolic.svg
Normal 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 |
8
ding/data/icons/prefs-more.svg
Normal 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 |
64
ding/data/icons/prefs-tweaks-symbolic.svg
Normal 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
@@ -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 |
1
ding/data/icons/view-grid-symbolic.svg
Normal 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 |
4
ding/data/icons/window-close-symbolic.svg
Normal 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 |
8
ding/data/icons/window-pop-out-symbolic.svg
Normal 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 |
96
ding/data/icons/xapp-edit-symbolic.svg
Normal 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
@@ -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
@@ -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);
|
||||
}
|
||||
120
ding/data/ui/ding-app-chooser.ui
Normal 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>
|
||||
71
ding/data/ui/ding-widget-chooser.ui
Normal 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>
|
||||
7
ding/dependencies/gettext.js
Normal 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
@@ -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
|
||||
};
|
||||
41
ding/dependencies/localFiles.js
Normal 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
839
ding/emulateX11WindowType.js
Normal 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
@@ -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
@@ -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
@@ -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
|
||||