mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-06 05:25:41 -05:00
Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6013c994a6 | ||
|
|
46c90628b9 | ||
|
|
d2d2dac5d1 | ||
|
|
fd3e7470f4 | ||
|
|
b79e9f72ce | ||
|
|
77eb5dd3bf | ||
|
|
b17c14a07b | ||
|
|
494d90be22 | ||
|
|
da7e599e65 | ||
|
|
e3b7360f39 | ||
|
|
367130882d | ||
|
|
d8563ba79d | ||
|
|
e527453964 | ||
|
|
88fe3c5fbd | ||
|
|
748faf92c1 | ||
|
|
0126aded78 | ||
|
|
695a75ea09 | ||
|
|
80e690f9fc | ||
|
|
e8770b90ef | ||
|
|
eec9da42bf | ||
|
|
1c8f0d6292 | ||
|
|
b753c8840b | ||
|
|
95589982a5 | ||
|
|
37a10bd453 | ||
|
|
7abc76e92c | ||
|
|
7aa4467bda | ||
|
|
471938adb6 | ||
|
|
201a7e3b34 | ||
|
|
11ec3723c3 | ||
|
|
75eb736856 | ||
|
|
8fea126c20 | ||
|
|
cc02d09c4d | ||
|
|
af95631a1d | ||
|
|
7b3d2ab85a | ||
|
|
c52df96af9 | ||
|
|
dee5fa60af | ||
|
|
5e99fdd9c9 | ||
|
|
eb01fe757b | ||
|
|
c52483da2c | ||
|
|
2714c0f4ad | ||
|
|
bba21408ea | ||
|
|
47c5320d67 | ||
|
|
b5c49573e5 | ||
|
|
0197961175 | ||
|
|
f08b98dcba | ||
|
|
3878998080 | ||
|
|
5fa117db4c | ||
|
|
caa085a646 | ||
|
|
392a1c03c5 | ||
|
|
1524d27f4c | ||
|
|
d309957927 | ||
|
|
e0f2c03b91 | ||
|
|
1e5848e0d5 | ||
|
|
18bb7dc47b | ||
|
|
0ea7de12a5 | ||
|
|
c8e382e2dd | ||
|
|
84e19f8565 | ||
|
|
f597ea9948 | ||
|
|
d43e1a7cbe | ||
|
|
8131e713cf | ||
|
|
fefa2bd839 | ||
|
|
cc0984db14 | ||
|
|
f87609417b | ||
|
|
8d57b55f94 | ||
|
|
55776fd7cb | ||
|
|
3963c98689 | ||
|
|
02c59636fc | ||
|
|
989f196894 | ||
|
|
9314de4772 | ||
|
|
44a6cd88cd | ||
|
|
d8774c4787 | ||
|
|
a56066bac1 | ||
|
|
8a96f71d10 | ||
|
|
20a684e8f5 | ||
|
|
c8fcf50095 | ||
|
|
58b637bcca | ||
|
|
86caf92c90 | ||
|
|
35ead280d5 | ||
|
|
5d6c3e364d | ||
|
|
f006175829 | ||
|
|
3e0f325734 | ||
|
|
f8d383cff0 | ||
|
|
f2ec3ae755 | ||
|
|
f95e4e016b | ||
|
|
898e9e67d0 | ||
|
|
15983921b0 | ||
|
|
65c2077e30 | ||
|
|
946a28d3be | ||
|
|
69accb5319 | ||
|
|
4ddcf4391a | ||
|
|
a0d886009a | ||
|
|
91c37aaa96 | ||
|
|
7602247558 |
24
.githooks/pre-commit
Executable file
24
.githooks/pre-commit
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)"
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
if [[ -z "${POEDITOR_API_TOKEN:-}" ]] || [[ -z "${POEDITOR_PROJECT_ID:-}" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v python3 &>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! python3 scripts/i18nsync.py check &>/dev/null; then
|
||||
echo "Translations out of sync"
|
||||
echo "run python3 scripts/i18nsync.py sync"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
17
.github/workflows/copr-release.yml
vendored
17
.github/workflows/copr-release.yml
vendored
@@ -79,7 +79,7 @@ jobs:
|
||||
Release: 1%{?dist}
|
||||
Summary: %{pkg_summary}
|
||||
|
||||
License: GPL-3.0-only
|
||||
License: MIT
|
||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||
|
||||
Source0: dms-qml.tar.gz
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
|
||||
%package -n dms-cli
|
||||
Summary: DankMaterialShell CLI tool
|
||||
License: GPL-3.0-only
|
||||
License: MIT
|
||||
URL: https://github.com/AvengeMedia/danklinux
|
||||
|
||||
%description -n dms-cli
|
||||
@@ -172,6 +172,14 @@ jobs:
|
||||
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
||||
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
|
||||
|
||||
# Shell completions
|
||||
install -d %{buildroot}%{_datadir}/bash-completion/completions
|
||||
install -d %{buildroot}%{_datadir}/zsh/site-functions
|
||||
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
|
||||
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
|
||||
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
||||
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
||||
|
||||
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
||||
|
||||
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
||||
@@ -193,7 +201,7 @@ jobs:
|
||||
|
||||
# Restart DMS for active users after upgrade
|
||||
if [ "$1" -ge 2 ]; then
|
||||
pkill -HUP -x dms >/dev/null 2>&1 || true
|
||||
pkill -USR1 -x dms >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
%files
|
||||
@@ -204,6 +212,9 @@ jobs:
|
||||
|
||||
%files -n dms-cli
|
||||
%{_bindir}/dms
|
||||
%{_datadir}/bash-completion/completions/dms
|
||||
%{_datadir}/zsh/site-functions/_dms
|
||||
%{_datadir}/fish/vendor_completions.d/dms.fish
|
||||
|
||||
%files -n dgop
|
||||
%{_bindir}/dgop
|
||||
|
||||
192
.github/workflows/poeditor-export.yml
vendored
192
.github/workflows/poeditor-export.yml
vendored
@@ -1,192 +0,0 @@
|
||||
name: POEditor Diff & Sync
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
workflow_dispatch: {}
|
||||
|
||||
concurrency:
|
||||
group: poeditor-sync
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
sync-translations:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install jq
|
||||
run: sudo apt-get update && sudo apt-get install -y jq
|
||||
|
||||
- name: Extract source strings from codebase
|
||||
env:
|
||||
API_TOKEN: ${{ secrets.POEDITOR_API_TOKEN }}
|
||||
PROJECT_ID: ${{ secrets.POEDITOR_PROJECT_ID }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "::group::Extracting strings from QML files"
|
||||
python3 translations/extract_translations.py
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::Checking for changes in en.json"
|
||||
if [[ -f "translations/en.json" ]]; then
|
||||
jq -S . "translations/en.json" > /tmp/en_new.json
|
||||
if [[ -f "translations/en.json.orig" ]]; then
|
||||
jq -S . "translations/en.json.orig" > /tmp/en_old.json
|
||||
else
|
||||
git show HEAD:translations/en.json > /tmp/en_old.json 2>/dev/null || echo "[]" > /tmp/en_old.json
|
||||
jq -S . /tmp/en_old.json > /tmp/en_old.json.tmp && mv /tmp/en_old.json.tmp /tmp/en_old.json
|
||||
fi
|
||||
|
||||
if diff -q /tmp/en_new.json /tmp/en_old.json >/dev/null 2>&1; then
|
||||
echo "No changes in source strings"
|
||||
echo "source_changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Detected changes in source strings"
|
||||
echo "source_changed=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "::group::Uploading source strings to POEditor"
|
||||
RESP=$(curl -sS -X POST https://api.poeditor.com/v2/projects/upload \
|
||||
-F api_token="$API_TOKEN" \
|
||||
-F id="$PROJECT_ID" \
|
||||
-F updating="terms" \
|
||||
-F file=@"translations/en.json")
|
||||
|
||||
STATUS=$(echo "$RESP" | jq -r '.response.status')
|
||||
if [[ "$STATUS" != "success" ]]; then
|
||||
echo "::warning::POEditor upload failed: $RESP"
|
||||
else
|
||||
TERMS_ADDED=$(echo "$RESP" | jq -r '.result.terms.added // 0')
|
||||
TERMS_UPDATED=$(echo "$RESP" | jq -r '.result.terms.updated // 0')
|
||||
TERMS_DELETED=$(echo "$RESP" | jq -r '.result.terms.deleted // 0')
|
||||
echo "Terms added: $TERMS_ADDED, updated: $TERMS_UPDATED, deleted: $TERMS_DELETED"
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
fi
|
||||
else
|
||||
echo "::warning::translations/en.json not found"
|
||||
echo "source_changed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
id: extract
|
||||
|
||||
- name: Commit and push source strings
|
||||
if: steps.extract.outputs.source_changed == 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
git add translations/en.json translations/template.json
|
||||
git commit -m "i18n: update source strings from codebase"
|
||||
|
||||
for attempt in 1 2 3; do
|
||||
if git push; then
|
||||
echo "Successfully pushed source string updates"
|
||||
exit 0
|
||||
fi
|
||||
echo "Push attempt $attempt failed, pulling and retrying..."
|
||||
git pull --rebase
|
||||
sleep $((attempt*2))
|
||||
done
|
||||
|
||||
echo "Failed to push after retries" >&2
|
||||
exit 1
|
||||
|
||||
- name: Export and update translations from POEditor
|
||||
env:
|
||||
API_TOKEN: ${{ secrets.POEDITOR_API_TOKEN }}
|
||||
PROJECT_ID: ${{ secrets.POEDITOR_PROJECT_ID }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
LANGUAGES=(
|
||||
"ja:translations/poexports/ja.json"
|
||||
"zh-Hans:translations/poexports/zh_CN.json"
|
||||
"zh-Hant:translations/poexports/zh_TW.json"
|
||||
"pt-br:translations/poexports/pt.json"
|
||||
"tr:translations/poexports/tr.json"
|
||||
"it:translations/poexports/it.json"
|
||||
)
|
||||
|
||||
ANY_CHANGED=false
|
||||
|
||||
for lang_pair in "${LANGUAGES[@]}"; do
|
||||
IFS=':' read -r PO_LANG REPO_FILE <<< "$lang_pair"
|
||||
|
||||
echo "::group::Processing $PO_LANG"
|
||||
|
||||
RESP=$(curl -sS -X POST https://api.poeditor.com/v2/projects/export \
|
||||
-d api_token="$API_TOKEN" \
|
||||
-d id="$PROJECT_ID" \
|
||||
-d language="$PO_LANG" \
|
||||
-d type="key_value_json")
|
||||
|
||||
STATUS=$(echo "$RESP" | jq -r '.response.status')
|
||||
if [[ "$STATUS" != "success" ]]; then
|
||||
echo "POEditor export request failed for $PO_LANG: $RESP" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
URL=$(echo "$RESP" | jq -r '.result.url')
|
||||
if [[ -z "$URL" || "$URL" == "null" ]]; then
|
||||
echo "No export URL returned for $PO_LANG" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
curl -sS -L "$URL" -o "/tmp/po_export_${PO_LANG}.json"
|
||||
jq -S . "/tmp/po_export_${PO_LANG}.json" > "/tmp/po_export_${PO_LANG}.norm.json"
|
||||
|
||||
if [[ -f "$REPO_FILE" ]]; then
|
||||
jq -S . "$REPO_FILE" > "/tmp/repo_${PO_LANG}.norm.json" || echo "{}" > "/tmp/repo_${PO_LANG}.norm.json"
|
||||
else
|
||||
echo "{}" > "/tmp/repo_${PO_LANG}.norm.json"
|
||||
fi
|
||||
|
||||
if diff -q "/tmp/po_export_${PO_LANG}.norm.json" "/tmp/repo_${PO_LANG}.norm.json" >/dev/null; then
|
||||
echo "No changes for $PO_LANG"
|
||||
else
|
||||
echo "Detected changes for $PO_LANG"
|
||||
mkdir -p "$(dirname "$REPO_FILE")"
|
||||
cp "/tmp/po_export_${PO_LANG}.norm.json" "$REPO_FILE"
|
||||
ANY_CHANGED=true
|
||||
fi
|
||||
|
||||
echo "::endgroup::"
|
||||
done
|
||||
|
||||
echo "any_changed=$ANY_CHANGED" >> "$GITHUB_OUTPUT"
|
||||
id: export
|
||||
|
||||
- name: Commit and push translation updates
|
||||
if: steps.export.outputs.any_changed == 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
git add translations/poexports/*.json
|
||||
git commit -m "i18n: update translations"
|
||||
|
||||
for attempt in 1 2 3; do
|
||||
if git push; then
|
||||
echo "Successfully pushed translation updates"
|
||||
exit 0
|
||||
fi
|
||||
echo "Push attempt $attempt failed, pulling and retrying..."
|
||||
git pull --rebase
|
||||
sleep $((attempt*2))
|
||||
done
|
||||
|
||||
echo "Failed to push after retries" >&2
|
||||
exit 1
|
||||
34
.github/workflows/release.yml
vendored
34
.github/workflows/release.yml
vendored
@@ -58,8 +58,8 @@ jobs:
|
||||
## Assets
|
||||
|
||||
### Complete Packages
|
||||
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + installation guide)
|
||||
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + installation guide)
|
||||
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + shell completions + installation guide)
|
||||
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + shell completions + installation guide)
|
||||
|
||||
### Individual Components
|
||||
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
|
||||
@@ -135,27 +135,28 @@ jobs:
|
||||
# Generate checksum for QML package
|
||||
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
|
||||
|
||||
# Create full packages for each architecture
|
||||
for arch in amd64 arm64; do
|
||||
mkdir -p _temp_full/dms
|
||||
mkdir -p _temp_full/bin
|
||||
mkdir -p _temp_full/completions
|
||||
|
||||
# Extract QML source to temp directory
|
||||
tar -xzf _release_assets/dms-qml.tar.gz -C _temp_full/dms
|
||||
|
||||
# Copy CLI binary if it exists
|
||||
if [ -f "_dms_assets/dms-${arch}.gz" ]; then
|
||||
gunzip -c "_dms_assets/dms-${arch}.gz" > _temp_full/bin/dms
|
||||
chmod +x _temp_full/bin/dms
|
||||
fi
|
||||
|
||||
# Copy distropkg binary if it exists
|
||||
if [ -f "_dms_assets/dms-distropkg-${arch}.gz" ]; then
|
||||
gunzip -c "_dms_assets/dms-distropkg-${arch}.gz" > _temp_full/bin/dms-distropkg
|
||||
chmod +x _temp_full/bin/dms-distropkg
|
||||
fi
|
||||
|
||||
# Create INSTALL.md
|
||||
for completion in _dms_assets/completion.*; do
|
||||
if [ -f "$completion" ]; then
|
||||
cp "$completion" _temp_full/completions/
|
||||
fi
|
||||
done
|
||||
cat > _temp_full/INSTALL.md << 'EOF'
|
||||
# DankMaterialShell Installation
|
||||
|
||||
@@ -176,16 +177,23 @@ jobs:
|
||||
2. **Install the DMS CLI binaries:**
|
||||
```bash
|
||||
sudo install -m 755 bin/dms /usr/local/bin/dms
|
||||
# or install to a local directory:
|
||||
mkdir -p ~/.local/bin
|
||||
install -m 755 bin/dms ~/.local/bin/dms
|
||||
```
|
||||
|
||||
3. **Start the shell:**
|
||||
3. **Install shell completions (optional):**
|
||||
```bash
|
||||
# Bash
|
||||
sudo install -m 644 completions/completion.bash /usr/share/bash-completion/completions/dms
|
||||
|
||||
# Fish
|
||||
sudo install -m 644 completions/completion.fish /usr/share/fish/vendor_completions.d/dms.fish
|
||||
|
||||
# Zsh
|
||||
sudo install -m 644 completions/completion.zsh /usr/share/zsh/site-functions/_dms
|
||||
```
|
||||
|
||||
4. **Start the shell:**
|
||||
```bash
|
||||
dms run
|
||||
# or directly with quickshell (will lack some dbus integrations & plugin management):
|
||||
quickshell -p ~/.config/quickshell/dms
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -53,7 +53,7 @@ Singleton {
|
||||
// ! Spotify and maybe some other apps report the wrong app id in toplevels, hardcode special case
|
||||
function moddedAppId(appId: string): string {
|
||||
if (appId === "Spotify")
|
||||
return "spotify-launcher"
|
||||
return "spotify"
|
||||
if (appId === "beepertexts")
|
||||
return "beeper"
|
||||
if (appId === "home assistant desktop")
|
||||
|
||||
@@ -56,6 +56,7 @@ Singleton {
|
||||
property string nightModeLocationProvider: ""
|
||||
|
||||
property var pinnedApps: []
|
||||
property var hiddenTrayIds: []
|
||||
property var recentColors: []
|
||||
property bool showThirdPartyPlugins: false
|
||||
property string launchPrefix: ""
|
||||
@@ -140,6 +141,7 @@ Singleton {
|
||||
nightModeUseIPLocation = settings.nightModeUseIPLocation !== undefined ? settings.nightModeUseIPLocation : false
|
||||
nightModeLocationProvider = settings.nightModeLocationProvider !== undefined ? settings.nightModeLocationProvider : ""
|
||||
pinnedApps = settings.pinnedApps !== undefined ? settings.pinnedApps : []
|
||||
hiddenTrayIds = settings.hiddenTrayIds !== undefined ? settings.hiddenTrayIds : []
|
||||
selectedGpuIndex = settings.selectedGpuIndex !== undefined ? settings.selectedGpuIndex : 0
|
||||
nvidiaGpuTempEnabled = settings.nvidiaGpuTempEnabled !== undefined ? settings.nvidiaGpuTempEnabled : false
|
||||
nonNvidiaGpuTempEnabled = settings.nonNvidiaGpuTempEnabled !== undefined ? settings.nonNvidiaGpuTempEnabled : false
|
||||
@@ -209,6 +211,7 @@ Singleton {
|
||||
"nightModeUseIPLocation": nightModeUseIPLocation,
|
||||
"nightModeLocationProvider": nightModeLocationProvider,
|
||||
"pinnedApps": pinnedApps,
|
||||
"hiddenTrayIds": hiddenTrayIds,
|
||||
"selectedGpuIndex": selectedGpuIndex,
|
||||
"nvidiaGpuTempEnabled": nvidiaGpuTempEnabled,
|
||||
"nonNvidiaGpuTempEnabled": nonNvidiaGpuTempEnabled,
|
||||
@@ -277,7 +280,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function cleanupUnusedKeys() {
|
||||
const validKeys = ["isLightMode", "wallpaperPath", "perMonitorWallpaper", "monitorWallpapers", "perModeWallpaper", "wallpaperPathLight", "wallpaperPathDark", "monitorWallpapersLight", "monitorWallpapersDark", "doNotDisturb", "nightModeEnabled", "nightModeTemperature", "nightModeHighTemperature", "nightModeAutoEnabled", "nightModeAutoMode", "nightModeStartHour", "nightModeStartMinute", "nightModeEndHour", "nightModeEndMinute", "latitude", "longitude", "nightModeUseIPLocation", "nightModeLocationProvider", "pinnedApps", "selectedGpuIndex", "nvidiaGpuTempEnabled", "nonNvidiaGpuTempEnabled", "enabledGpuPciIds", "wallpaperCyclingEnabled", "wallpaperCyclingMode", "wallpaperCyclingInterval", "wallpaperCyclingTime", "monitorCyclingSettings", "lastBrightnessDevice", "brightnessExponentialDevices", "brightnessUserSetValues", "launchPrefix", "wallpaperTransition", "includedTransitions", "recentColors", "showThirdPartyPlugins", "configVersion"]
|
||||
const validKeys = ["isLightMode", "wallpaperPath", "perMonitorWallpaper", "monitorWallpapers", "perModeWallpaper", "wallpaperPathLight", "wallpaperPathDark", "monitorWallpapersLight", "monitorWallpapersDark", "doNotDisturb", "nightModeEnabled", "nightModeTemperature", "nightModeHighTemperature", "nightModeAutoEnabled", "nightModeAutoMode", "nightModeStartHour", "nightModeStartMinute", "nightModeEndHour", "nightModeEndMinute", "latitude", "longitude", "nightModeUseIPLocation", "nightModeLocationProvider", "pinnedApps", "hiddenTrayIds", "selectedGpuIndex", "nvidiaGpuTempEnabled", "nonNvidiaGpuTempEnabled", "enabledGpuPciIds", "wallpaperCyclingEnabled", "wallpaperCyclingMode", "wallpaperCyclingInterval", "wallpaperCyclingTime", "monitorCyclingSettings", "lastBrightnessDevice", "brightnessExponentialDevices", "brightnessUserSetValues", "brightnessExponentValues", "launchPrefix", "wallpaperTransition", "includedTransitions", "recentColors", "showThirdPartyPlugins", "configVersion"]
|
||||
|
||||
try {
|
||||
const content = settingsFile.text()
|
||||
@@ -639,6 +642,26 @@ Singleton {
|
||||
return appId && pinnedApps.indexOf(appId) !== -1
|
||||
}
|
||||
|
||||
function hideTrayId(trayId) {
|
||||
if (!trayId) return
|
||||
const current = [...hiddenTrayIds]
|
||||
if (current.indexOf(trayId) === -1) {
|
||||
current.push(trayId)
|
||||
hiddenTrayIds = current
|
||||
saveSettings()
|
||||
}
|
||||
}
|
||||
|
||||
function showTrayId(trayId) {
|
||||
if (!trayId) return
|
||||
hiddenTrayIds = hiddenTrayIds.filter(id => id !== trayId)
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function isHiddenTrayId(trayId) {
|
||||
return trayId && hiddenTrayIds.indexOf(trayId) !== -1
|
||||
}
|
||||
|
||||
function addRecentColor(color) {
|
||||
const colorStr = color.toString()
|
||||
let recent = recentColors.slice()
|
||||
|
||||
@@ -66,7 +66,6 @@ Singleton {
|
||||
property real popupTransparency: 1.0
|
||||
property real dockTransparency: 1
|
||||
property string widgetBackgroundColor: "sch"
|
||||
property string surfaceBase: "s"
|
||||
property real cornerRadius: 12
|
||||
|
||||
property bool use24HourClock: true
|
||||
@@ -225,6 +224,7 @@ Singleton {
|
||||
property int batterySuspendTimeout: 0
|
||||
property int batterySuspendBehavior: SettingsData.SuspendBehavior.Suspend
|
||||
property bool lockBeforeSuspend: false
|
||||
property bool preventIdleForMedia: false
|
||||
property bool loginctlLockIntegration: true
|
||||
property string launchPrefix: ""
|
||||
property var brightnessDevicePins: ({})
|
||||
@@ -240,6 +240,7 @@ Singleton {
|
||||
property int dockPosition: SettingsData.Position.Bottom
|
||||
property real dockSpacing: 4
|
||||
property real dockBottomGap: 0
|
||||
property real dockMargin: 0
|
||||
property real dockIconSize: 40
|
||||
property string dockIndicatorStyle: "circle"
|
||||
|
||||
@@ -259,11 +260,15 @@ Singleton {
|
||||
property bool dankBarSquareCorners: false
|
||||
property bool dankBarNoBackground: false
|
||||
property bool dankBarGothCornersEnabled: false
|
||||
property bool dankBarGothCornerRadiusOverride: false
|
||||
property real dankBarGothCornerRadiusValue: 12
|
||||
property bool dankBarBorderEnabled: false
|
||||
property string dankBarBorderColor: "surfaceText"
|
||||
property real dankBarBorderOpacity: 1.0
|
||||
property real dankBarBorderThickness: 1
|
||||
|
||||
onDankBarGothCornerRadiusOverrideChanged: saveSettings()
|
||||
onDankBarGothCornerRadiusValueChanged: saveSettings()
|
||||
onDankBarBorderColorChanged: saveSettings()
|
||||
onDankBarBorderOpacityChanged: saveSettings()
|
||||
onDankBarBorderThicknessChanged: saveSettings()
|
||||
@@ -287,6 +292,8 @@ Singleton {
|
||||
property bool osdAlwaysShowValue: false
|
||||
|
||||
property bool powerActionConfirm: true
|
||||
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
|
||||
property string powerMenuDefaultAction: "logout"
|
||||
property string customPowerActionLock: ""
|
||||
property string customPowerActionLogout: ""
|
||||
property string customPowerActionSuspend: ""
|
||||
@@ -617,12 +624,6 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
|
||||
}
|
||||
}
|
||||
|
||||
function setSurfaceBase(base) {
|
||||
set("surfaceBase", base)
|
||||
if (typeof Theme !== "undefined") {
|
||||
Theme.generateSystemThemesFromCurrentTheme()
|
||||
}
|
||||
}
|
||||
|
||||
function setCornerRadius(radius) {
|
||||
set("cornerRadius", radius)
|
||||
@@ -805,10 +806,12 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
|
||||
}
|
||||
|
||||
function setPluginSetting(pluginId, key, value) {
|
||||
if (!pluginSettings[pluginId]) {
|
||||
pluginSettings[pluginId] = {}
|
||||
const updated = JSON.parse(JSON.stringify(pluginSettings))
|
||||
if (!updated[pluginId]) {
|
||||
updated[pluginId] = {}
|
||||
}
|
||||
pluginSettings[pluginId][key] = value
|
||||
updated[pluginId][key] = value
|
||||
pluginSettings = updated
|
||||
savePluginSettings()
|
||||
}
|
||||
|
||||
@@ -820,7 +823,8 @@ rm -rf '${home}'/.cache/icon-cache '${home}'/.cache/thumbnails 2>/dev/null || tr
|
||||
}
|
||||
|
||||
function getPluginSettingsForPlugin(pluginId) {
|
||||
return pluginSettings[pluginId] || {}
|
||||
const settings = pluginSettings[pluginId]
|
||||
return settings ? JSON.parse(JSON.stringify(settings)) : {}
|
||||
}
|
||||
|
||||
|
||||
|
||||
112
Common/Theme.qml
112
Common/Theme.qml
@@ -200,43 +200,52 @@ Singleton {
|
||||
"value": "scheme-tonal-spot",
|
||||
"label": "Tonal Spot",
|
||||
"description": I18n.tr("Balanced palette with focused accents (default).")
|
||||
}), ({
|
||||
"value": "scheme-vibrant-spot",
|
||||
"label": "Vibrant Spot",
|
||||
"description": I18n.tr("Lively palette with saturated accents.")
|
||||
}), ({
|
||||
"value": "scheme-dynamic-contrast",
|
||||
"label": "Dynamic Contrast",
|
||||
"description": I18n.tr("High-contrast palette for strong visual distinction.")
|
||||
}), ({
|
||||
"value": "scheme-content",
|
||||
"label": "Content",
|
||||
"description": I18n.tr("Derives colors that closely match the underlying image.")
|
||||
}), ({
|
||||
"value": "scheme-expressive",
|
||||
"label": "Expressive",
|
||||
"description": I18n.tr("Vibrant palette with playful saturation.")
|
||||
}), ({
|
||||
"value": "scheme-fidelity",
|
||||
"label": "Fidelity",
|
||||
"description": I18n.tr("High-fidelity palette that preserves source hues.")
|
||||
}), ({
|
||||
"value": "scheme-fruit-salad",
|
||||
"label": "Fruit Salad",
|
||||
"description": I18n.tr("Colorful mix of bright contrasting accents.")
|
||||
}), ({
|
||||
"value": "scheme-monochrome",
|
||||
"label": "Monochrome",
|
||||
"description": I18n.tr("Minimal palette built around a single hue.")
|
||||
}), ({
|
||||
"value": "scheme-neutral",
|
||||
"label": "Neutral",
|
||||
"description": I18n.tr("Muted palette with subdued, calming tones.")
|
||||
}), ({
|
||||
"value": "scheme-rainbow",
|
||||
"label": "Rainbow",
|
||||
"description": I18n.tr("Diverse palette spanning the full spectrum.")
|
||||
})]
|
||||
}),
|
||||
({
|
||||
"value": "scheme-vibrant",
|
||||
"label": "Vibrant",
|
||||
"description": I18n.tr("Lively palette with saturated accents.")
|
||||
}),
|
||||
({
|
||||
"value": "scheme-dynamic-contrast",
|
||||
"label": "Dynamic Contrast",
|
||||
"description": I18n.tr("High-contrast palette for strong visual distinction.")
|
||||
}),
|
||||
({
|
||||
"value": "scheme-content",
|
||||
"label": "Content",
|
||||
"description": I18n.tr("Derives colors that closely match the underlying image.")
|
||||
}),
|
||||
({
|
||||
"value": "scheme-expressive",
|
||||
"label": "Expressive",
|
||||
"description": I18n.tr("Vibrant palette with playful saturation.")
|
||||
}),
|
||||
({
|
||||
"value": "scheme-fidelity",
|
||||
"label": "Fidelity",
|
||||
"description": I18n.tr("High-fidelity palette that preserves source hues.")
|
||||
}),
|
||||
({
|
||||
"value": "scheme-fruit-salad",
|
||||
"label": "Fruit Salad",
|
||||
"description": I18n.tr("Colorful mix of bright contrasting accents.")
|
||||
}),
|
||||
({
|
||||
"value": "scheme-monochrome",
|
||||
"label": "Monochrome",
|
||||
"description": I18n.tr("Minimal palette built around a single hue.")
|
||||
}),
|
||||
({
|
||||
"value": "scheme-neutral",
|
||||
"label": "Neutral",
|
||||
"description": I18n.tr("Muted palette with subdued, calming tones.")
|
||||
}),
|
||||
({
|
||||
"value": "scheme-rainbow",
|
||||
"label": "Rainbow",
|
||||
"description": I18n.tr("Diverse palette spanning the full spectrum.")
|
||||
})]
|
||||
|
||||
function getMatugenScheme(value) {
|
||||
const schemes = availableMatugenSchemes
|
||||
@@ -251,12 +260,7 @@ Singleton {
|
||||
property color primaryText: currentThemeData.primaryText
|
||||
property color primaryContainer: currentThemeData.primaryContainer
|
||||
property color secondary: currentThemeData.secondary
|
||||
property color surface: {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.surfaceBase === "s") {
|
||||
return currentThemeData.background
|
||||
}
|
||||
return currentThemeData.surface
|
||||
}
|
||||
property color surface: currentThemeData.surface
|
||||
property color surfaceText: currentThemeData.surfaceText
|
||||
property color surfaceVariant: currentThemeData.surfaceVariant
|
||||
property color surfaceVariantText: currentThemeData.surfaceVariantText
|
||||
@@ -265,24 +269,9 @@ Singleton {
|
||||
property color backgroundText: currentThemeData.backgroundText
|
||||
property color outline: currentThemeData.outline
|
||||
property color outlineVariant: currentThemeData.outlineVariant || Qt.rgba(outline.r, outline.g, outline.b, 0.6)
|
||||
property color surfaceContainer: {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.surfaceBase === "s") {
|
||||
return currentThemeData.surface
|
||||
}
|
||||
return currentThemeData.surfaceContainer
|
||||
}
|
||||
property color surfaceContainerHigh: {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.surfaceBase === "s") {
|
||||
return currentThemeData.surfaceContainer
|
||||
}
|
||||
return currentThemeData.surfaceContainerHigh
|
||||
}
|
||||
property color surfaceContainerHighest: {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.surfaceBase === "s") {
|
||||
return currentThemeData.surfaceContainerHigh
|
||||
}
|
||||
return currentThemeData.surfaceContainerHighest
|
||||
}
|
||||
property color surfaceContainer: currentThemeData.surfaceContainer
|
||||
property color surfaceContainerHigh: currentThemeData.surfaceContainerHigh
|
||||
property color surfaceContainerHighest: currentThemeData.surfaceContainerHighest
|
||||
|
||||
property color onSurface: surfaceText
|
||||
property color onSurfaceVariant: surfaceVariantText
|
||||
@@ -781,7 +770,6 @@ Singleton {
|
||||
"mode": isLight ? "light" : "dark",
|
||||
"iconTheme": iconTheme || "System Default",
|
||||
"matugenType": matugenType || "scheme-tonal-spot",
|
||||
"surfaceBase": (typeof SettingsData !== "undefined" && SettingsData.surfaceBase) ? SettingsData.surfaceBase : "sc",
|
||||
"runUserTemplates": (typeof SettingsData !== "undefined") ? SettingsData.runUserMatugenTemplates : true
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ var SPEC = {
|
||||
dockTransparency: { def: 1.0, coerce: percentToUnit },
|
||||
|
||||
widgetBackgroundColor: { def: "sch" },
|
||||
surfaceBase: { def: "s", onChange: "regenSystemThemes" },
|
||||
cornerRadius: { def: 12, onChange: "updateNiriLayout" },
|
||||
|
||||
use24HourClock: { def: true },
|
||||
@@ -142,6 +141,7 @@ var SPEC = {
|
||||
batterySuspendTimeout: { def: 0 },
|
||||
batterySuspendBehavior: { def: 0 },
|
||||
lockBeforeSuspend: { def: false },
|
||||
preventIdleForMedia: { def: false },
|
||||
loginctlLockIntegration: { def: true },
|
||||
launchPrefix: { def: "" },
|
||||
brightnessDevicePins: { def: {} },
|
||||
@@ -157,6 +157,7 @@ var SPEC = {
|
||||
dockPosition: { def: 1 },
|
||||
dockSpacing: { def: 4 },
|
||||
dockBottomGap: { def: 0 },
|
||||
dockMargin: { def: 0 },
|
||||
dockIconSize: { def: 40 },
|
||||
dockIndicatorStyle: { def: "circle" },
|
||||
|
||||
@@ -176,6 +177,8 @@ var SPEC = {
|
||||
dankBarSquareCorners: { def: false, migrate: ["topBarSquareCorners"] },
|
||||
dankBarNoBackground: { def: false, migrate: ["topBarNoBackground"] },
|
||||
dankBarGothCornersEnabled: { def: false, migrate: ["topBarGothCornersEnabled"] },
|
||||
dankBarGothCornerRadiusOverride: { def: false },
|
||||
dankBarGothCornerRadiusValue: { def: 12 },
|
||||
dankBarBorderEnabled: { def: false },
|
||||
dankBarBorderColor: { def: "surfaceText" },
|
||||
dankBarBorderOpacity: { def: 1.0 },
|
||||
@@ -200,6 +203,8 @@ var SPEC = {
|
||||
osdAlwaysShowValue: { def: false },
|
||||
|
||||
powerActionConfirm: { def: true },
|
||||
powerMenuActions: { def: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] },
|
||||
powerMenuDefaultAction: { def: "logout" },
|
||||
customPowerActionLock: { def: "" },
|
||||
customPowerActionLogout: { def: "" },
|
||||
customPowerActionSuspend: { def: "" },
|
||||
|
||||
10
DMSShell.qml
10
DMSShell.qml
@@ -482,6 +482,10 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
onLockRequested: {
|
||||
lock.activate()
|
||||
}
|
||||
|
||||
function actionApply(action) {
|
||||
switch (action) {
|
||||
case "logout":
|
||||
@@ -513,11 +517,11 @@ Item {
|
||||
|
||||
active: false
|
||||
|
||||
HyprKeybindsModal {
|
||||
id: hyprKeybindsModal
|
||||
KeybindsModal {
|
||||
id: keybindsModal
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.hyprKeybindsModal = hyprKeybindsModal
|
||||
PopoutService.hyprKeybindsModal = keybindsModal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,11 +306,58 @@ Item {
|
||||
target: "mpris"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function toggle(provider: string): string {
|
||||
if (!provider) {
|
||||
return "ERROR: No provider specified"
|
||||
}
|
||||
|
||||
KeybindsService.loadProvider(provider)
|
||||
root.hyprKeybindsModalLoader.active = true
|
||||
|
||||
if (root.hyprKeybindsModalLoader.item) {
|
||||
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
|
||||
root.hyprKeybindsModalLoader.item.close()
|
||||
} else {
|
||||
root.hyprKeybindsModalLoader.item.open()
|
||||
}
|
||||
return `KEYBINDS_TOGGLE_SUCCESS: ${provider}`
|
||||
}
|
||||
return `KEYBINDS_TOGGLE_FAILED: ${provider}`
|
||||
}
|
||||
|
||||
function open(provider: string): string {
|
||||
if (!provider) {
|
||||
return "ERROR: No provider specified"
|
||||
}
|
||||
|
||||
KeybindsService.loadProvider(provider)
|
||||
root.hyprKeybindsModalLoader.active = true
|
||||
|
||||
if (root.hyprKeybindsModalLoader.item) {
|
||||
root.hyprKeybindsModalLoader.item.open()
|
||||
return `KEYBINDS_OPEN_SUCCESS: ${provider}`
|
||||
}
|
||||
return `KEYBINDS_OPEN_FAILED: ${provider}`
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
if (root.hyprKeybindsModalLoader.item) {
|
||||
root.hyprKeybindsModalLoader.item.close()
|
||||
return "KEYBINDS_CLOSE_SUCCESS"
|
||||
}
|
||||
return "KEYBINDS_CLOSE_FAILED"
|
||||
}
|
||||
|
||||
target: "keybinds"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function openBinds(): string {
|
||||
if (!CompositorService.isHyprland) {
|
||||
return "HYPR_NOT_AVAILABLE"
|
||||
}
|
||||
KeybindsService.loadProvider("hyprland")
|
||||
root.hyprKeybindsModalLoader.active = true
|
||||
if (root.hyprKeybindsModalLoader.item) {
|
||||
root.hyprKeybindsModalLoader.item.open()
|
||||
@@ -334,6 +381,7 @@ Item {
|
||||
if (!CompositorService.isHyprland) {
|
||||
return "HYPR_NOT_AVAILABLE"
|
||||
}
|
||||
KeybindsService.loadProvider("hyprland")
|
||||
root.hyprKeybindsModalLoader.active = true
|
||||
if (root.hyprKeybindsModalLoader.item) {
|
||||
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
|
||||
|
||||
694
LICENSE
694
LICENSE
@@ -1,674 +1,20 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Avenge Media LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR
|
||||
|
||||
23
LICENSE_CHANGE_12_11_2025.md
Normal file
23
LICENSE_CHANGE_12_11_2025.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# DankMaterialShell License Change
|
||||
|
||||
Relicensing from GPL-3.0 to MIT.
|
||||
|
||||
Consent and documentation is available in [pull request #686](https://github.com/AvengeMedia/DankMaterialShell/pull/686) and is summarized below.
|
||||
|
||||
## Contributors
|
||||
|
||||
The project has 21 contributors requiring consent:
|
||||
- 2 primary authors (10k+ lines each)
|
||||
- 7 major contributors (1k+ lines or 10+ commits)
|
||||
- 12 significant contributors (100+ lines)
|
||||
- 2 derivative works (code adapted from other projects)
|
||||
|
||||
Contributors with <50 lines are excluded as they don't meet copyright thresholds. Automated bots don't hold copyright.
|
||||
|
||||
## Consent Status
|
||||
|
||||
All 21 contributors have provided consent. Three contributions were sufficiently reverted or rewritten by maintainers, making them eligible without original author consent.
|
||||
|
||||
## Reasoning
|
||||
|
||||
GPL-3.0 may require plugins to also be GPL-3.0 licensed. MIT allows plugin authors to choose their own licenses.
|
||||
@@ -4,6 +4,7 @@ import Quickshell.Hyprland
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
@@ -52,11 +53,15 @@ PanelWindow {
|
||||
closeTimer.stop()
|
||||
shouldBeVisible = true
|
||||
visible = true
|
||||
focusScope.forceActiveFocus()
|
||||
shouldHaveFocus = false
|
||||
Qt.callLater(() => {
|
||||
shouldHaveFocus = Qt.binding(() => shouldBeVisible)
|
||||
})
|
||||
}
|
||||
|
||||
function close() {
|
||||
shouldBeVisible = false
|
||||
shouldHaveFocus = false
|
||||
closeTimer.restart()
|
||||
}
|
||||
|
||||
@@ -70,7 +75,18 @@ PanelWindow {
|
||||
|
||||
visible: shouldBeVisible
|
||||
color: "transparent"
|
||||
WlrLayershell.layer: WlrLayershell.Top // if set to overlay -> virtual keyboards can be stuck under modal
|
||||
WlrLayershell.layer: {
|
||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||
case "bottom":
|
||||
return WlrLayershell.Bottom
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay
|
||||
case "background":
|
||||
return WlrLayershell.Background
|
||||
default:
|
||||
return WlrLayershell.Top
|
||||
}
|
||||
}
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: shouldHaveFocus ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||
onVisibleChanged: {
|
||||
@@ -139,12 +155,11 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: contentContainer
|
||||
Item {
|
||||
id: modalContainer
|
||||
|
||||
width: Theme.px(root.width, dpr)
|
||||
height: Theme.px(root.height, dpr)
|
||||
anchors.centerIn: undefined
|
||||
x: {
|
||||
if (positioning === "center") {
|
||||
return Theme.snap((root.screenWidth - width) / 2, dpr)
|
||||
@@ -165,101 +180,96 @@ PanelWindow {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
color: root.backgroundColor
|
||||
radius: root.cornerRadius
|
||||
border.color: root.borderColor
|
||||
border.width: root.borderWidth
|
||||
clip: false
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
layer.textureSize: Qt.size(width * Math.max(2, root.screen?.devicePixelRatio || 1), height * Math.max(2, root.screen?.devicePixelRatio || 1))
|
||||
layer.samples: 4
|
||||
opacity: root.shouldBeVisible ? 1 : 0
|
||||
transform: [scaleTransform, motionTransform]
|
||||
|
||||
Scale {
|
||||
id: scaleTransform
|
||||
readonly property bool slide: root.animationType === "slide"
|
||||
readonly property real offsetX: slide ? 15 : 0
|
||||
readonly property real offsetY: slide ? -30 : root.animationOffset
|
||||
|
||||
origin.x: contentContainer.width / 2
|
||||
origin.y: contentContainer.height / 2
|
||||
xScale: root.shouldBeVisible ? 1 : root.animationScaleCollapsed
|
||||
yScale: root.shouldBeVisible ? 1 : root.animationScaleCollapsed
|
||||
property real animX: 0
|
||||
property real animY: 0
|
||||
property real scaleValue: root.animationScaleCollapsed
|
||||
|
||||
Behavior on xScale {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
|
||||
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
|
||||
|
||||
Behavior on yScale {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr)
|
||||
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr)
|
||||
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed
|
||||
}
|
||||
}
|
||||
|
||||
Translate {
|
||||
id: motionTransform
|
||||
|
||||
readonly property bool slide: root.animationType === "slide"
|
||||
readonly property real hiddenX: slide ? 15 : 0
|
||||
readonly property real hiddenY: slide ? -30 : root.animationOffset
|
||||
|
||||
x: Theme.snap(root.shouldBeVisible ? 0 : hiddenX, root.dpr)
|
||||
y: Theme.snap(root.shouldBeVisible ? 0 : hiddenY, root.dpr)
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Behavior on animX {
|
||||
NumberAnimation {
|
||||
duration: animationDuration
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: root.shouldBeVisible
|
||||
Behavior on animY {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scaleValue {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
clip: false
|
||||
layer.enabled: true
|
||||
layer.smooth: false
|
||||
layer.textureSize: Qt.size(width * root.dpr, height * root.dpr)
|
||||
opacity: root.shouldBeVisible ? 1 : 0
|
||||
scale: modalContainer.scaleValue
|
||||
x: Theme.snap(modalContainer.animX + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5, root.dpr)
|
||||
y: Theme.snap(modalContainer.animY + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5, root.dpr)
|
||||
|
||||
Item {
|
||||
id: directContentWrapper
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
DankRectangle {
|
||||
anchors.fill: parent
|
||||
visible: root.directContent !== null
|
||||
focus: true
|
||||
color: root.backgroundColor
|
||||
borderColor: root.borderColor
|
||||
borderWidth: root.borderWidth
|
||||
radius: root.cornerRadius
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: root.shouldBeVisible
|
||||
clip: false
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.directContent) {
|
||||
root.directContent.parent = directContentWrapper
|
||||
root.directContent.anchors.fill = directContentWrapper
|
||||
Qt.callLater(() => root.directContent.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
Item {
|
||||
id: directContentWrapper
|
||||
|
||||
Connections {
|
||||
function onDirectContentChanged() {
|
||||
anchors.fill: parent
|
||||
visible: root.directContent !== null
|
||||
focus: true
|
||||
clip: false
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.directContent) {
|
||||
root.directContent.parent = directContentWrapper
|
||||
root.directContent.anchors.fill = directContentWrapper
|
||||
@@ -267,23 +277,33 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
target: root
|
||||
Connections {
|
||||
function onDirectContentChanged() {
|
||||
if (root.directContent) {
|
||||
root.directContent.parent = directContentWrapper
|
||||
root.directContent.anchors.fill = directContentWrapper
|
||||
Qt.callLater(() => root.directContent.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
Loader {
|
||||
id: contentLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || root.visible)
|
||||
asynchronous: false
|
||||
focus: true
|
||||
clip: false
|
||||
visible: root.directContent === null
|
||||
anchors.fill: parent
|
||||
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || root.visible)
|
||||
asynchronous: false
|
||||
focus: true
|
||||
clip: false
|
||||
visible: root.directContent === null
|
||||
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -303,20 +323,5 @@ PanelWindow {
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
onVisibleChanged: {
|
||||
if (visible && shouldHaveFocus) {
|
||||
Qt.callLater(() => focusScope.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onShouldHaveFocusChanged() {
|
||||
if (shouldHaveFocus && shouldBeVisible) {
|
||||
Qt.callLater(() => focusScope.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,12 +106,13 @@ DankModal {
|
||||
}
|
||||
|
||||
width: 680
|
||||
height: 680
|
||||
height: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 680
|
||||
backgroundColor: Theme.surfaceContainer
|
||||
cornerRadius: Theme.cornerRadius
|
||||
borderColor: Theme.outlineMedium
|
||||
borderWidth: 1
|
||||
keepContentLoaded: true
|
||||
allowStacking: true
|
||||
|
||||
onBackgroundClicked: hide()
|
||||
|
||||
@@ -122,6 +123,7 @@ DankModal {
|
||||
property alias hexInput: hexInput
|
||||
|
||||
anchors.fill: parent
|
||||
implicitHeight: mainColumn.implicitHeight
|
||||
focus: true
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
@@ -130,7 +132,10 @@ DankModal {
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
id: mainColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
@@ -444,103 +449,212 @@ DankModal {
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Hex:")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankTextField {
|
||||
id: hexInput
|
||||
width: 120
|
||||
height: 38
|
||||
text: root.currentColor.toString()
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: {
|
||||
if (text.length === 0) return Theme.surfaceText
|
||||
const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/
|
||||
return hexPattern.test(text) ? Theme.surfaceText : Theme.error
|
||||
Column {
|
||||
width: (parent.width - Theme.spacingM * 2) / 3
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Hex")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankTextField {
|
||||
id: hexInput
|
||||
width: parent.width - 36
|
||||
height: 36
|
||||
text: root.currentColor.toString()
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
textColor: {
|
||||
if (text.length === 0) return Theme.surfaceText
|
||||
const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/
|
||||
return hexPattern.test(text) ? Theme.surfaceText : Theme.error
|
||||
}
|
||||
placeholderText: "#000000"
|
||||
backgroundColor: Theme.surfaceHover
|
||||
borderWidth: 1
|
||||
focusedBorderWidth: 2
|
||||
topPadding: Theme.spacingS
|
||||
bottomPadding: Theme.spacingS
|
||||
onAccepted: () => {
|
||||
const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/
|
||||
if (!hexPattern.test(text)) return
|
||||
const color = Qt.color(text)
|
||||
if (color) {
|
||||
root.selectedColor = color
|
||||
root.currentColor = color
|
||||
root.updateFromColor(color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "content_copy"
|
||||
iconSize: Theme.iconSize - 6
|
||||
iconColor: Theme.surfaceText
|
||||
buttonSize: 36
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: () => {
|
||||
root.copyColorToClipboard(hexInput.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
placeholderText: "#000000"
|
||||
backgroundColor: Theme.surfaceHover
|
||||
borderWidth: 1
|
||||
focusedBorderWidth: 2
|
||||
topPadding: Theme.spacingS
|
||||
bottomPadding: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onAccepted: () => {
|
||||
const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/
|
||||
if (!hexPattern.test(text)) return
|
||||
const color = Qt.color(text)
|
||||
if (color) {
|
||||
root.selectedColor = color
|
||||
root.currentColor = color
|
||||
root.updateFromColor(color)
|
||||
|
||||
Column {
|
||||
width: (parent.width - Theme.spacingM * 2) / 3
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("RGB")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceHover
|
||||
border.color: Theme.outline
|
||||
border.width: 1
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
const r = Math.round(root.currentColor.r * 255)
|
||||
const g = Math.round(root.currentColor.g * 255)
|
||||
const b = Math.round(root.currentColor.b * 255)
|
||||
if (root.alpha < 1) {
|
||||
const a = Math.round(root.alpha * 255)
|
||||
return `${r}, ${g}, ${b}, ${a}`
|
||||
}
|
||||
return `${r}, ${g}, ${b}`
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "content_copy"
|
||||
iconSize: Theme.iconSize - 6
|
||||
iconColor: Theme.surfaceText
|
||||
buttonSize: 36
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: () => {
|
||||
const r = Math.round(root.currentColor.r * 255)
|
||||
const g = Math.round(root.currentColor.g * 255)
|
||||
const b = Math.round(root.currentColor.b * 255)
|
||||
let rgbString
|
||||
if (root.alpha < 1) {
|
||||
const a = Math.round(root.alpha * 255)
|
||||
rgbString = `rgba(${r}, ${g}, ${b}, ${a})`
|
||||
} else {
|
||||
rgbString = `rgb(${r}, ${g}, ${b})`
|
||||
}
|
||||
Quickshell.execDetached(["sh", "-c", `echo "${rgbString}" | wl-copy`])
|
||||
ToastService.showInfo(`${rgbString} copied`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: (parent.width - Theme.spacingM * 2) / 3
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("HSV")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceHover
|
||||
border.color: Theme.outline
|
||||
border.width: 1
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
const h = Math.round(root.hue * 360)
|
||||
const s = Math.round(root.saturation * 100)
|
||||
const v = Math.round(root.value * 100)
|
||||
if (root.alpha < 1) {
|
||||
const a = Math.round(root.alpha * 100)
|
||||
return `${h}°, ${s}%, ${v}%, ${a}%`
|
||||
}
|
||||
return `${h}°, ${s}%, ${v}%`
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "content_copy"
|
||||
iconSize: Theme.iconSize - 6
|
||||
iconColor: Theme.surfaceText
|
||||
buttonSize: 36
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: () => {
|
||||
const h = Math.round(root.hue * 360)
|
||||
const s = Math.round(root.saturation * 100)
|
||||
const v = Math.round(root.value * 100)
|
||||
let hsvString
|
||||
if (root.alpha < 1) {
|
||||
const a = Math.round(root.alpha * 100)
|
||||
hsvString = `${h}, ${s}, ${v}, ${a}`
|
||||
} else {
|
||||
hsvString = `${h}, ${s}, ${v}`
|
||||
}
|
||||
Quickshell.execDetached(["sh", "-c", `echo "${hsvString}" | wl-copy`])
|
||||
ToastService.showInfo(`HSV ${hsvString} copied`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankButton {
|
||||
width: 80
|
||||
buttonHeight: 36
|
||||
text: I18n.tr("Apply")
|
||||
backgroundColor: Theme.primary
|
||||
textColor: Theme.background
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: {
|
||||
const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/
|
||||
if (!hexPattern.test(hexInput.text)) return
|
||||
const color = Qt.color(hexInput.text)
|
||||
if (color) {
|
||||
root.currentColor = color
|
||||
root.updateFromColor(color)
|
||||
root.selectedColor = root.currentColor
|
||||
root.colorSelected(root.currentColor)
|
||||
SessionData.addRecentColor(root.currentColor)
|
||||
root.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - 460
|
||||
height: 1
|
||||
}
|
||||
|
||||
DankButton {
|
||||
visible: root.onColorSelectedCallback !== null && root.onColorSelectedCallback !== undefined
|
||||
width: 70
|
||||
buttonHeight: 36
|
||||
text: I18n.tr("Cancel")
|
||||
backgroundColor: "transparent"
|
||||
textColor: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: root.hide()
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
border.color: Theme.surfaceVariantAlpha
|
||||
border.width: 1
|
||||
z: -1
|
||||
}
|
||||
}
|
||||
|
||||
DankButton {
|
||||
width: 70
|
||||
buttonHeight: 36
|
||||
text: I18n.tr("Copy")
|
||||
text: I18n.tr("Save")
|
||||
backgroundColor: Theme.primary
|
||||
textColor: Theme.background
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
onClicked: {
|
||||
const colorString = root.currentColor.toString()
|
||||
root.copyColorToClipboard(colorString)
|
||||
SessionData.addRecentColor(root.currentColor)
|
||||
root.colorSelected(root.currentColor)
|
||||
root.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
246
Modals/DisplayConfirmationModal.qml
Normal file
246
Modals/DisplayConfirmationModal.qml
Normal file
@@ -0,0 +1,246 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
property string outputName: ""
|
||||
property var position: undefined
|
||||
property var mode: undefined
|
||||
property var vrr: undefined
|
||||
property int countdown: 15
|
||||
|
||||
shouldBeVisible: false
|
||||
allowStacking: true
|
||||
width: 420
|
||||
height: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 200
|
||||
|
||||
Timer {
|
||||
id: countdownTimer
|
||||
interval: 1000
|
||||
repeat: true
|
||||
running: root.shouldBeVisible
|
||||
onTriggered: {
|
||||
countdown--
|
||||
if (countdown <= 0) {
|
||||
revert()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
countdown = 15
|
||||
countdownTimer.start()
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
countdownTimer.stop()
|
||||
}
|
||||
|
||||
onBackgroundClicked: revert
|
||||
|
||||
content: Component {
|
||||
FocusScope {
|
||||
id: confirmContent
|
||||
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
implicitHeight: mainColumn.implicitHeight
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
revert()
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: event => {
|
||||
confirm()
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Confirm Display Changes")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Display settings for ") + outputName
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHighest
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Reverting in:")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: countdown + "s"
|
||||
font.pixelSize: Theme.fontSizeXLarge * 1.5
|
||||
color: Theme.primary
|
||||
font.weight: Font.Bold
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Changes:")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: position !== undefined && position !== null
|
||||
text: I18n.tr("Position: ") + (position ? position.x + ", " + position.y : "")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: mode !== undefined && mode !== null && mode !== ""
|
||||
text: I18n.tr("Mode: ") + (mode || "")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: vrr !== undefined && vrr !== null
|
||||
text: I18n.tr("VRR: ") + (vrr ? I18n.tr("Enabled") : I18n.tr("Disabled"))
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 36
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(70, revertText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: revertArea.containsMouse ? Theme.surfaceTextHover : "transparent"
|
||||
border.color: Theme.surfaceVariantAlpha
|
||||
border.width: 1
|
||||
|
||||
StyledText {
|
||||
id: revertText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Revert")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: revertArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: revert
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(80, confirmText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: confirmArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
|
||||
StyledText {
|
||||
id: confirmText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Keep Changes")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: confirmArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: confirm
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: revert
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
displaysTab.confirmChanges()
|
||||
close()
|
||||
}
|
||||
|
||||
function revert() {
|
||||
displaysTab.revertChanges()
|
||||
close()
|
||||
}
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
layerNamespace: "dms:hyprkeybinds"
|
||||
|
||||
width: 1400
|
||||
height: 900
|
||||
onBackgroundClicked: close()
|
||||
|
||||
function categorizeKeybinds() {
|
||||
const categories = {
|
||||
"Workspace": [],
|
||||
"Window": [],
|
||||
"Monitor": [],
|
||||
"Execute": [],
|
||||
"System": [],
|
||||
"Other": []
|
||||
}
|
||||
|
||||
function addKeybind(keybind) {
|
||||
const dispatcher = keybind.dispatcher || ""
|
||||
if (dispatcher.includes("workspace")) {
|
||||
categories["Workspace"].push(keybind)
|
||||
} else if (dispatcher.includes("monitor")) {
|
||||
categories["Monitor"].push(keybind)
|
||||
} else if (dispatcher.includes("window") || dispatcher.includes("focus") || dispatcher.includes("move") || dispatcher.includes("swap") || dispatcher.includes("resize") || dispatcher === "killactive" || dispatcher === "fullscreen" || dispatcher === "togglefloating") {
|
||||
categories["Window"].push(keybind)
|
||||
} else if (dispatcher === "exec") {
|
||||
categories["Execute"].push(keybind)
|
||||
} else if (dispatcher === "exit" || dispatcher.includes("dpms")) {
|
||||
categories["System"].push(keybind)
|
||||
} else {
|
||||
categories["Other"].push(keybind)
|
||||
}
|
||||
}
|
||||
|
||||
const allKeybinds = HyprKeybindsService.keybinds.keybinds || []
|
||||
for (let i = 0; i < allKeybinds.length; i++) {
|
||||
addKeybind(allKeybinds[i])
|
||||
}
|
||||
|
||||
const children = HyprKeybindsService.keybinds.children || []
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i]
|
||||
const childKeybinds = child.keybinds || []
|
||||
for (let j = 0; j < childKeybinds.length; j++) {
|
||||
addKeybind(childKeybinds[j])
|
||||
}
|
||||
}
|
||||
|
||||
categories["Workspace"].sort((a, b) => {
|
||||
const dispA = a.dispatcher || ""
|
||||
const dispB = b.dispatcher || ""
|
||||
return dispA.localeCompare(dispB)
|
||||
})
|
||||
|
||||
categories["Window"].sort((a, b) => {
|
||||
const dispA = a.dispatcher || ""
|
||||
const dispB = b.dispatcher || ""
|
||||
return dispA.localeCompare(dispB)
|
||||
})
|
||||
|
||||
categories["Monitor"].sort((a, b) => {
|
||||
const dispA = a.dispatcher || ""
|
||||
const dispB = b.dispatcher || ""
|
||||
return dispA.localeCompare(dispB)
|
||||
})
|
||||
|
||||
categories["Execute"].sort((a, b) => {
|
||||
const modsA = a.mods || []
|
||||
const keyA = a.key || ""
|
||||
const bindA = [...modsA, keyA].join("+")
|
||||
|
||||
const modsB = b.mods || []
|
||||
const keyB = b.key || ""
|
||||
const bindB = [...modsB, keyB].join("+")
|
||||
|
||||
return bindA.localeCompare(bindB)
|
||||
})
|
||||
|
||||
return categories
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
DankFlickable {
|
||||
id: mainFlickable
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
contentWidth: rowLayout.implicitWidth
|
||||
contentHeight: rowLayout.implicitHeight
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
id: rowLayout
|
||||
spacing: Theme.spacingM
|
||||
|
||||
property var categories: root.categorizeKeybinds()
|
||||
property real columnWidth: (mainFlickable.width - spacing * 2) / 3
|
||||
|
||||
Column {
|
||||
width: rowLayout.columnWidth
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: "Window / Monitor"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.primary
|
||||
opacity: 0.3
|
||||
}
|
||||
|
||||
Item { width: 1; height: Theme.spacingXS }
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: [...(rowLayout.categories["Window"] || []), ...(rowLayout.categories["Monitor"] || [])]
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledRect {
|
||||
width: Math.min(140, parent.width * 0.42)
|
||||
height: 22
|
||||
radius: 4
|
||||
opacity: 0.3
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
anchors.margins: 2
|
||||
width: parent.width - 4
|
||||
text: {
|
||||
const mods = modelData.mods || []
|
||||
const key = modelData.key || ""
|
||||
const parts = [...mods, key]
|
||||
return parts.join("+")
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
isMonospace: true
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width - 150
|
||||
text: {
|
||||
const comment = modelData.comment || ""
|
||||
if (comment) return comment
|
||||
|
||||
const dispatcher = modelData.dispatcher || ""
|
||||
const params = modelData.params || ""
|
||||
return params ? `${dispatcher} ${params}` : dispatcher
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
opacity: 0.9
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ["Workspace", "Execute"]
|
||||
|
||||
Column {
|
||||
width: rowLayout.columnWidth
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.primary
|
||||
opacity: 0.3
|
||||
}
|
||||
|
||||
Item { width: 1; height: Theme.spacingXS }
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: rowLayout.categories[modelData] || []
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledRect {
|
||||
width: Math.min(140, parent.width * 0.42)
|
||||
height: 22
|
||||
radius: 4
|
||||
opacity: 0.3
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
anchors.margins: 2
|
||||
width: parent.width - 4
|
||||
text: {
|
||||
const mods = modelData.mods || []
|
||||
const key = modelData.key || ""
|
||||
const parts = [...mods, key]
|
||||
return parts.join("+")
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
isMonospace: true
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width - 150
|
||||
text: {
|
||||
const comment = modelData.comment || ""
|
||||
if (comment) return comment
|
||||
|
||||
const dispatcher = modelData.dispatcher || ""
|
||||
const params = modelData.params || ""
|
||||
return params ? `${dispatcher} ${params}` : dispatcher
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
opacity: 0.9
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
232
Modals/KeybindsModal.qml
Normal file
232
Modals/KeybindsModal.qml
Normal file
@@ -0,0 +1,232 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
layerNamespace: "dms:keybinds"
|
||||
property real scrollStep: 60
|
||||
property var activeFlickable: null
|
||||
property real _maxW: Math.min(Screen.width * 0.92, 1200)
|
||||
property real _maxH: Math.min(Screen.height * 0.92, 900)
|
||||
width: _maxW
|
||||
height: _maxH
|
||||
onBackgroundClicked: close()
|
||||
|
||||
function scrollDown() {
|
||||
if (!root.activeFlickable) return
|
||||
let newY = root.activeFlickable.contentY + scrollStep
|
||||
newY = Math.min(newY, root.activeFlickable.contentHeight - root.activeFlickable.height)
|
||||
root.activeFlickable.contentY = newY
|
||||
}
|
||||
|
||||
function scrollUp() {
|
||||
if (!root.activeFlickable) return
|
||||
let newY = root.activeFlickable.contentY - root.scrollStep
|
||||
newY = Math.max(0, newY)
|
||||
root.activeFlickable.contentY = newY
|
||||
}
|
||||
|
||||
Shortcut { sequence: "Ctrl+j"; onActivated: root.scrollDown() }
|
||||
Shortcut { sequence: "Down"; onActivated: root.scrollDown() }
|
||||
Shortcut { sequence: "Ctrl+k"; onActivated: root.scrollUp() }
|
||||
Shortcut { sequence: "Up"; onActivated: root.scrollUp() }
|
||||
Shortcut { sequence: "Esc"; onActivated: root.close() }
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
StyledText {
|
||||
text: KeybindsService.keybinds.title || "Keybinds"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
id: mainFlickable
|
||||
width: parent.width
|
||||
height: parent.height - parent.spacing - 40
|
||||
contentWidth: rowLayout.implicitWidth
|
||||
contentHeight: rowLayout.implicitHeight
|
||||
clip: true
|
||||
|
||||
Component.onCompleted: root.activeFlickable = mainFlickable
|
||||
|
||||
property var rawBinds: KeybindsService.keybinds.binds || {}
|
||||
property var categories: {
|
||||
const processed = {}
|
||||
for (const cat in rawBinds) {
|
||||
const binds = rawBinds[cat]
|
||||
const subcats = {}
|
||||
let hasSubcats = false
|
||||
|
||||
for (let i = 0; i < binds.length; i++) {
|
||||
const bind = binds[i]
|
||||
if (bind.subcat) {
|
||||
hasSubcats = true
|
||||
if (!subcats[bind.subcat]) {
|
||||
subcats[bind.subcat] = []
|
||||
}
|
||||
subcats[bind.subcat].push(bind)
|
||||
} else {
|
||||
if (!subcats["_root"]) {
|
||||
subcats["_root"] = []
|
||||
}
|
||||
subcats["_root"].push(bind)
|
||||
}
|
||||
}
|
||||
|
||||
processed[cat] = {
|
||||
hasSubcats: hasSubcats,
|
||||
subcats: subcats,
|
||||
subcatKeys: Object.keys(subcats)
|
||||
}
|
||||
}
|
||||
return processed
|
||||
}
|
||||
property var categoryKeys: Object.keys(categories)
|
||||
|
||||
function distributeCategories(cols) {
|
||||
const columns = []
|
||||
for (let i = 0; i < cols; i++) {
|
||||
columns.push([])
|
||||
}
|
||||
for (let i = 0; i < categoryKeys.length; i++) {
|
||||
columns[i % cols].push(categoryKeys[i])
|
||||
}
|
||||
return columns
|
||||
}
|
||||
|
||||
Row {
|
||||
id: rowLayout
|
||||
width: mainFlickable.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
property int numColumns: Math.max(1, Math.min(3, Math.floor(width / 350)))
|
||||
property var columnCategories: mainFlickable.distributeCategories(numColumns)
|
||||
|
||||
Repeater {
|
||||
model: rowLayout.numColumns
|
||||
|
||||
Column {
|
||||
id: masonryColumn
|
||||
width: (rowLayout.width - rowLayout.spacing * (rowLayout.numColumns - 1)) / rowLayout.numColumns
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Repeater {
|
||||
model: rowLayout.columnCategories[index] || []
|
||||
|
||||
Column {
|
||||
id: categoryColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
property string catName: modelData
|
||||
property var catData: mainFlickable.categories[catName]
|
||||
|
||||
StyledText {
|
||||
text: categoryColumn.catName
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.primary
|
||||
opacity: 0.3
|
||||
}
|
||||
|
||||
Item { width: 1; height: Theme.spacingXS }
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Repeater {
|
||||
model: categoryColumn.catData?.subcatKeys || []
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
property string subcatName: modelData
|
||||
property var subcatBinds: categoryColumn.catData?.subcats?.[subcatName] || []
|
||||
|
||||
StyledText {
|
||||
visible: parent.subcatName !== "_root"
|
||||
text: parent.subcatName
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.DemiBold
|
||||
color: Theme.primary
|
||||
opacity: 0.7
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: parent.parent.subcatBinds
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledRect {
|
||||
width: Math.min(140, parent.width * 0.42)
|
||||
height: 22
|
||||
radius: 4
|
||||
opacity: 0.9
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
anchors.margins: 2
|
||||
width: parent.width - 4
|
||||
color: Theme.secondary
|
||||
text: modelData.key || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
isMonospace: true
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width - 150
|
||||
text: modelData.desc || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
opacity: 0.9
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import qs.Widgets
|
||||
DankModal {
|
||||
id: notificationModal
|
||||
|
||||
layerNamespace: "dms:notification-modal"
|
||||
layerNamespace: "dms:notification-center-modal"
|
||||
|
||||
property bool notificationModalOpen: false
|
||||
property var notificationListRef: null
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
@@ -9,12 +10,17 @@ DankModal {
|
||||
|
||||
layerNamespace: "dms:power-menu"
|
||||
|
||||
property int selectedIndex: 0
|
||||
property int optionCount: SessionService.hibernateSupported ? 5 : 4
|
||||
property int selectedRow: 0
|
||||
property int selectedCol: 0
|
||||
property int selectedIndex: selectedRow * gridColumns + selectedCol
|
||||
property rect parentBounds: Qt.rect(0, 0, 0, 0)
|
||||
property var parentScreen: null
|
||||
property var visibleActions: []
|
||||
property int gridColumns: 3
|
||||
property int gridRows: 2
|
||||
|
||||
signal powerActionRequested(string action, string title, string message)
|
||||
signal lockRequested
|
||||
|
||||
function openCentered() {
|
||||
parentBounds = Qt.rect(0, 0, 0, 0)
|
||||
@@ -30,8 +36,114 @@ DankModal {
|
||||
open()
|
||||
}
|
||||
|
||||
function updateVisibleActions() {
|
||||
const allActions = SettingsData.powerMenuActions || ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
|
||||
visibleActions = allActions.filter(action => {
|
||||
if (action === "hibernate" && !SessionService.hibernateSupported)
|
||||
return false
|
||||
return true
|
||||
})
|
||||
|
||||
const count = visibleActions.length
|
||||
switch (count) {
|
||||
case 0:
|
||||
gridColumns = 1
|
||||
gridRows = 1
|
||||
break
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
gridColumns = 1
|
||||
gridRows = count
|
||||
break
|
||||
case 4:
|
||||
gridColumns = 2
|
||||
gridRows = 2
|
||||
break
|
||||
default:
|
||||
gridColumns = 3
|
||||
gridRows = Math.ceil(count / 3)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultActionIndex() {
|
||||
const defaultAction = SettingsData.powerMenuDefaultAction || "logout"
|
||||
const index = visibleActions.indexOf(defaultAction)
|
||||
return index >= 0 ? index : 0
|
||||
}
|
||||
|
||||
function getActionAtIndex(index) {
|
||||
if (index < 0 || index >= visibleActions.length)
|
||||
return ""
|
||||
return visibleActions[index]
|
||||
}
|
||||
|
||||
function getActionData(action) {
|
||||
switch (action) {
|
||||
case "reboot":
|
||||
return {
|
||||
"icon": "restart_alt",
|
||||
"label": I18n.tr("Reboot"),
|
||||
"key": "R"
|
||||
}
|
||||
case "logout":
|
||||
return {
|
||||
"icon": "logout",
|
||||
"label": I18n.tr("Log Out"),
|
||||
"key": "X"
|
||||
}
|
||||
case "poweroff":
|
||||
return {
|
||||
"icon": "power_settings_new",
|
||||
"label": I18n.tr("Power Off"),
|
||||
"key": "P"
|
||||
}
|
||||
case "lock":
|
||||
return {
|
||||
"icon": "lock",
|
||||
"label": I18n.tr("Lock"),
|
||||
"key": "L"
|
||||
}
|
||||
case "suspend":
|
||||
return {
|
||||
"icon": "bedtime",
|
||||
"label": I18n.tr("Suspend"),
|
||||
"key": "S"
|
||||
}
|
||||
case "hibernate":
|
||||
return {
|
||||
"icon": "ac_unit",
|
||||
"label": I18n.tr("Hibernate"),
|
||||
"key": "H"
|
||||
}
|
||||
case "restart":
|
||||
return {
|
||||
"icon": "refresh",
|
||||
"label": I18n.tr("Restart DMS"),
|
||||
"key": "D"
|
||||
}
|
||||
default:
|
||||
return {
|
||||
"icon": "help",
|
||||
"label": action,
|
||||
"key": "?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectOption(action) {
|
||||
close();
|
||||
if (action === "lock") {
|
||||
close()
|
||||
lockRequested()
|
||||
return
|
||||
}
|
||||
if (action === "restart") {
|
||||
close()
|
||||
Quickshell.execDetached(["dms", "restart"])
|
||||
return
|
||||
}
|
||||
close()
|
||||
const actions = {
|
||||
"logout": {
|
||||
"title": I18n.tr("Log Out"),
|
||||
@@ -56,13 +168,12 @@ DankModal {
|
||||
}
|
||||
const selected = actions[action]
|
||||
if (selected) {
|
||||
root.powerActionRequested(action, selected.title, selected.message);
|
||||
root.powerActionRequested(action, selected.title, selected.message)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
shouldBeVisible: false
|
||||
width: 320
|
||||
width: Math.min(550, gridColumns * 180 + Theme.spacingS * (gridColumns - 1) + Theme.spacingL * 2)
|
||||
height: contentLoader.item ? contentLoader.item.implicitHeight : 300
|
||||
enableShadow: true
|
||||
screen: parentScreen
|
||||
@@ -75,382 +186,190 @@ DankModal {
|
||||
}
|
||||
return Qt.point(0, 0)
|
||||
}
|
||||
onBackgroundClicked: () => {
|
||||
return close();
|
||||
}
|
||||
onBackgroundClicked: () => close()
|
||||
onOpened: () => {
|
||||
selectedIndex = 0;
|
||||
Qt.callLater(() => modalFocusScope.forceActiveFocus());
|
||||
}
|
||||
modalFocusScope.Keys.onPressed: (event) => {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Up:
|
||||
case Qt.Key_Backtab:
|
||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount;
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Down:
|
||||
case Qt.Key_Tab:
|
||||
selectedIndex = (selectedIndex + 1) % optionCount;
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
const actions = ["logout", "suspend"];
|
||||
if (SessionService.hibernateSupported) actions.push("hibernate");
|
||||
actions.push("reboot", "poweroff");
|
||||
if (selectedIndex < actions.length) {
|
||||
selectOption(actions[selectedIndex]);
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_N:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectedIndex = (selectedIndex + 1) % optionCount;
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_P:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount;
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_J:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectedIndex = (selectedIndex + 1) % optionCount;
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_K:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectedIndex = (selectedIndex - 1 + optionCount) % optionCount;
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
updateVisibleActions()
|
||||
const defaultIndex = getDefaultActionIndex()
|
||||
selectedRow = Math.floor(defaultIndex / gridColumns)
|
||||
selectedCol = defaultIndex % gridColumns
|
||||
Qt.callLater(() => modalFocusScope.forceActiveFocus())
|
||||
}
|
||||
Component.onCompleted: updateVisibleActions()
|
||||
modalFocusScope.Keys.onPressed: event => {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Left:
|
||||
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Right:
|
||||
selectedCol = (selectedCol + 1) % gridColumns
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Up:
|
||||
case Qt.Key_Backtab:
|
||||
selectedRow = (selectedRow - 1 + gridRows) % gridRows
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Down:
|
||||
case Qt.Key_Tab:
|
||||
selectedRow = (selectedRow + 1) % gridRows
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
selectOption(getActionAtIndex(selectedIndex))
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_N:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectedCol = (selectedCol + 1) % gridColumns
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_P:
|
||||
if (!(event.modifiers & Qt.ControlModifier)) {
|
||||
selectOption("poweroff")
|
||||
event.accepted = true
|
||||
} else {
|
||||
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_J:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectedRow = (selectedRow + 1) % gridRows
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_K:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
selectedRow = (selectedRow - 1 + gridRows) % gridRows
|
||||
event.accepted = true
|
||||
}
|
||||
break
|
||||
case Qt.Key_R:
|
||||
selectOption("reboot")
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_X:
|
||||
selectOption("logout")
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_L:
|
||||
selectOption("lock")
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_S:
|
||||
selectOption("suspend")
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_H:
|
||||
selectOption("hibernate")
|
||||
event.accepted = true
|
||||
break
|
||||
case Qt.Key_D:
|
||||
selectOption("restart")
|
||||
event.accepted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
implicitHeight: mainColumn.implicitHeight + Theme.spacingL * 2
|
||||
implicitHeight: buttonGrid.implicitHeight + Theme.spacingL * 2
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
Grid {
|
||||
id: buttonGrid
|
||||
anchors.centerIn: parent
|
||||
columns: root.gridColumns
|
||||
columnSpacing: Theme.spacingS
|
||||
rowSpacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
Repeater {
|
||||
model: root.visibleActions
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Power Options")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Rectangle {
|
||||
required property int index
|
||||
required property string modelData
|
||||
|
||||
Item {
|
||||
width: parent.width - 150
|
||||
height: 1
|
||||
}
|
||||
readonly property var actionData: root.getActionData(modelData)
|
||||
readonly property bool isSelected: root.selectedIndex === index
|
||||
readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff"
|
||||
|
||||
DankActionButton {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: () => {
|
||||
return close();
|
||||
width: (root.width - Theme.spacingL * 2 - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns
|
||||
height: 100
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (isSelected)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
if (mouseArea.containsMouse)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
}
|
||||
border.color: isSelected ? Theme.primary : "transparent"
|
||||
border.width: isSelected ? 2 : 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: parent.parent.actionData.icon
|
||||
size: Theme.iconSize + 8
|
||||
color: {
|
||||
if (parent.parent.showWarning && mouseArea.containsMouse) {
|
||||
return parent.parent.modelData === "poweroff" ? Theme.error : Theme.warning
|
||||
}
|
||||
return Theme.surfaceText
|
||||
}
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: parent.parent.actionData.label
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: {
|
||||
if (parent.parent.showWarning && mouseArea.containsMouse) {
|
||||
return parent.parent.modelData === "poweroff" ? Theme.error : Theme.warning
|
||||
}
|
||||
return Theme.surfaceText
|
||||
}
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 20
|
||||
height: 16
|
||||
radius: 4
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.1)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
StyledText {
|
||||
text: parent.parent.parent.actionData.key
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.selectedRow = Math.floor(index / root.gridColumns)
|
||||
root.selectedCol = index % root.gridColumns
|
||||
root.selectOption(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (selectedIndex === 0) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
} else if (logoutArea.containsMouse) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||
} else {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||
}
|
||||
}
|
||||
border.color: selectedIndex === 0 ? Theme.primary : "transparent"
|
||||
border.width: selectedIndex === 0 ? 1 : 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "logout"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Log Out")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: logoutArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
selectedIndex = 0;
|
||||
selectOption("logout");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (selectedIndex === 1) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
} else if (suspendArea.containsMouse) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||
} else {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||
}
|
||||
}
|
||||
border.color: selectedIndex === 1 ? Theme.primary : "transparent"
|
||||
border.width: selectedIndex === 1 ? 1 : 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "bedtime"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Suspend")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: suspendArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
selectedIndex = 1;
|
||||
selectOption("suspend");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (selectedIndex === 2) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
} else if (hibernateArea.containsMouse) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||
} else {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||
}
|
||||
}
|
||||
border.color: selectedIndex === 2 ? Theme.primary : "transparent"
|
||||
border.width: selectedIndex === 2 ? 1 : 0
|
||||
visible: SessionService.hibernateSupported
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "ac_unit"
|
||||
size: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Hibernate")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: hibernateArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
selectedIndex = 2;
|
||||
selectOption("hibernate");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
const rebootIndex = SessionService.hibernateSupported ? 3 : 2;
|
||||
if (selectedIndex === rebootIndex) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
} else if (rebootArea.containsMouse) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||
} else {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||
}
|
||||
}
|
||||
border.color: selectedIndex === (SessionService.hibernateSupported ? 3 : 2) ? Theme.primary : "transparent"
|
||||
border.width: selectedIndex === (SessionService.hibernateSupported ? 3 : 2) ? 1 : 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "restart_alt"
|
||||
size: Theme.iconSize
|
||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Reboot")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rebootArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
selectedIndex = SessionService.hibernateSupported ? 3 : 2;
|
||||
selectOption("reboot");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
const powerOffIndex = SessionService.hibernateSupported ? 4 : 3;
|
||||
if (selectedIndex === powerOffIndex) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
} else if (powerOffArea.containsMouse) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||
} else {
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||
}
|
||||
}
|
||||
border.color: selectedIndex === (SessionService.hibernateSupported ? 4 : 3) ? Theme.primary : "transparent"
|
||||
border.width: selectedIndex === (SessionService.hibernateSupported ? 4 : 3) ? 1 : 0
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "power_settings_new"
|
||||
size: Theme.iconSize
|
||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Power Off")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: powerOffArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
selectedIndex = SessionService.hibernateSupported ? 4 : 3;
|
||||
selectOption("poweroff");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
height: Theme.spacingS
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,13 +18,6 @@ Item {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Battery not detected - only AC power settings available")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: !BatteryService.batteryAvailable
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: lockScreenSection.implicitHeight + Theme.spacingL * 2
|
||||
@@ -83,10 +76,10 @@ Item {
|
||||
checked: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration
|
||||
enabled: SessionService.loginctlAvailable
|
||||
onToggled: checked => {
|
||||
if (SessionService.loginctlAvailable) {
|
||||
SettingsData.set("loginctlLockIntegration", checked)
|
||||
}
|
||||
}
|
||||
if (SessionService.loginctlAvailable) {
|
||||
SettingsData.set("loginctlLockIntegration", checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
@@ -158,11 +151,21 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Prevent idle for media")
|
||||
description: I18n.tr("Inhibit idle timeout when audio or video is playing")
|
||||
checked: SettingsData.preventIdleForMedia
|
||||
visible: IdleService.idleMonitorAvailable
|
||||
onToggled: checked => SettingsData.set("preventIdleForMedia", checked)
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
id: lockDropdown
|
||||
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
||||
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
||||
|
||||
addHorizontalPadding: true
|
||||
text: I18n.tr("Automatically lock after")
|
||||
options: timeoutOptions
|
||||
|
||||
@@ -182,16 +185,16 @@ Item {
|
||||
}
|
||||
|
||||
onValueChanged: value => {
|
||||
const index = timeoutOptions.indexOf(value)
|
||||
if (index >= 0) {
|
||||
const timeout = timeoutValues[index]
|
||||
if (powerCategory.currentIndex === 0) {
|
||||
SettingsData.set("acLockTimeout", timeout)
|
||||
} else {
|
||||
SettingsData.set("batteryLockTimeout", timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
const index = timeoutOptions.indexOf(value)
|
||||
if (index >= 0) {
|
||||
const timeout = timeoutValues[index]
|
||||
if (powerCategory.currentIndex === 0) {
|
||||
SettingsData.set("acLockTimeout", timeout)
|
||||
} else {
|
||||
SettingsData.set("batteryLockTimeout", timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
@@ -199,6 +202,7 @@ Item {
|
||||
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
||||
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
||||
|
||||
addHorizontalPadding: true
|
||||
text: I18n.tr("Turn off monitors after")
|
||||
options: timeoutOptions
|
||||
|
||||
@@ -218,16 +222,16 @@ Item {
|
||||
}
|
||||
|
||||
onValueChanged: value => {
|
||||
const index = timeoutOptions.indexOf(value)
|
||||
if (index >= 0) {
|
||||
const timeout = timeoutValues[index]
|
||||
if (powerCategory.currentIndex === 0) {
|
||||
SettingsData.set("acMonitorTimeout", timeout)
|
||||
} else {
|
||||
SettingsData.set("batteryMonitorTimeout", timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
const index = timeoutOptions.indexOf(value)
|
||||
if (index >= 0) {
|
||||
const timeout = timeoutValues[index]
|
||||
if (powerCategory.currentIndex === 0) {
|
||||
SettingsData.set("acMonitorTimeout", timeout)
|
||||
} else {
|
||||
SettingsData.set("batteryMonitorTimeout", timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
@@ -235,6 +239,7 @@ Item {
|
||||
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
||||
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
||||
|
||||
addHorizontalPadding: true
|
||||
text: I18n.tr("Suspend system after")
|
||||
options: timeoutOptions
|
||||
|
||||
@@ -254,16 +259,16 @@ Item {
|
||||
}
|
||||
|
||||
onValueChanged: value => {
|
||||
const index = timeoutOptions.indexOf(value)
|
||||
if (index >= 0) {
|
||||
const timeout = timeoutValues[index]
|
||||
if (powerCategory.currentIndex === 0) {
|
||||
SettingsData.set("acSuspendTimeout", timeout)
|
||||
} else {
|
||||
SettingsData.set("batterySuspendTimeout", timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
const index = timeoutOptions.indexOf(value)
|
||||
if (index >= 0) {
|
||||
const timeout = timeoutValues[index]
|
||||
if (powerCategory.currentIndex === 0) {
|
||||
SettingsData.set("acSuspendTimeout", timeout)
|
||||
} else {
|
||||
SettingsData.set("batterySuspendTimeout", timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
@@ -275,6 +280,7 @@ Item {
|
||||
text: I18n.tr("Suspend behavior")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
leftPadding: Theme.spacingM
|
||||
}
|
||||
|
||||
DankButtonGroup {
|
||||
@@ -298,14 +304,14 @@ Item {
|
||||
}
|
||||
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (selected) {
|
||||
if (powerCategory.currentIndex === 0) {
|
||||
SettingsData.set("acSuspendBehavior", index)
|
||||
} else {
|
||||
SettingsData.set("batterySuspendBehavior", index)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selected) {
|
||||
if (powerCategory.currentIndex === 0) {
|
||||
SettingsData.set("acSuspendBehavior", index)
|
||||
} else {
|
||||
SettingsData.set("batterySuspendBehavior", index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,6 +325,185 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: powerMenuCustomSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: powerMenuCustomSection
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "tune"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Power Menu Customization")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Customize which actions appear in the power menu")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
id: defaultActionDropdown
|
||||
width: parent.width
|
||||
addHorizontalPadding: true
|
||||
text: I18n.tr("Default selected action")
|
||||
options: ["Reboot", "Log Out", "Power Off", "Lock", "Suspend", "Restart DMS", "Hibernate"]
|
||||
property var actionValues: ["reboot", "logout", "poweroff", "lock", "suspend", "restart", "hibernate"]
|
||||
|
||||
Component.onCompleted: {
|
||||
const currentAction = SettingsData.powerMenuDefaultAction || "logout"
|
||||
const index = actionValues.indexOf(currentAction)
|
||||
currentValue = index >= 0 ? options[index] : "Log Out"
|
||||
}
|
||||
|
||||
onValueChanged: value => {
|
||||
const index = options.indexOf(value)
|
||||
if (index >= 0) {
|
||||
SettingsData.set("powerMenuDefaultAction", actionValues[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Reboot")
|
||||
checked: SettingsData.powerMenuActions.includes("reboot")
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions]
|
||||
if (checked && !actions.includes("reboot")) {
|
||||
actions.push("reboot")
|
||||
} else if (!checked) {
|
||||
actions = actions.filter(a => a !== "reboot")
|
||||
}
|
||||
SettingsData.set("powerMenuActions", actions)
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Log Out")
|
||||
checked: SettingsData.powerMenuActions.includes("logout")
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions]
|
||||
if (checked && !actions.includes("logout")) {
|
||||
actions.push("logout")
|
||||
} else if (!checked) {
|
||||
actions = actions.filter(a => a !== "logout")
|
||||
}
|
||||
SettingsData.set("powerMenuActions", actions)
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Power Off")
|
||||
checked: SettingsData.powerMenuActions.includes("poweroff")
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions]
|
||||
if (checked && !actions.includes("poweroff")) {
|
||||
actions.push("poweroff")
|
||||
} else if (!checked) {
|
||||
actions = actions.filter(a => a !== "poweroff")
|
||||
}
|
||||
SettingsData.set("powerMenuActions", actions)
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Lock")
|
||||
checked: SettingsData.powerMenuActions.includes("lock")
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions]
|
||||
if (checked && !actions.includes("lock")) {
|
||||
actions.push("lock")
|
||||
} else if (!checked) {
|
||||
actions = actions.filter(a => a !== "lock")
|
||||
}
|
||||
SettingsData.set("powerMenuActions", actions)
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Suspend")
|
||||
checked: SettingsData.powerMenuActions.includes("suspend")
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions]
|
||||
if (checked && !actions.includes("suspend")) {
|
||||
actions.push("suspend")
|
||||
} else if (!checked) {
|
||||
actions = actions.filter(a => a !== "suspend")
|
||||
}
|
||||
SettingsData.set("powerMenuActions", actions)
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Restart DMS")
|
||||
description: I18n.tr("Restart the DankMaterialShell")
|
||||
checked: SettingsData.powerMenuActions.includes("restart")
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions]
|
||||
if (checked && !actions.includes("restart")) {
|
||||
actions.push("restart")
|
||||
} else if (!checked) {
|
||||
actions = actions.filter(a => a !== "restart")
|
||||
}
|
||||
SettingsData.set("powerMenuActions", actions)
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Hibernate")
|
||||
description: I18n.tr("Only visible if hibernate is supported by your system")
|
||||
checked: SettingsData.powerMenuActions.includes("hibernate")
|
||||
visible: SessionService.hibernateSupported
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions]
|
||||
if (checked && !actions.includes("hibernate")) {
|
||||
actions.push("hibernate")
|
||||
} else if (!checked) {
|
||||
actions = actions.filter(a => a !== "hibernate")
|
||||
}
|
||||
SettingsData.set("powerMenuActions", actions)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: powerCommandConfirmSection.implicitHeight + Theme.spacingL * 2
|
||||
@@ -413,18 +598,18 @@ Item {
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "/usr/bin/myLock.sh"
|
||||
backgroundColor: Theme.surfaceVariant
|
||||
normalBorderColor: Theme.primarySelected
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionLock) {
|
||||
text = SettingsData.customPowerActionLock;
|
||||
text = SettingsData.customPowerActionLock
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.set("customPowerActionLock", text.trim());
|
||||
SettingsData.set("customPowerActionLock", text.trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -445,18 +630,18 @@ Item {
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "/usr/bin/myLogout.sh"
|
||||
backgroundColor: Theme.surfaceVariant
|
||||
normalBorderColor: Theme.primarySelected
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionLogout) {
|
||||
text = SettingsData.customPowerActionLogout;
|
||||
text = SettingsData.customPowerActionLogout
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.set("customPowerActionLogout", text.trim());
|
||||
SettingsData.set("customPowerActionLogout", text.trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -477,18 +662,18 @@ Item {
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "/usr/bin/mySuspend.sh"
|
||||
backgroundColor: Theme.surfaceVariant
|
||||
normalBorderColor: Theme.primarySelected
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionSuspend) {
|
||||
text = SettingsData.customPowerActionSuspend;
|
||||
text = SettingsData.customPowerActionSuspend
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.set("customPowerActionSuspend", text.trim());
|
||||
SettingsData.set("customPowerActionSuspend", text.trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -509,18 +694,18 @@ Item {
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "/usr/bin/myHibernate.sh"
|
||||
backgroundColor: Theme.surfaceVariant
|
||||
normalBorderColor: Theme.primarySelected
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionHibernate) {
|
||||
text = SettingsData.customPowerActionHibernate;
|
||||
text = SettingsData.customPowerActionHibernate
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.set("customPowerActionHibernate", text.trim());
|
||||
SettingsData.set("customPowerActionHibernate", text.trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -541,18 +726,18 @@ Item {
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "/usr/bin/myReboot.sh"
|
||||
backgroundColor: Theme.surfaceVariant
|
||||
normalBorderColor: Theme.primarySelected
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionReboot) {
|
||||
text = SettingsData.customPowerActionReboot;
|
||||
text = SettingsData.customPowerActionReboot
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.set("customPowerActionReboot", text.trim());
|
||||
SettingsData.set("customPowerActionReboot", text.trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -573,18 +758,18 @@ Item {
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "/usr/bin/myPowerOff.sh"
|
||||
backgroundColor: Theme.surfaceVariant
|
||||
normalBorderColor: Theme.primarySelected
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionPowerOff) {
|
||||
text = SettingsData.customPowerActionPowerOff;
|
||||
text = SettingsData.customPowerActionPowerOff
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.set("customPowerActionPowerOff", text.trim());
|
||||
SettingsData.set("customPowerActionPowerOff", text.trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modules.Settings
|
||||
|
||||
Item {
|
||||
FocusScope {
|
||||
id: root
|
||||
|
||||
property int currentIndex: 0
|
||||
property var parentModal: null
|
||||
|
||||
focus: true
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 0
|
||||
@@ -22,6 +24,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 0
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: Component {
|
||||
PersonalizationTab {
|
||||
@@ -30,6 +33,12 @@ Item {
|
||||
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
@@ -38,10 +47,17 @@ Item {
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 1
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: TimeWeatherTab {
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
@@ -50,11 +66,18 @@ Item {
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 2
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: DankBarTab {
|
||||
parentModal: root.parentModal
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
@@ -63,10 +86,17 @@ Item {
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 3
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: WidgetTweaksTab {
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
@@ -75,6 +105,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 4
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: Component {
|
||||
DockTab {
|
||||
@@ -82,6 +113,12 @@ Item {
|
||||
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
@@ -90,10 +127,17 @@ Item {
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 5
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: DisplaysTab {
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
@@ -102,10 +146,17 @@ Item {
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 6
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: LauncherTab {
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
@@ -114,10 +165,17 @@ Item {
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 7
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: ThemeColorsTab {
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
@@ -126,10 +184,17 @@ Item {
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 8
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: PowerSettings {
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
@@ -138,11 +203,18 @@ Item {
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 9
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: PluginsTab {
|
||||
parentModal: root.parentModal
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
@@ -151,10 +223,17 @@ Item {
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 10
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: AboutTab {
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -45,7 +45,23 @@ DankModal {
|
||||
}
|
||||
content: settingsContent
|
||||
onOpened: () => {
|
||||
Qt.callLater(() => modalFocusScope.forceActiveFocus())
|
||||
Qt.callLater(() => {
|
||||
modalFocusScope.forceActiveFocus()
|
||||
if (contentLoader.item) {
|
||||
contentLoader.item.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible && shouldBeVisible) {
|
||||
Qt.callLater(() => {
|
||||
modalFocusScope.forceActiveFocus()
|
||||
if (contentLoader.item) {
|
||||
contentLoader.item.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
modalFocusScope.Keys.onPressed: event => {
|
||||
const tabCount = 11
|
||||
@@ -113,6 +129,14 @@ DankModal {
|
||||
}
|
||||
onDialogClosed: () => {
|
||||
allowStacking = true;
|
||||
if (settingsModal.shouldBeVisible) {
|
||||
Qt.callLater(() => {
|
||||
settingsModal.modalFocusScope.forceActiveFocus()
|
||||
if (settingsModal.contentLoader.item) {
|
||||
settingsModal.contentLoader.item.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +156,14 @@ DankModal {
|
||||
}
|
||||
onDialogClosed: () => {
|
||||
allowStacking = true;
|
||||
if (settingsModal.shouldBeVisible) {
|
||||
Qt.callLater(() => {
|
||||
settingsModal.modalFocusScope.forceActiveFocus()
|
||||
if (settingsModal.contentLoader.item) {
|
||||
settingsModal.contentLoader.item.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +172,11 @@ DankModal {
|
||||
id: rootScope
|
||||
anchors.fill: parent
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
settingsModal.hide()
|
||||
event.accepted = true
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
|
||||
@@ -183,6 +183,7 @@ Item {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
leftPadding: Theme.spacingS
|
||||
topPadding: Theme.spacingS
|
||||
|
||||
DankTextField {
|
||||
id: searchField
|
||||
|
||||
@@ -210,8 +210,8 @@ DankPopout {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
height: 52
|
||||
cornerRadius: Theme.cornerRadius
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceVariant, Theme.popupTransparency)
|
||||
normalBorderColor: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
leftIconName: "search"
|
||||
leftIconSize: Theme.iconSize
|
||||
|
||||
@@ -39,6 +39,26 @@ PluginComponent {
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
Column {
|
||||
visible: !CupsService.cupsAvailable || CupsService.getPrintersNum() == 0
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "print_disabled"
|
||||
size: 36
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: !CupsService.cupsAvailable ? I18n.tr("Print Server not available") : I18n.tr("No printer found")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: detailColumn
|
||||
anchors.fill: parent
|
||||
|
||||
@@ -27,6 +27,7 @@ DankPopout {
|
||||
property bool editMode: false
|
||||
property int expandedWidgetIndex: -1
|
||||
property var expandedWidgetData: null
|
||||
property bool powerMenuOpen: powerMenuModalLoader?.item?.shouldBeVisible ?? false
|
||||
|
||||
signal lockRequested
|
||||
|
||||
@@ -115,6 +116,21 @@ DankPopout {
|
||||
antialiasing: true
|
||||
smooth: true
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, 0.6)
|
||||
radius: parent.radius
|
||||
visible: root.powerMenuOpen
|
||||
z: 5000
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
width: parent.width - Theme.spacingL * 2
|
||||
|
||||
@@ -123,9 +123,11 @@ Rectangle {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: Pipewire.nodes.values.filter(node => {
|
||||
return node.audio && !node.isSink && !node.isStream
|
||||
})
|
||||
model: ScriptModel {
|
||||
values: Pipewire.nodes.values.filter(node => {
|
||||
return node.audio && !node.isSink && !node.isStream
|
||||
})
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
|
||||
@@ -128,9 +128,11 @@ Rectangle {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: Pipewire.nodes.values.filter(node => {
|
||||
return node.audio && node.isSink && !node.isStream
|
||||
})
|
||||
model: ScriptModel {
|
||||
values: Pipewire.nodes.values.filter(node => {
|
||||
return node.audio && node.isSink && !node.isStream
|
||||
})
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
|
||||
@@ -210,19 +210,20 @@ Rectangle {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: sortedNetworks
|
||||
|
||||
property var sortedNetworks: {
|
||||
const currentUuid = NetworkService.ethernetConnectionUuid
|
||||
const networks = NetworkService.wiredConnections
|
||||
let sorted = [...networks]
|
||||
sorted.sort((a, b) => {
|
||||
if (a.isActive && !b.isActive) return -1
|
||||
if (!a.isActive && b.isActive) return 1
|
||||
return a.id.localeCompare(b.id)
|
||||
})
|
||||
return sorted
|
||||
model: ScriptModel {
|
||||
values: {
|
||||
const currentUuid = NetworkService.ethernetConnectionUuid
|
||||
const networks = NetworkService.wiredConnections
|
||||
let sorted = [...networks]
|
||||
sorted.sort((a, b) => {
|
||||
if (a.isActive && !b.isActive) return -1
|
||||
if (!a.isActive && b.isActive) return 1
|
||||
return a.id.localeCompare(b.id)
|
||||
})
|
||||
return sorted
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
@@ -406,22 +407,23 @@ Rectangle {
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: wifiContent.menuOpen ? wifiContent.frozenNetworks : sortedNetworks
|
||||
|
||||
property var sortedNetworks: {
|
||||
const ssid = NetworkService.currentWifiSSID
|
||||
const networks = NetworkService.wifiNetworks
|
||||
let sorted = [...networks]
|
||||
sorted.sort((a, b) => {
|
||||
if (a.ssid === ssid) return -1
|
||||
if (b.ssid === ssid) return 1
|
||||
return b.signal - a.signal
|
||||
})
|
||||
if (!wifiContent.menuOpen) {
|
||||
wifiContent.frozenNetworks = sorted
|
||||
model: ScriptModel {
|
||||
values: {
|
||||
const ssid = NetworkService.currentWifiSSID
|
||||
const networks = NetworkService.wifiNetworks
|
||||
let sorted = [...networks]
|
||||
sorted.sort((a, b) => {
|
||||
if (a.ssid === ssid) return -1
|
||||
if (b.ssid === ssid) return 1
|
||||
return b.signal - a.signal
|
||||
})
|
||||
if (!wifiContent.menuOpen) {
|
||||
wifiContent.frozenNetworks = sorted
|
||||
}
|
||||
return wifiContent.menuOpen ? wifiContent.frozenNetworks : sorted
|
||||
}
|
||||
return sorted
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
@@ -72,6 +72,12 @@ Item {
|
||||
function onSurfaceContainerChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onDankBarGothCornerRadiusOverrideChanged() { root.requestRepaint() }
|
||||
function onDankBarGothCornerRadiusValueChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
const W = barWindow.isVertical ? correctHeight : correctWidth
|
||||
@@ -171,6 +177,12 @@ Item {
|
||||
function onSurfaceChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onDankBarGothCornerRadiusOverrideChanged() { root.requestRepaint() }
|
||||
function onDankBarGothCornerRadiusValueChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d")
|
||||
const W = barWindow.isVertical ? correctHeight : correctWidth
|
||||
@@ -278,6 +290,8 @@ Item {
|
||||
function onDankBarSpacingChanged() { root.requestRepaint() }
|
||||
function onDankBarSquareCornersChanged() { root.requestRepaint() }
|
||||
function onDankBarTransparencyChanged() { root.requestRepaint() }
|
||||
function onDankBarGothCornerRadiusOverrideChanged() { root.requestRepaint() }
|
||||
function onDankBarGothCornerRadiusValueChanged() { root.requestRepaint() }
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
|
||||
@@ -24,8 +24,7 @@ Item {
|
||||
property real totalSize: 0
|
||||
|
||||
function updateLayout() {
|
||||
const containerSize = isVertical ? height : width
|
||||
if (containerSize <= 0 || !visible) {
|
||||
if ((isVertical ? height : width) <= 0 || !visible) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -34,32 +33,58 @@ Item {
|
||||
totalSize = 0
|
||||
|
||||
let configuredWidgets = 0
|
||||
let configuredMiddleWidget = null
|
||||
let configuredLeftWidget = null
|
||||
let configuredRightWidget = null
|
||||
|
||||
for (var i = 0; i < centerRepeater.count; i++) {
|
||||
const item = centerRepeater.itemAt(i)
|
||||
if (item && getWidgetVisible(item.widgetId)) {
|
||||
configuredWidgets++
|
||||
}
|
||||
}
|
||||
|
||||
const isOddConfigured = configuredWidgets % 2 === 1
|
||||
const configuredMiddlePos = Math.floor(configuredWidgets / 2)
|
||||
const configuredLeftPos = isOddConfigured ? -1 : ((configuredWidgets / 2) - 1)
|
||||
const configuredRightPos = isOddConfigured ? -1 : (configuredWidgets / 2)
|
||||
let currentConfigIndex = 0
|
||||
|
||||
for (var i = 0; i < centerRepeater.count; i++) {
|
||||
const item = centerRepeater.itemAt(i)
|
||||
if (item && getWidgetVisible(item.widgetId)) {
|
||||
if (isOddConfigured && currentConfigIndex === configuredMiddlePos && item.active && item.item) {
|
||||
configuredMiddleWidget = item.item
|
||||
}
|
||||
if (!isOddConfigured && currentConfigIndex === configuredLeftPos && item.active && item.item) {
|
||||
configuredLeftWidget = item.item
|
||||
}
|
||||
if (!isOddConfigured && currentConfigIndex === configuredRightPos && item.active && item.item) {
|
||||
configuredRightWidget = item.item
|
||||
}
|
||||
if (item.active && item.item) {
|
||||
centerWidgets.push(item.item)
|
||||
totalWidgets++
|
||||
totalSize += isVertical ? item.item.height : item.item.width
|
||||
}
|
||||
currentConfigIndex++
|
||||
}
|
||||
}
|
||||
|
||||
if (totalWidgets === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (totalWidgets > 1) {
|
||||
totalSize += spacing * (totalWidgets - 1)
|
||||
}
|
||||
|
||||
positionWidgets(configuredWidgets)
|
||||
positionWidgets(configuredWidgets, configuredMiddleWidget, configuredLeftWidget, configuredRightWidget)
|
||||
}
|
||||
|
||||
function positionWidgets(configuredWidgets) {
|
||||
if (totalWidgets === 0 || (isVertical ? height : width) <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
function positionWidgets(configuredWidgets, configuredMiddleWidget, configuredLeftWidget, configuredRightWidget) {
|
||||
const parentCenter = (isVertical ? height : width) / 2
|
||||
const isOdd = configuredWidgets % 2 === 1
|
||||
const isOddConfigured = configuredWidgets % 2 === 1
|
||||
|
||||
centerWidgets.forEach(widget => {
|
||||
if (isVertical) {
|
||||
@@ -69,193 +94,169 @@ Item {
|
||||
}
|
||||
})
|
||||
|
||||
if (isOdd) {
|
||||
const middleIndex = Math.floor(configuredWidgets / 2)
|
||||
let currentActiveIndex = 0
|
||||
let middleWidget = null
|
||||
if (isOddConfigured && configuredMiddleWidget) {
|
||||
const middleWidget = configuredMiddleWidget
|
||||
const middleIndex = centerWidgets.indexOf(middleWidget)
|
||||
const middleSize = isVertical ? middleWidget.height : middleWidget.width
|
||||
|
||||
for (var i = 0; i < centerRepeater.count; i++) {
|
||||
const item = centerRepeater.itemAt(i)
|
||||
if (item && getWidgetVisible(item.widgetId)) {
|
||||
if (currentActiveIndex === middleIndex && item.active && item.item) {
|
||||
middleWidget = item.item
|
||||
break
|
||||
}
|
||||
currentActiveIndex++
|
||||
if (isVertical) {
|
||||
middleWidget.y = parentCenter - (middleSize / 2)
|
||||
} else {
|
||||
middleWidget.x = parentCenter - (middleSize / 2)
|
||||
}
|
||||
|
||||
let currentPos = isVertical ? middleWidget.y : middleWidget.x
|
||||
for (var i = middleIndex - 1; i >= 0; i--) {
|
||||
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width
|
||||
currentPos -= (spacing + size)
|
||||
if (isVertical) {
|
||||
centerWidgets[i].y = currentPos
|
||||
} else {
|
||||
centerWidgets[i].x = currentPos
|
||||
}
|
||||
}
|
||||
|
||||
if (middleWidget) {
|
||||
const middleSize = isVertical ? middleWidget.height : middleWidget.width
|
||||
currentPos = (isVertical ? middleWidget.y : middleWidget.x) + middleSize
|
||||
for (var i = middleIndex + 1; i < totalWidgets; i++) {
|
||||
currentPos += spacing
|
||||
if (isVertical) {
|
||||
middleWidget.y = parentCenter - (middleSize / 2)
|
||||
centerWidgets[i].y = currentPos
|
||||
} else {
|
||||
middleWidget.x = parentCenter - (middleSize / 2)
|
||||
}
|
||||
|
||||
let leftWidgets = []
|
||||
let rightWidgets = []
|
||||
let foundMiddle = false
|
||||
|
||||
for (var i = 0; i < centerWidgets.length; i++) {
|
||||
if (centerWidgets[i] === middleWidget) {
|
||||
foundMiddle = true
|
||||
continue
|
||||
}
|
||||
if (!foundMiddle) {
|
||||
leftWidgets.push(centerWidgets[i])
|
||||
} else {
|
||||
rightWidgets.push(centerWidgets[i])
|
||||
}
|
||||
}
|
||||
|
||||
let currentPos = isVertical ? middleWidget.y : middleWidget.x
|
||||
for (var i = leftWidgets.length - 1; i >= 0; i--) {
|
||||
const size = isVertical ? leftWidgets[i].height : leftWidgets[i].width
|
||||
currentPos -= (spacing + size)
|
||||
if (isVertical) {
|
||||
leftWidgets[i].y = currentPos
|
||||
} else {
|
||||
leftWidgets[i].x = currentPos
|
||||
}
|
||||
}
|
||||
|
||||
currentPos = (isVertical ? middleWidget.y : middleWidget.x) + middleSize
|
||||
for (var i = 0; i < rightWidgets.length; i++) {
|
||||
currentPos += spacing
|
||||
if (isVertical) {
|
||||
rightWidgets[i].y = currentPos
|
||||
} else {
|
||||
rightWidgets[i].x = currentPos
|
||||
}
|
||||
currentPos += isVertical ? rightWidgets[i].height : rightWidgets[i].width
|
||||
centerWidgets[i].x = currentPos
|
||||
}
|
||||
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width
|
||||
}
|
||||
} else {
|
||||
let configuredLeftIndex = (configuredWidgets / 2) - 1
|
||||
let configuredRightIndex = configuredWidgets / 2
|
||||
const halfSpacing = spacing / 2
|
||||
if (totalWidgets === 1) {
|
||||
const widget = centerWidgets[0]
|
||||
const size = isVertical ? widget.height : widget.width
|
||||
if (isVertical) {
|
||||
widget.y = parentCenter - (size / 2)
|
||||
} else {
|
||||
widget.x = parentCenter - (size / 2)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let leftWidget = null
|
||||
let rightWidget = null
|
||||
let leftWidgets = []
|
||||
let rightWidgets = []
|
||||
if (!configuredLeftWidget || !configuredRightWidget) {
|
||||
if (totalWidgets % 2 === 1) {
|
||||
const middleIndex = Math.floor(totalWidgets / 2)
|
||||
const middleWidget = centerWidgets[middleIndex]
|
||||
|
||||
let currentConfigIndex = 0
|
||||
for (var i = 0; i < centerRepeater.count; i++) {
|
||||
const item = centerRepeater.itemAt(i)
|
||||
if (item && getWidgetVisible(item.widgetId)) {
|
||||
if (item.active && item.item) {
|
||||
if (currentConfigIndex < configuredLeftIndex) {
|
||||
leftWidgets.push(item.item)
|
||||
} else if (currentConfigIndex === configuredLeftIndex) {
|
||||
leftWidget = item.item
|
||||
} else if (currentConfigIndex === configuredRightIndex) {
|
||||
rightWidget = item.item
|
||||
if (!middleWidget) {
|
||||
return
|
||||
}
|
||||
|
||||
const middleSize = isVertical ? middleWidget.height : middleWidget.width
|
||||
|
||||
if (isVertical) {
|
||||
middleWidget.y = parentCenter - (middleSize / 2)
|
||||
} else {
|
||||
middleWidget.x = parentCenter - (middleSize / 2)
|
||||
}
|
||||
|
||||
let currentPos = isVertical ? middleWidget.y : middleWidget.x
|
||||
for (var i = middleIndex - 1; i >= 0; i--) {
|
||||
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width
|
||||
currentPos -= (spacing + size)
|
||||
if (isVertical) {
|
||||
centerWidgets[i].y = currentPos
|
||||
} else {
|
||||
rightWidgets.push(item.item)
|
||||
centerWidgets[i].x = currentPos
|
||||
}
|
||||
}
|
||||
currentConfigIndex++
|
||||
|
||||
currentPos = (isVertical ? middleWidget.y : middleWidget.x) + middleSize
|
||||
for (var i = middleIndex + 1; i < totalWidgets; i++) {
|
||||
currentPos += spacing
|
||||
if (isVertical) {
|
||||
centerWidgets[i].y = currentPos
|
||||
} else {
|
||||
centerWidgets[i].x = currentPos
|
||||
}
|
||||
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width
|
||||
}
|
||||
} else {
|
||||
const leftIndex = (totalWidgets / 2) - 1
|
||||
const rightIndex = totalWidgets / 2
|
||||
const fallbackLeft = centerWidgets[leftIndex]
|
||||
const fallbackRight = centerWidgets[rightIndex]
|
||||
|
||||
if (!fallbackLeft || !fallbackRight) {
|
||||
return
|
||||
}
|
||||
|
||||
const halfSpacing = spacing / 2
|
||||
const leftSize = isVertical ? fallbackLeft.height : fallbackLeft.width
|
||||
|
||||
if (isVertical) {
|
||||
fallbackLeft.y = parentCenter - halfSpacing - leftSize
|
||||
fallbackRight.y = parentCenter + halfSpacing
|
||||
} else {
|
||||
fallbackLeft.x = parentCenter - halfSpacing - leftSize
|
||||
fallbackRight.x = parentCenter + halfSpacing
|
||||
}
|
||||
|
||||
let currentPos = isVertical ? fallbackLeft.y : fallbackLeft.x
|
||||
for (var i = leftIndex - 1; i >= 0; i--) {
|
||||
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width
|
||||
currentPos -= (spacing + size)
|
||||
if (isVertical) {
|
||||
centerWidgets[i].y = currentPos
|
||||
} else {
|
||||
centerWidgets[i].x = currentPos
|
||||
}
|
||||
}
|
||||
|
||||
currentPos = (isVertical ? fallbackRight.y + fallbackRight.height : fallbackRight.x + fallbackRight.width)
|
||||
for (var i = rightIndex + 1; i < totalWidgets; i++) {
|
||||
currentPos += spacing
|
||||
if (isVertical) {
|
||||
centerWidgets[i].y = currentPos
|
||||
} else {
|
||||
centerWidgets[i].x = currentPos
|
||||
}
|
||||
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const leftWidget = configuredLeftWidget
|
||||
const rightWidget = configuredRightWidget
|
||||
const leftIndex = centerWidgets.indexOf(leftWidget)
|
||||
const rightIndex = centerWidgets.indexOf(rightWidget)
|
||||
const halfSpacing = spacing / 2
|
||||
const leftSize = isVertical ? leftWidget.height : leftWidget.width
|
||||
|
||||
if (isVertical) {
|
||||
leftWidget.y = parentCenter - halfSpacing - leftSize
|
||||
rightWidget.y = parentCenter + halfSpacing
|
||||
} else {
|
||||
leftWidget.x = parentCenter - halfSpacing - leftSize
|
||||
rightWidget.x = parentCenter + halfSpacing
|
||||
}
|
||||
|
||||
let currentPos = isVertical ? leftWidget.y : leftWidget.x
|
||||
for (var i = leftIndex - 1; i >= 0; i--) {
|
||||
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width
|
||||
currentPos -= (spacing + size)
|
||||
if (isVertical) {
|
||||
centerWidgets[i].y = currentPos
|
||||
} else {
|
||||
centerWidgets[i].x = currentPos
|
||||
}
|
||||
}
|
||||
|
||||
if (leftWidget && rightWidget) {
|
||||
const leftSize = isVertical ? leftWidget.height : leftWidget.width
|
||||
currentPos = (isVertical ? rightWidget.y + rightWidget.height : rightWidget.x + rightWidget.width)
|
||||
for (var i = rightIndex + 1; i < totalWidgets; i++) {
|
||||
currentPos += spacing
|
||||
if (isVertical) {
|
||||
leftWidget.y = parentCenter - halfSpacing - leftSize
|
||||
rightWidget.y = parentCenter + halfSpacing
|
||||
centerWidgets[i].y = currentPos
|
||||
} else {
|
||||
leftWidget.x = parentCenter - halfSpacing - leftSize
|
||||
rightWidget.x = parentCenter + halfSpacing
|
||||
}
|
||||
|
||||
let currentPos = isVertical ? leftWidget.y : leftWidget.x
|
||||
for (var i = leftWidgets.length - 1; i >= 0; i--) {
|
||||
const size = isVertical ? leftWidgets[i].height : leftWidgets[i].width
|
||||
currentPos -= (spacing + size)
|
||||
if (isVertical) {
|
||||
leftWidgets[i].y = currentPos
|
||||
} else {
|
||||
leftWidgets[i].x = currentPos
|
||||
}
|
||||
}
|
||||
|
||||
currentPos = (isVertical ? rightWidget.y + rightWidget.height : rightWidget.x + rightWidget.width)
|
||||
for (var i = 0; i < rightWidgets.length; i++) {
|
||||
currentPos += spacing
|
||||
if (isVertical) {
|
||||
rightWidgets[i].y = currentPos
|
||||
} else {
|
||||
rightWidgets[i].x = currentPos
|
||||
}
|
||||
currentPos += isVertical ? rightWidgets[i].height : rightWidgets[i].width
|
||||
}
|
||||
} else if (leftWidget && !rightWidget) {
|
||||
const leftSize = isVertical ? leftWidget.height : leftWidget.width
|
||||
if (isVertical) {
|
||||
leftWidget.y = parentCenter - halfSpacing - leftSize
|
||||
} else {
|
||||
leftWidget.x = parentCenter - halfSpacing - leftSize
|
||||
}
|
||||
|
||||
let currentPos = isVertical ? leftWidget.y : leftWidget.x
|
||||
for (var i = leftWidgets.length - 1; i >= 0; i--) {
|
||||
const size = isVertical ? leftWidgets[i].height : leftWidgets[i].width
|
||||
currentPos -= (spacing + size)
|
||||
if (isVertical) {
|
||||
leftWidgets[i].y = currentPos
|
||||
} else {
|
||||
leftWidgets[i].x = currentPos
|
||||
}
|
||||
}
|
||||
|
||||
currentPos = (isVertical ? leftWidget.y + leftWidget.height : leftWidget.x + leftWidget.width) + spacing
|
||||
for (var i = 0; i < rightWidgets.length; i++) {
|
||||
currentPos += spacing
|
||||
if (isVertical) {
|
||||
rightWidgets[i].y = currentPos
|
||||
} else {
|
||||
rightWidgets[i].x = currentPos
|
||||
}
|
||||
currentPos += isVertical ? rightWidgets[i].height : rightWidgets[i].width
|
||||
}
|
||||
} else if (!leftWidget && rightWidget) {
|
||||
if (isVertical) {
|
||||
rightWidget.y = parentCenter + halfSpacing
|
||||
} else {
|
||||
rightWidget.x = parentCenter + halfSpacing
|
||||
}
|
||||
|
||||
let currentPos = (isVertical ? rightWidget.y : rightWidget.x) - spacing
|
||||
for (var i = leftWidgets.length - 1; i >= 0; i--) {
|
||||
const size = isVertical ? leftWidgets[i].height : leftWidgets[i].width
|
||||
currentPos -= size
|
||||
if (isVertical) {
|
||||
leftWidgets[i].y = currentPos
|
||||
} else {
|
||||
leftWidgets[i].x = currentPos
|
||||
}
|
||||
currentPos -= spacing
|
||||
}
|
||||
|
||||
currentPos = (isVertical ? rightWidget.y + rightWidget.height : rightWidget.x + rightWidget.width)
|
||||
for (var i = 0; i < rightWidgets.length; i++) {
|
||||
currentPos += spacing
|
||||
if (isVertical) {
|
||||
rightWidgets[i].y = currentPos
|
||||
} else {
|
||||
rightWidgets[i].x = currentPos
|
||||
}
|
||||
currentPos += isVertical ? rightWidgets[i].height : rightWidgets[i].width
|
||||
}
|
||||
} else if (totalWidgets === 1 && centerWidgets[0]) {
|
||||
const size = isVertical ? centerWidgets[0].height : centerWidgets[0].width
|
||||
if (isVertical) {
|
||||
centerWidgets[0].y = parentCenter - (size / 2)
|
||||
} else {
|
||||
centerWidgets[0].x = parentCenter - (size / 2)
|
||||
centerWidgets[i].x = currentPos
|
||||
}
|
||||
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ Item {
|
||||
|
||||
property alias barVariants: barVariants
|
||||
property var hyprlandOverviewLoader: null
|
||||
property bool systemTrayMenuOpen: false
|
||||
|
||||
function triggerControlCenterOnFocusedScreen() {
|
||||
let focusedScreenName = ""
|
||||
@@ -175,7 +176,7 @@ Item {
|
||||
readonly property bool isVertical: axis.isVertical
|
||||
|
||||
property bool gothCornersEnabled: SettingsData.dankBarGothCornersEnabled
|
||||
property real wingtipsRadius: Theme.cornerRadius
|
||||
property real wingtipsRadius: SettingsData.dankBarGothCornerRadiusOverride ? SettingsData.dankBarGothCornerRadiusValue : Theme.cornerRadius
|
||||
readonly property real _wingR: Math.max(0, wingtipsRadius)
|
||||
readonly property color _surfaceContainer: Theme.surfaceContainer
|
||||
readonly property real _backgroundAlpha: topBarCore?.backgroundTransparency ?? SettingsData.dankBarTransparency
|
||||
@@ -444,7 +445,7 @@ Item {
|
||||
return item.loader.item[item.prop]
|
||||
}
|
||||
return false
|
||||
})
|
||||
}) || root.systemTrayMenuOpen
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -470,7 +471,10 @@ Item {
|
||||
}
|
||||
|
||||
onHasActivePopoutChanged: {
|
||||
if (!hasActivePopout && autoHide && !topBarMouseArea.containsMouse) {
|
||||
if (hasActivePopout) {
|
||||
revealSticky = true
|
||||
revealHold.stop()
|
||||
} else if (autoHide && !topBarMouseArea.containsMouse) {
|
||||
revealSticky = true
|
||||
revealHold.restart()
|
||||
}
|
||||
|
||||
@@ -381,7 +381,9 @@ DankPopout {
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: BatteryService.batteries
|
||||
model: ScriptModel {
|
||||
values: BatteryService.batteries
|
||||
}
|
||||
|
||||
delegate: StyledRect {
|
||||
required property var modelData
|
||||
|
||||
@@ -56,7 +56,7 @@ BasePill {
|
||||
}
|
||||
|
||||
IconImage {
|
||||
visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway)
|
||||
visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isLabwc)
|
||||
anchors.centerIn: parent
|
||||
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
|
||||
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
|
||||
@@ -71,6 +71,8 @@ BasePill {
|
||||
return "file://" + Theme.shellDir + "/assets/mango.png"
|
||||
} else if (CompositorService.isSway) {
|
||||
return "file://" + Theme.shellDir + "/assets/sway.svg"
|
||||
} else if (CompositorService.isLabwc) {
|
||||
return "file://" + Theme.shellDir + "/assets/labwc.png"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -21,21 +21,27 @@ Item {
|
||||
property real barThickness: 48
|
||||
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 2 : Theme.spacingS
|
||||
property Item windowRoot: (Window.window ? Window.window.contentItem : null)
|
||||
property int _workspaceUpdateTrigger: 0
|
||||
property int _desktopEntriesUpdateTrigger: 0
|
||||
property int _toplevelsUpdateTrigger: 0
|
||||
|
||||
readonly property var sortedToplevels: {
|
||||
_workspaceUpdateTrigger
|
||||
_toplevelsUpdateTrigger
|
||||
const toplevels = CompositorService.sortedToplevels
|
||||
if (!toplevels || toplevels.length === 0)
|
||||
return []
|
||||
if (!toplevels || toplevels.length === 0) return []
|
||||
|
||||
if (SettingsData.runningAppsCurrentWorkspace) {
|
||||
const filtered = CompositorService.filterCurrentWorkspace(toplevels, parentScreen?.name)
|
||||
return filtered || []
|
||||
return CompositorService.filterCurrentWorkspace(toplevels, parentScreen?.name) || []
|
||||
}
|
||||
return toplevels
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onToplevelsChanged() {
|
||||
_toplevelsUpdateTrigger++
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DesktopEntries
|
||||
function onApplicationsChanged() {
|
||||
@@ -89,36 +95,6 @@ Item {
|
||||
height: isVertical ? calculatedSize : barThickness
|
||||
visible: windowCount > 0
|
||||
|
||||
Connections {
|
||||
target: NiriService
|
||||
function onAllWorkspacesChanged() {
|
||||
_workspaceUpdateTrigger++
|
||||
}
|
||||
function onWindowsChanged() {
|
||||
_workspaceUpdateTrigger++
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Hyprland
|
||||
function onFocusedWorkspaceChanged() {
|
||||
_workspaceUpdateTrigger++
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Hyprland.workspaces
|
||||
function onValuesChanged() {
|
||||
_workspaceUpdateTrigger++
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Hyprland.toplevels
|
||||
function onValuesChanged() {
|
||||
_workspaceUpdateTrigger++
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: visualBackground
|
||||
@@ -241,7 +217,10 @@ Item {
|
||||
|
||||
Repeater {
|
||||
id: windowRepeater
|
||||
model: SettingsData.runningAppsGroupByApp ? groupedWindows : sortedToplevels
|
||||
model: ScriptModel {
|
||||
values: SettingsData.runningAppsGroupByApp ? groupedWindows : sortedToplevels
|
||||
objectProp: SettingsData.runningAppsGroupByApp ? "appId" : "address"
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: delegateItem
|
||||
@@ -442,7 +421,11 @@ Item {
|
||||
tooltipLoader.item.show(delegateItem.tooltipText, screenX + tooltipX, relativeY, root.parentScreen, isLeft, !isLeft)
|
||||
} else {
|
||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height)
|
||||
const tooltipY = Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS
|
||||
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height
|
||||
const isBottom = root.axis?.edge === "bottom"
|
||||
const tooltipY = isBottom
|
||||
? (screenHeight - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS - 35)
|
||||
: (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS)
|
||||
tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false)
|
||||
}
|
||||
}
|
||||
@@ -470,7 +453,10 @@ Item {
|
||||
|
||||
Repeater {
|
||||
id: windowRepeater
|
||||
model: SettingsData.runningAppsGroupByApp ? groupedWindows : sortedToplevels
|
||||
model: ScriptModel {
|
||||
values: SettingsData.runningAppsGroupByApp ? groupedWindows : sortedToplevels
|
||||
objectProp: SettingsData.runningAppsGroupByApp ? "appId" : "address"
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: delegateItem
|
||||
@@ -668,7 +654,11 @@ Item {
|
||||
tooltipLoader.item.show(delegateItem.tooltipText, screenX + tooltipX, relativeY, root.parentScreen, isLeft, !isLeft)
|
||||
} else {
|
||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height)
|
||||
const tooltipY = Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS
|
||||
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height
|
||||
const isBottom = root.axis?.edge === "bottom"
|
||||
const tooltipY = isBottom
|
||||
? (screenHeight - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS - 35)
|
||||
: (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS)
|
||||
tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ Item {
|
||||
const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || ""
|
||||
return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : []
|
||||
}
|
||||
readonly property var visibleTrayItems: {
|
||||
readonly property var allTrayItems: {
|
||||
if (!hiddenTrayIds.length) {
|
||||
return SystemTray.items.values
|
||||
}
|
||||
@@ -31,13 +31,20 @@ Item {
|
||||
return !hiddenTrayIds.includes(itemId.toLowerCase())
|
||||
})
|
||||
}
|
||||
readonly property int calculatedSize: visibleTrayItems.length > 0 ? visibleTrayItems.length * 24 + horizontalPadding * 2 : 0
|
||||
readonly property var mainBarItems: allTrayItems.filter(item => !SessionData.isHiddenTrayId(item?.id || ""))
|
||||
readonly property int calculatedSize: {
|
||||
if (allTrayItems.length === 0) return 0
|
||||
const itemCount = mainBarItems.length + 1
|
||||
return itemCount * 24 + horizontalPadding * 2
|
||||
}
|
||||
readonly property real visualWidth: isVertical ? widgetThickness : calculatedSize
|
||||
readonly property real visualHeight: isVertical ? calculatedSize : widgetThickness
|
||||
|
||||
width: isVertical ? barThickness : visualWidth
|
||||
height: isVertical ? visualHeight : barThickness
|
||||
visible: visibleTrayItems.length > 0
|
||||
visible: allTrayItems.length > 0
|
||||
|
||||
property bool menuOpen: false
|
||||
|
||||
Rectangle {
|
||||
id: visualBackground
|
||||
@@ -46,7 +53,7 @@ Item {
|
||||
anchors.centerIn: parent
|
||||
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
|
||||
color: {
|
||||
if (visibleTrayItems.length === 0) {
|
||||
if (allTrayItems.length === 0) {
|
||||
return "transparent";
|
||||
}
|
||||
|
||||
@@ -71,7 +78,7 @@ Item {
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: root.visibleTrayItems
|
||||
model: root.mainBarItems
|
||||
|
||||
delegate: Item {
|
||||
id: delegateRoot
|
||||
@@ -165,6 +172,35 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 24
|
||||
height: root.barThickness
|
||||
|
||||
Rectangle {
|
||||
id: caretButton
|
||||
width: 24
|
||||
height: 24
|
||||
anchors.centerIn: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: caretArea.containsMouse ? Theme.primaryHover : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root.menuOpen ? "expand_less" : "expand_more"
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: caretArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.menuOpen = !root.menuOpen
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +210,7 @@ Item {
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: root.visibleTrayItems
|
||||
model: root.mainBarItems
|
||||
|
||||
delegate: Item {
|
||||
id: delegateRoot
|
||||
@@ -268,6 +304,320 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: root.barThickness
|
||||
height: 24
|
||||
|
||||
Rectangle {
|
||||
id: caretButtonVert
|
||||
width: 24
|
||||
height: 24
|
||||
anchors.centerIn: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: caretAreaVert.containsMouse ? Theme.primaryHover : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: {
|
||||
const edge = root.axis?.edge
|
||||
if (edge === "left") {
|
||||
return root.menuOpen ? "chevron_left" : "chevron_right"
|
||||
} else {
|
||||
return root.menuOpen ? "chevron_right" : "chevron_left"
|
||||
}
|
||||
}
|
||||
size: Theme.barIconSize(root.barThickness)
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: caretAreaVert
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.menuOpen = !root.menuOpen
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: overflowMenu
|
||||
visible: root.menuOpen
|
||||
screen: root.parentScreen
|
||||
WlrLayershell.layer: WlrLayershell.Top
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
WlrLayershell.namespace: "dms:tray-overflow-menu"
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
property point anchorPos: Qt.point(screen.width / 2, screen.height / 2)
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) updatePosition()
|
||||
}
|
||||
|
||||
function updatePosition() {
|
||||
if (!root.parentWindow) {
|
||||
anchorPos = Qt.point(screen.width / 2, screen.height / 2)
|
||||
return
|
||||
}
|
||||
|
||||
const globalPos = root.mapToGlobal(0, 0)
|
||||
const screenX = screen.x || 0
|
||||
const screenY = screen.y || 0
|
||||
const relativeX = globalPos.x - screenX
|
||||
const relativeY = globalPos.y - screenY
|
||||
|
||||
const widgetThickness = Math.max(20, 26 + SettingsData.dankBarInnerPadding * 0.6)
|
||||
const effectiveBarThickness = Math.max(widgetThickness + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding))
|
||||
|
||||
if (root.isVertical) {
|
||||
const edge = root.axis?.edge
|
||||
let targetX = edge === "left"
|
||||
? effectiveBarThickness + SettingsData.dankBarSpacing + Theme.popupDistance
|
||||
: screen.width - (effectiveBarThickness + SettingsData.dankBarSpacing + Theme.popupDistance)
|
||||
anchorPos = Qt.point(targetX, relativeY + root.height / 2)
|
||||
} else {
|
||||
let targetY = root.isAtBottom
|
||||
? screen.height - (effectiveBarThickness + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap + Theme.popupDistance)
|
||||
: effectiveBarThickness + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap + Theme.popupDistance
|
||||
anchorPos = Qt.point(relativeX + root.width / 2, targetY)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: menuContainer
|
||||
width: 250
|
||||
height: Math.min(screen.height - 100, menuColumn.implicitHeight + Theme.spacingS * 2)
|
||||
|
||||
x: {
|
||||
if (root.isVertical) {
|
||||
const edge = root.axis?.edge
|
||||
if (edge === "left") {
|
||||
const targetX = overflowMenu.anchorPos.x
|
||||
return Math.min(overflowMenu.screen.width - width - 10, targetX)
|
||||
} else {
|
||||
const targetX = overflowMenu.anchorPos.x - width
|
||||
return Math.max(10, targetX)
|
||||
}
|
||||
} else {
|
||||
const left = 10
|
||||
const right = overflowMenu.width - width - 10
|
||||
const want = overflowMenu.anchorPos.x - width / 2
|
||||
return Math.max(left, Math.min(right, want))
|
||||
}
|
||||
}
|
||||
|
||||
y: {
|
||||
if (root.isVertical) {
|
||||
const top = 10
|
||||
const bottom = overflowMenu.height - height - 10
|
||||
const want = overflowMenu.anchorPos.y - height / 2
|
||||
return Math.max(top, Math.min(bottom, want))
|
||||
} else {
|
||||
if (root.isAtBottom) {
|
||||
const targetY = overflowMenu.anchorPos.y - height
|
||||
return Math.max(10, targetY)
|
||||
} else {
|
||||
const targetY = overflowMenu.anchorPos.y
|
||||
return Math.min(overflowMenu.screen.height - height - 10, targetY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
|
||||
opacity: root.menuOpen ? 1 : 0
|
||||
scale: root.menuOpen ? 1 : 0.85
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 4
|
||||
anchors.leftMargin: 2
|
||||
anchors.rightMargin: -2
|
||||
anchors.bottomMargin: -4
|
||||
radius: parent.radius
|
||||
color: Qt.rgba(0, 0, 0, 0.15)
|
||||
z: parent.z - 1
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
id: scrollView
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
contentWidth: width
|
||||
contentHeight: menuColumn.implicitHeight
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: menuColumn
|
||||
width: parent.width
|
||||
spacing: 2
|
||||
|
||||
Repeater {
|
||||
model: root.allTrayItems
|
||||
|
||||
delegate: Rectangle {
|
||||
property var trayItem: modelData
|
||||
property string iconSource: {
|
||||
let icon = trayItem?.icon
|
||||
if (typeof icon === 'string' || icon instanceof String) {
|
||||
if (icon === "") return ""
|
||||
if (icon.includes("?path=")) {
|
||||
const split = icon.split("?path=")
|
||||
if (split.length !== 2) return icon
|
||||
const name = split[0]
|
||||
const path = split[1]
|
||||
let fileName = name.substring(name.lastIndexOf("/") + 1)
|
||||
if (fileName.startsWith("dropboxstatus")) {
|
||||
fileName = `hicolor/16x16/status/${fileName}`
|
||||
}
|
||||
return `file://${path}/${fileName}`
|
||||
}
|
||||
if (icon.startsWith("/") && !icon.startsWith("file://")) {
|
||||
return `file://${icon}`
|
||||
}
|
||||
return icon
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
width: menuColumn.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: itemArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
IconImage {
|
||||
id: menuIconImg
|
||||
width: 20
|
||||
height: 20
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
source: parent.parent.iconSource
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
mipmap: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !menuIconImg.visible
|
||||
text: {
|
||||
const itemId = trayItem?.id || ""
|
||||
if (!itemId) return "?"
|
||||
return itemId.charAt(0).toUpperCase()
|
||||
}
|
||||
font.pixelSize: 10
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: trayItem?.tooltip?.title || trayItem?.id || "Unknown"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Math.min(implicitWidth, menuColumn.width - 80)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 24
|
||||
height: 24
|
||||
radius: Theme.cornerRadius
|
||||
color: visibilityArea.containsMouse ? Theme.primaryHover : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: SessionData.isHiddenTrayId(trayItem?.id || "") ? "visibility_off" : "visibility"
|
||||
size: 16
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: visibilityArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
const itemId = trayItem?.id || ""
|
||||
if (!itemId) return
|
||||
if (SessionData.isHiddenTrayId(itemId)) {
|
||||
SessionData.showTrayId(itemId)
|
||||
} else {
|
||||
SessionData.hideTrayId(itemId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: itemArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 32
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: (mouse) => {
|
||||
if (!trayItem) return
|
||||
|
||||
if (mouse.button === Qt.LeftButton && !trayItem.onlyMenu) {
|
||||
trayItem.activate()
|
||||
root.menuOpen = false
|
||||
return
|
||||
}
|
||||
if (trayItem.hasMenu) {
|
||||
root.menuOpen = false
|
||||
root.showForTrayItem(trayItem, parent, parentScreen, root.isAtBottom, root.isVertical, root.axis)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
onClicked: root.menuOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,9 +690,12 @@ Item {
|
||||
color: "transparent"
|
||||
|
||||
PanelWindow {
|
||||
|
||||
WlrLayershell.namespace: "dms:tray-menu-window"
|
||||
|
||||
id: menuWindow
|
||||
visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false)
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.layer: WlrLayershell.Top
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
@@ -678,7 +1031,21 @@ Item {
|
||||
|
||||
property var currentTrayMenu: null
|
||||
|
||||
Connections {
|
||||
target: currentTrayMenu
|
||||
enabled: currentTrayMenu !== null
|
||||
function onShowMenuChanged() {
|
||||
if (parentWindow && typeof parentWindow.systemTrayMenuOpen !== "undefined") {
|
||||
parentWindow.systemTrayMenuOpen = currentTrayMenu.showMenu
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) {
|
||||
if (parentWindow && typeof parentWindow.systemTrayMenuOpen !== "undefined") {
|
||||
parentWindow.systemTrayMenuOpen = true
|
||||
}
|
||||
|
||||
if (currentTrayMenu) {
|
||||
currentTrayMenu.destroy()
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ Item {
|
||||
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
|
||||
}
|
||||
|
||||
readonly property bool useExtWorkspace: DMSService.forceExtWorkspace || (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isDwl && !CompositorService.isSway && ExtWorkspaceService.extWorkspaceAvailable)
|
||||
|
||||
Connections {
|
||||
target: DesktopEntries
|
||||
function onApplicationsChanged() {
|
||||
@@ -30,18 +32,22 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
property int currentWorkspace: {
|
||||
if (CompositorService.isNiri) {
|
||||
property var currentWorkspace: {
|
||||
if (useExtWorkspace) return getExtWorkspaceActiveWorkspace()
|
||||
|
||||
switch (CompositorService.compositor) {
|
||||
case "niri":
|
||||
return getNiriActiveWorkspace()
|
||||
} else if (CompositorService.isHyprland) {
|
||||
case "hyprland":
|
||||
return getHyprlandActiveWorkspace()
|
||||
} else if (CompositorService.isDwl) {
|
||||
case "dwl":
|
||||
const activeTags = getDwlActiveTags()
|
||||
return activeTags.length > 0 ? activeTags[0] : -1
|
||||
} else if (CompositorService.isSway) {
|
||||
case "sway":
|
||||
return getSwayActiveWorkspace()
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
property var dwlActiveTags: {
|
||||
if (CompositorService.isDwl) {
|
||||
@@ -50,24 +56,29 @@ Item {
|
||||
return []
|
||||
}
|
||||
property var workspaceList: {
|
||||
if (CompositorService.isNiri) {
|
||||
const baseList = getNiriWorkspaces()
|
||||
if (useExtWorkspace) {
|
||||
const baseList = getExtWorkspaceWorkspaces()
|
||||
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
|
||||
}
|
||||
if (CompositorService.isHyprland) {
|
||||
const baseList = getHyprlandWorkspaces()
|
||||
const filteredList = baseList.filter(ws => ws.id > -1)
|
||||
return SettingsData.showWorkspacePadding ? padWorkspaces(filteredList) : filteredList
|
||||
|
||||
let baseList
|
||||
switch (CompositorService.compositor) {
|
||||
case "niri":
|
||||
baseList = getNiriWorkspaces()
|
||||
break
|
||||
case "hyprland":
|
||||
baseList = getHyprlandWorkspaces()
|
||||
break
|
||||
case "dwl":
|
||||
baseList = getDwlTags()
|
||||
break
|
||||
case "sway":
|
||||
baseList = getSwayWorkspaces()
|
||||
break
|
||||
default:
|
||||
return [1]
|
||||
}
|
||||
if (CompositorService.isDwl) {
|
||||
const baseList = getDwlTags()
|
||||
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
|
||||
}
|
||||
if (CompositorService.isSway) {
|
||||
const baseList = getSwayWorkspaces()
|
||||
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
|
||||
}
|
||||
return [1]
|
||||
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
|
||||
}
|
||||
|
||||
function getSwayWorkspaces() {
|
||||
@@ -92,6 +103,27 @@ Item {
|
||||
return focusedWs ? focusedWs.num : 1
|
||||
}
|
||||
|
||||
function getHyprlandWorkspaces() {
|
||||
const workspaces = Hyprland.workspaces?.values || []
|
||||
if (workspaces.length === 0) return [{id: 1}]
|
||||
|
||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
||||
return workspaces.slice().sort((a, b) => a.id - b.id)
|
||||
}
|
||||
|
||||
const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === root.screenName)
|
||||
return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.id - b.id) : [{id: 1}]
|
||||
}
|
||||
|
||||
function getHyprlandActiveWorkspace() {
|
||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
||||
return Hyprland.focusedWorkspace?.id || 1
|
||||
}
|
||||
|
||||
const monitor = Hyprland.monitors?.values?.find(m => m.name === root.screenName)
|
||||
return monitor?.activeWorkspace?.id || 1
|
||||
}
|
||||
|
||||
function getWorkspaceIcons(ws) {
|
||||
_desktopEntriesUpdateTrigger
|
||||
if (!SettingsData.showWorkspaceApps || !ws) {
|
||||
@@ -162,13 +194,13 @@ Item {
|
||||
return
|
||||
}
|
||||
|
||||
const keyBase = (w.app_id || w.appId || w.class || w.windowClass || "unknown").toLowerCase()
|
||||
const keyBase = (w.app_id || w.appId || w.class || w.windowClass || "unknown")
|
||||
const key = isActiveWs ? `${keyBase}_${i}` : keyBase
|
||||
|
||||
if (!byApp[key]) {
|
||||
const moddedId = Paths.moddedAppId(keyBase)
|
||||
const isSteamApp = moddedId.toLowerCase().includes("steam_app")
|
||||
const icon = isSteamApp ? "" : Quickshell.iconPath(DesktopEntries.heuristicLookup(moddedId)?.icon, true)
|
||||
const icon = isSteamApp ? "" : DesktopService.resolveIconPath(moddedId)
|
||||
byApp[key] = {
|
||||
"type": "icon",
|
||||
"icon": icon,
|
||||
@@ -192,7 +224,9 @@ Item {
|
||||
function padWorkspaces(list) {
|
||||
const padded = list.slice()
|
||||
let placeholder
|
||||
if (CompositorService.isHyprland) {
|
||||
if (useExtWorkspace) {
|
||||
placeholder = {"id": "", "name": "", "active": false, "hidden": true}
|
||||
} else if (CompositorService.isHyprland) {
|
||||
placeholder = {"id": -1, "name": ""}
|
||||
} else if (CompositorService.isDwl) {
|
||||
placeholder = {"tag": -1}
|
||||
@@ -233,51 +267,6 @@ Item {
|
||||
return activeWs ? activeWs.idx + 1 : 1
|
||||
}
|
||||
|
||||
function getHyprlandWorkspaces() {
|
||||
const workspaces = Hyprland.workspaces?.values || []
|
||||
|
||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
||||
// Show all workspaces on all monitors if per-monitor filtering is disabled
|
||||
const sorted = workspaces.slice().sort((a, b) => a.id - b.id)
|
||||
return sorted.length > 0 ? sorted : [{
|
||||
"id": 1,
|
||||
"name": "1"
|
||||
}]
|
||||
}
|
||||
|
||||
// Filter workspaces for this specific monitor using lastIpcObject.monitor
|
||||
// This matches the approach from the original kyle-config
|
||||
const monitorWorkspaces = workspaces.filter(ws => {
|
||||
return ws.lastIpcObject && ws.lastIpcObject.monitor === root.screenName
|
||||
})
|
||||
|
||||
if (monitorWorkspaces.length === 0) {
|
||||
// Fallback if no workspaces exist for this monitor
|
||||
return [{
|
||||
"id": 1,
|
||||
"name": "1"
|
||||
}]
|
||||
}
|
||||
|
||||
// Return all workspaces for this monitor, sorted by ID
|
||||
return monitorWorkspaces.sort((a, b) => a.id - b.id)
|
||||
}
|
||||
|
||||
function getHyprlandActiveWorkspace() {
|
||||
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
|
||||
return Hyprland.focusedWorkspace ? Hyprland.focusedWorkspace.id : 1
|
||||
}
|
||||
|
||||
const monitors = Hyprland.monitors?.values || []
|
||||
const currentMonitor = monitors.find(monitor => monitor.name === root.screenName)
|
||||
|
||||
if (!currentMonitor) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return currentMonitor.activeWorkspace?.id ?? 1
|
||||
}
|
||||
|
||||
function getDwlTags() {
|
||||
if (!DwlService.dwlAvailable) {
|
||||
return []
|
||||
@@ -313,25 +302,77 @@ Item {
|
||||
return activeTags
|
||||
}
|
||||
|
||||
function getExtWorkspaceWorkspaces() {
|
||||
const groups = ExtWorkspaceService.groups
|
||||
if (!ExtWorkspaceService.extWorkspaceAvailable || groups.length === 0) {
|
||||
return [{"id": "1", "name": "1", "active": false}]
|
||||
}
|
||||
|
||||
const group = groups.find(g => g.outputs && g.outputs.includes(root.screenName))
|
||||
if (!group || !group.workspaces) {
|
||||
return [{"id": "1", "name": "1", "active": false}]
|
||||
}
|
||||
|
||||
const visible = group.workspaces.filter(ws => !ws.hidden).sort((a, b) => {
|
||||
const coordsA = a.coordinates || [0, 0]
|
||||
const coordsB = b.coordinates || [0, 0]
|
||||
if (coordsA[0] !== coordsB[0]) return coordsA[0] - coordsB[0]
|
||||
return coordsA[1] - coordsB[1]
|
||||
}).map(ws => ({
|
||||
id: ws.id,
|
||||
name: ws.name,
|
||||
coordinates: ws.coordinates,
|
||||
state: ws.state,
|
||||
active: ws.active,
|
||||
urgent: ws.urgent,
|
||||
hidden: ws.hidden,
|
||||
groupID: group.id
|
||||
}))
|
||||
|
||||
return visible.length > 0 ? visible : [{"id": "1", "name": "1", "active": false}]
|
||||
}
|
||||
|
||||
function getExtWorkspaceActiveWorkspace() {
|
||||
if (!ExtWorkspaceService.extWorkspaceAvailable) {
|
||||
return 1
|
||||
}
|
||||
|
||||
const activeWs = ExtWorkspaceService.getActiveWorkspaceForOutput(root.screenName)
|
||||
return activeWs ? (activeWs.id || activeWs.name || "1") : "1"
|
||||
}
|
||||
|
||||
readonly property real padding: Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
||||
readonly property real visualWidth: isVertical ? widgetHeight : (workspaceRow.implicitWidth + padding * 2)
|
||||
readonly property real visualHeight: isVertical ? (workspaceRow.implicitHeight + padding * 2) : widgetHeight
|
||||
|
||||
function getRealWorkspaces() {
|
||||
return root.workspaceList.filter(ws => {
|
||||
if (CompositorService.isHyprland) {
|
||||
return ws && ws.id !== -1
|
||||
} else if (CompositorService.isDwl) {
|
||||
return ws && ws.tag !== -1
|
||||
} else if (CompositorService.isSway) {
|
||||
return ws && ws.num !== -1
|
||||
}
|
||||
if (useExtWorkspace) return ws && ws.id !== "" && !ws.hidden
|
||||
if (CompositorService.isHyprland) return ws && ws.id !== -1
|
||||
if (CompositorService.isDwl) return ws && ws.tag !== -1
|
||||
if (CompositorService.isSway) return ws && ws.num !== -1
|
||||
return ws !== -1
|
||||
})
|
||||
}
|
||||
|
||||
function switchWorkspace(direction) {
|
||||
if (CompositorService.isNiri) {
|
||||
if (useExtWorkspace) {
|
||||
const realWorkspaces = getRealWorkspaces()
|
||||
if (realWorkspaces.length < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentIndex = realWorkspaces.findIndex(ws => (ws.id || ws.name) === root.currentWorkspace)
|
||||
const validIndex = currentIndex === -1 ? 0 : currentIndex
|
||||
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
|
||||
|
||||
if (nextIndex === validIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextWorkspace = realWorkspaces[nextIndex]
|
||||
ExtWorkspaceService.activateWorkspace(nextWorkspace.id || nextWorkspace.name, nextWorkspace.groupID || "")
|
||||
} else if (CompositorService.isNiri) {
|
||||
const realWorkspaces = getRealWorkspaces()
|
||||
if (realWorkspaces.length < 2) {
|
||||
return
|
||||
@@ -396,7 +437,7 @@ Item {
|
||||
|
||||
width: isVertical ? barThickness : visualWidth
|
||||
height: isVertical ? visualHeight : barThickness
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || useExtWorkspace
|
||||
|
||||
Rectangle {
|
||||
id: visualBackground
|
||||
@@ -435,29 +476,25 @@ Item {
|
||||
flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
|
||||
|
||||
Repeater {
|
||||
model: root.workspaceList
|
||||
model: ScriptModel {
|
||||
values: root.workspaceList
|
||||
}
|
||||
|
||||
Item {
|
||||
id: delegateRoot
|
||||
|
||||
property bool isActive: {
|
||||
if (CompositorService.isHyprland) {
|
||||
return modelData && modelData.id === root.currentWorkspace
|
||||
} else if (CompositorService.isDwl) {
|
||||
return modelData && root.dwlActiveTags.includes(modelData.tag)
|
||||
} else if (CompositorService.isSway) {
|
||||
return modelData && modelData.num === root.currentWorkspace
|
||||
}
|
||||
if (root.useExtWorkspace) return (modelData?.id || modelData?.name) === root.currentWorkspace
|
||||
if (CompositorService.isHyprland) return !!(modelData && modelData.id === root.currentWorkspace)
|
||||
if (CompositorService.isDwl) return !!(modelData && root.dwlActiveTags.includes(modelData.tag))
|
||||
if (CompositorService.isSway) return !!(modelData && modelData.num === root.currentWorkspace)
|
||||
return modelData === root.currentWorkspace
|
||||
}
|
||||
property bool isPlaceholder: {
|
||||
if (CompositorService.isHyprland) {
|
||||
return modelData && modelData.id === -1
|
||||
} else if (CompositorService.isDwl) {
|
||||
return modelData && modelData.tag === -1
|
||||
} else if (CompositorService.isSway) {
|
||||
return modelData && modelData.num === -1
|
||||
}
|
||||
if (root.useExtWorkspace) return !!(modelData && modelData.hidden)
|
||||
if (CompositorService.isHyprland) return !!(modelData && modelData.id === -1)
|
||||
if (CompositorService.isDwl) return !!(modelData && modelData.tag === -1)
|
||||
if (CompositorService.isSway) return !!(modelData && modelData.num === -1)
|
||||
return modelData === -1
|
||||
}
|
||||
property bool isHovered: mouseArea.containsMouse
|
||||
@@ -465,15 +502,11 @@ Item {
|
||||
property var loadedWorkspaceData: null
|
||||
property bool loadedIsUrgent: false
|
||||
property bool isUrgent: {
|
||||
if (CompositorService.isHyprland) {
|
||||
return modelData?.urgent ?? false
|
||||
} else if (CompositorService.isNiri) {
|
||||
return loadedIsUrgent
|
||||
} else if (CompositorService.isDwl) {
|
||||
return modelData?.state === 2
|
||||
} else if (CompositorService.isSway) {
|
||||
return loadedIsUrgent
|
||||
}
|
||||
if (root.useExtWorkspace) return modelData?.urgent ?? false
|
||||
if (CompositorService.isHyprland) return modelData?.urgent ?? false
|
||||
if (CompositorService.isNiri) return loadedIsUrgent
|
||||
if (CompositorService.isDwl) return modelData?.state === 2
|
||||
if (CompositorService.isSway) return loadedIsUrgent
|
||||
return false
|
||||
}
|
||||
property var loadedIconData: null
|
||||
@@ -509,13 +542,13 @@ Item {
|
||||
enabled: !isPlaceholder
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: mouse => {
|
||||
if (isPlaceholder) {
|
||||
return
|
||||
}
|
||||
if (isPlaceholder) return
|
||||
|
||||
const isRightClick = mouse.button === Qt.RightButton
|
||||
|
||||
if (CompositorService.isNiri) {
|
||||
if (root.useExtWorkspace && (modelData?.id || modelData?.name)) {
|
||||
ExtWorkspaceService.activateWorkspace(modelData.id || modelData.name, modelData.groupID || "")
|
||||
} else if (CompositorService.isNiri) {
|
||||
if (isRightClick) {
|
||||
NiriService.toggleOverview()
|
||||
} else {
|
||||
@@ -556,7 +589,9 @@ Item {
|
||||
}
|
||||
|
||||
var wsData = null;
|
||||
if (CompositorService.isNiri) {
|
||||
if (root.useExtWorkspace) {
|
||||
wsData = modelData;
|
||||
} else if (CompositorService.isNiri) {
|
||||
wsData = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.screenName) || null;
|
||||
} else if (CompositorService.isHyprland) {
|
||||
wsData = modelData;
|
||||
@@ -658,7 +693,9 @@ Item {
|
||||
visible: loadedIcons.length > 0
|
||||
|
||||
Repeater {
|
||||
model: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons)
|
||||
model: ScriptModel {
|
||||
values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons)
|
||||
}
|
||||
delegate: Item {
|
||||
width: 18
|
||||
height: 18
|
||||
@@ -687,6 +724,7 @@ Item {
|
||||
enabled: isActive
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (!appIcon.windowId) return
|
||||
if (CompositorService.isHyprland) {
|
||||
Hyprland.dispatch(`focuswindow address:${appIcon.windowId}`)
|
||||
} else if (CompositorService.isNiri) {
|
||||
@@ -726,7 +764,9 @@ Item {
|
||||
visible: loadedIcons.length > 0
|
||||
|
||||
Repeater {
|
||||
model: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons)
|
||||
model: ScriptModel {
|
||||
values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons)
|
||||
}
|
||||
delegate: Item {
|
||||
width: 18
|
||||
height: 18
|
||||
@@ -755,6 +795,7 @@ Item {
|
||||
enabled: isActive
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (!appIcon.windowId) return
|
||||
if (CompositorService.isHyprland) {
|
||||
Hyprland.dispatch(`focuswindow address:${appIcon.windowId}`)
|
||||
} else if (CompositorService.isNiri) {
|
||||
@@ -831,7 +872,9 @@ Item {
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
let isPlaceholder
|
||||
if (CompositorService.isHyprland) {
|
||||
if (root.useExtWorkspace) {
|
||||
isPlaceholder = modelData?.hidden === true
|
||||
} else if (CompositorService.isHyprland) {
|
||||
isPlaceholder = modelData?.id === -1
|
||||
} else if (CompositorService.isDwl) {
|
||||
isPlaceholder = modelData?.tag === -1
|
||||
@@ -841,17 +884,12 @@ Item {
|
||||
isPlaceholder = modelData === -1
|
||||
}
|
||||
|
||||
if (isPlaceholder) {
|
||||
return index + 1
|
||||
}
|
||||
if (isPlaceholder) return index + 1
|
||||
|
||||
if (CompositorService.isHyprland) {
|
||||
return modelData?.id || ""
|
||||
} else if (CompositorService.isDwl) {
|
||||
return (modelData?.tag !== undefined) ? (modelData.tag + 1) : ""
|
||||
} else if (CompositorService.isSway) {
|
||||
return modelData?.num || ""
|
||||
}
|
||||
if (root.useExtWorkspace) return modelData?.name || modelData?.id || ""
|
||||
if (CompositorService.isHyprland) return modelData?.id || ""
|
||||
if (CompositorService.isDwl) return (modelData?.tag !== undefined) ? (modelData.tag + 1) : ""
|
||||
if (CompositorService.isSway) return modelData?.num || ""
|
||||
return modelData - 1
|
||||
}
|
||||
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
|
||||
@@ -885,12 +923,28 @@ Item {
|
||||
enabled: CompositorService.isDwl
|
||||
function onStateChanged() { delegateRoot.updateAllData() }
|
||||
}
|
||||
Connections {
|
||||
target: Hyprland.workspaces
|
||||
enabled: CompositorService.isHyprland
|
||||
function onValuesChanged() { delegateRoot.updateAllData() }
|
||||
}
|
||||
Connections {
|
||||
target: I3.workspaces
|
||||
enabled: CompositorService.isSway
|
||||
function onValuesChanged() { delegateRoot.updateAllData() }
|
||||
}
|
||||
Connections {
|
||||
target: ExtWorkspaceService
|
||||
enabled: root.useExtWorkspace
|
||||
function onStateChanged() { delegateRoot.updateAllData() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (useExtWorkspace && !DMSService.activeSubscriptions.includes("extworkspace")) {
|
||||
DMSService.addSubscription("extworkspace")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ Variants {
|
||||
}
|
||||
|
||||
readonly property real dockMargin: SettingsData.dockSpacing
|
||||
readonly property real positionSpacing: barSpacing + SettingsData.dockBottomGap
|
||||
readonly property real positionSpacing: barSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin
|
||||
readonly property real _dpr: (dock.screen && dock.screen.devicePixelRatio) ? dock.screen.devicePixelRatio : 1
|
||||
function px(v) { return Math.round(v * _dpr) / _dpr }
|
||||
|
||||
@@ -103,13 +103,13 @@ Variants {
|
||||
exclusiveZone: {
|
||||
if (!SettingsData.showDock || autoHide) return -1
|
||||
if (barSpacing > 0) return -1
|
||||
return px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap)
|
||||
return px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin)
|
||||
}
|
||||
|
||||
property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35)
|
||||
|
||||
implicitWidth: isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
||||
implicitHeight: !isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
||||
implicitWidth: isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
||||
implicitHeight: !isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
||||
|
||||
Item {
|
||||
id: maskItem
|
||||
@@ -188,7 +188,7 @@ Variants {
|
||||
const isBottom = SettingsData.dockPosition === SettingsData.Position.Bottom
|
||||
const globalX = buttonGlobalPos.x + dock.hoveredButton.width / 2
|
||||
const screenRelativeY = isBottom
|
||||
? (screenHeight - dock.effectiveBarHeight - SettingsData.dockSpacing - SettingsData.dockBottomGap - 35)
|
||||
? (screenHeight - dock.effectiveBarHeight - SettingsData.dockSpacing - SettingsData.dockBottomGap - SettingsData.dockMargin - 35)
|
||||
: (buttonGlobalPos.y - screenY + dock.hoveredButton.height + Theme.spacingS)
|
||||
dockTooltip.show(tooltipText,
|
||||
globalX,
|
||||
@@ -197,7 +197,7 @@ Variants {
|
||||
false, false)
|
||||
} else {
|
||||
const isLeft = SettingsData.dockPosition === SettingsData.Position.Left
|
||||
const tooltipOffset = dock.effectiveBarHeight + SettingsData.dockSpacing + Theme.spacingXS
|
||||
const tooltipOffset = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockMargin + Theme.spacingXS
|
||||
const tooltipX = isLeft ? tooltipOffset : (dock.screen.width - tooltipOffset)
|
||||
const screenRelativeY = buttonGlobalPos.y - screenY + dock.hoveredButton.height / 2
|
||||
dockTooltip.show(tooltipText,
|
||||
@@ -259,12 +259,12 @@ Variants {
|
||||
if (dock.isVertical) {
|
||||
return dock.reveal ? Math.min(dockBackground.implicitHeight + 4, maxDockHeight) : Math.min(Math.max(dockBackground.implicitHeight + 64, 200), screenHeight * 0.5)
|
||||
} else {
|
||||
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap) : 1
|
||||
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1
|
||||
}
|
||||
}
|
||||
width: {
|
||||
if (dock.isVertical) {
|
||||
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap) : 1
|
||||
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1
|
||||
} else {
|
||||
return dock.reveal ? Math.min(dockBackground.implicitWidth + 4, maxDockWidth) : Math.min(Math.max(dockBackground.implicitWidth + 64, 200), screenWidth * 0.5)
|
||||
}
|
||||
@@ -304,7 +304,7 @@ Variants {
|
||||
x: {
|
||||
if (!dock.isVertical) return 0
|
||||
if (dock.reveal) return 0
|
||||
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + 10
|
||||
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Right) {
|
||||
return hideDistance
|
||||
} else {
|
||||
@@ -314,7 +314,7 @@ Variants {
|
||||
y: {
|
||||
if (dock.isVertical) return 0
|
||||
if (dock.reveal) return 0
|
||||
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + 10
|
||||
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Bottom) {
|
||||
return hideDistance
|
||||
} else {
|
||||
@@ -339,7 +339,7 @@ Variants {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Item {
|
||||
id: dockBackground
|
||||
objectName: "dockBackground"
|
||||
anchors {
|
||||
@@ -350,26 +350,23 @@ Variants {
|
||||
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
|
||||
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
|
||||
}
|
||||
anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? barSpacing + 1 : 0
|
||||
anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? barSpacing + 1 : 0
|
||||
anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? barSpacing + 1 : 0
|
||||
anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? barSpacing + 1 : 0
|
||||
anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? barSpacing + SettingsData.dockMargin + 1 : 0
|
||||
anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? barSpacing + SettingsData.dockMargin + 1 : 0
|
||||
anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? barSpacing + SettingsData.dockMargin + 1 : 0
|
||||
anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? barSpacing + SettingsData.dockMargin + 1 : 0
|
||||
|
||||
implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2)
|
||||
implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2)
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, backgroundTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 1
|
||||
border.color: Theme.outlineMedium
|
||||
layer.enabled: true
|
||||
clip: false
|
||||
|
||||
Rectangle {
|
||||
DankRectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
|
||||
radius: parent.radius
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, backgroundTransparency)
|
||||
overlayColor: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
@@ -91,56 +92,11 @@ Item {
|
||||
}
|
||||
|
||||
function getToplevelObject() {
|
||||
if (!appData) {
|
||||
return null
|
||||
}
|
||||
|
||||
const sortedToplevels = CompositorService.sortedToplevels
|
||||
if (!sortedToplevels) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (appData.type === "window") {
|
||||
if (appData.uniqueId) {
|
||||
for (var i = 0; i < sortedToplevels.length; i++) {
|
||||
const toplevel = sortedToplevels[i]
|
||||
const checkId = toplevel.title + "|" + (toplevel.appId || "") + "|" + i
|
||||
if (checkId === appData.uniqueId) {
|
||||
return toplevel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (appData.windowId !== undefined && appData.windowId !== null && appData.windowId >= 0) {
|
||||
if (appData.windowId < sortedToplevels.length) {
|
||||
return sortedToplevels[appData.windowId]
|
||||
}
|
||||
}
|
||||
} else if (appData.type === "grouped") {
|
||||
if (appData.windowId !== undefined && appData.windowId !== null && appData.windowId >= 0) {
|
||||
if (appData.windowId < sortedToplevels.length) {
|
||||
return sortedToplevels[appData.windowId]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
return appData?.toplevel || null
|
||||
}
|
||||
|
||||
function getGroupedToplevels() {
|
||||
if (!appData || appData.type !== "grouped") {
|
||||
return []
|
||||
}
|
||||
|
||||
const toplevels = []
|
||||
const allToplevels = ToplevelManager.toplevels.values
|
||||
for (let i = 0; i < allToplevels.length; i++) {
|
||||
const toplevel = allToplevels[i]
|
||||
if (toplevel.appId === appData.appId) {
|
||||
toplevels.push(toplevel)
|
||||
}
|
||||
}
|
||||
return toplevels
|
||||
return appData?.allWindows?.map(w => w.toplevel).filter(t => t !== null) || []
|
||||
}
|
||||
onIsHoveredChanged: {
|
||||
if (mouseArea.pressed) return
|
||||
@@ -325,17 +281,9 @@ Item {
|
||||
}
|
||||
}
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
if (appData && appData.type === "window") {
|
||||
const sortedToplevels = CompositorService.sortedToplevels
|
||||
for (var i = 0; i < sortedToplevels.length; i++) {
|
||||
const toplevel = sortedToplevels[i]
|
||||
const checkId = toplevel.title + "|" + (toplevel.appId || "") + "|" + i
|
||||
if (checkId === appData.uniqueId) {
|
||||
toplevel.close()
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (appData && appData.type === "grouped") {
|
||||
if (appData?.type === "window") {
|
||||
appData?.toplevel?.close()
|
||||
} else if (appData?.type === "grouped") {
|
||||
if (contextMenu) {
|
||||
contextMenu.showForButton(root, appData, root.height, false, cachedDesktopEntry, parentDockScreen)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -54,158 +55,149 @@ Item {
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: ListModel {
|
||||
id: dockModel
|
||||
|
||||
Component.onCompleted: updateModel()
|
||||
property var dockItems: []
|
||||
|
||||
function updateModel() {
|
||||
clear()
|
||||
model: ScriptModel {
|
||||
values: repeater.dockItems
|
||||
objectProp: "uniqueKey"
|
||||
}
|
||||
|
||||
const items = []
|
||||
const pinnedApps = [...(SessionData.pinnedApps || [])]
|
||||
const sortedToplevels = CompositorService.sortedToplevels
|
||||
Component.onCompleted: updateModel()
|
||||
|
||||
if (root.groupByApp) {
|
||||
// Group windows by appId
|
||||
const appGroups = new Map()
|
||||
function updateModel() {
|
||||
const items = []
|
||||
const pinnedApps = [...(SessionData.pinnedApps || [])]
|
||||
const sortedToplevels = CompositorService.sortedToplevels
|
||||
|
||||
// Add pinned apps first (even if they have no windows)
|
||||
pinnedApps.forEach(appId => {
|
||||
if (root.groupByApp) {
|
||||
const appGroups = new Map()
|
||||
|
||||
pinnedApps.forEach(appId => {
|
||||
appGroups.set(appId, {
|
||||
appId: appId,
|
||||
isPinned: true,
|
||||
windows: []
|
||||
})
|
||||
})
|
||||
|
||||
sortedToplevels.forEach((toplevel, index) => {
|
||||
const appId = toplevel.appId || "unknown"
|
||||
if (!appGroups.has(appId)) {
|
||||
appGroups.set(appId, {
|
||||
appId: appId,
|
||||
isPinned: true,
|
||||
isPinned: false,
|
||||
windows: []
|
||||
})
|
||||
})
|
||||
|
||||
// Group all running windows by appId
|
||||
sortedToplevels.forEach((toplevel, index) => {
|
||||
const appId = toplevel.appId || "unknown"
|
||||
if (!appGroups.has(appId)) {
|
||||
appGroups.set(appId, {
|
||||
appId: appId,
|
||||
isPinned: false,
|
||||
windows: []
|
||||
})
|
||||
}
|
||||
const title = toplevel.title || "(Unnamed)"
|
||||
const truncatedTitle = title.length > 50 ? title.substring(0, 47) + "..." : title
|
||||
const uniqueId = toplevel.title + "|" + (toplevel.appId || "") + "|" + index
|
||||
|
||||
appGroups.get(appId).windows.push({
|
||||
windowId: index,
|
||||
windowTitle: truncatedTitle,
|
||||
uniqueId: uniqueId
|
||||
})
|
||||
})
|
||||
|
||||
// Sort groups: pinned first, then unpinned
|
||||
const pinnedGroups = []
|
||||
const unpinnedGroups = []
|
||||
|
||||
Array.from(appGroups.entries()).forEach(([appId, group]) => {
|
||||
// For grouped apps, just show the first window info but track all windows
|
||||
const firstWindow = group.windows.length > 0 ? group.windows[0] : null
|
||||
|
||||
const item = {
|
||||
"type": "grouped",
|
||||
"appId": appId,
|
||||
"windowId": firstWindow ? firstWindow.windowId : -1,
|
||||
"windowTitle": firstWindow ? firstWindow.windowTitle : "",
|
||||
"workspaceId": -1,
|
||||
"isPinned": group.isPinned,
|
||||
"isRunning": group.windows.length > 0,
|
||||
"windowCount": group.windows.length,
|
||||
"uniqueId": firstWindow ? firstWindow.uniqueId : "",
|
||||
"allWindows": group.windows
|
||||
}
|
||||
|
||||
if (group.isPinned) {
|
||||
pinnedGroups.push(item)
|
||||
} else {
|
||||
unpinnedGroups.push(item)
|
||||
}
|
||||
})
|
||||
|
||||
// Add items in order
|
||||
pinnedGroups.forEach(item => items.push(item))
|
||||
|
||||
// Add separator if needed
|
||||
if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) {
|
||||
items.push({
|
||||
"type": "separator",
|
||||
"appId": "__SEPARATOR__",
|
||||
"windowId": -1,
|
||||
"windowTitle": "",
|
||||
"workspaceId": -1,
|
||||
"isPinned": false,
|
||||
"isRunning": false
|
||||
})
|
||||
}
|
||||
|
||||
unpinnedGroups.forEach(item => items.push(item))
|
||||
root.pinnedAppCount = pinnedGroups.length
|
||||
} else {
|
||||
pinnedApps.forEach(appId => {
|
||||
items.push({
|
||||
"type": "pinned",
|
||||
"appId": appId,
|
||||
"windowId": -1,
|
||||
"windowTitle": "",
|
||||
"workspaceId": -1,
|
||||
"isPinned": true,
|
||||
"isRunning": false
|
||||
})
|
||||
appGroups.get(appId).windows.push({
|
||||
toplevel: toplevel,
|
||||
index: index
|
||||
})
|
||||
})
|
||||
|
||||
root.pinnedAppCount = pinnedApps.length
|
||||
const pinnedGroups = []
|
||||
const unpinnedGroups = []
|
||||
|
||||
if (pinnedApps.length > 0 && sortedToplevels.length > 0) {
|
||||
items.push({
|
||||
"type": "separator",
|
||||
"appId": "__SEPARATOR__",
|
||||
"windowId": -1,
|
||||
"windowTitle": "",
|
||||
"workspaceId": -1,
|
||||
"isPinned": false,
|
||||
"isRunning": false,
|
||||
"isFocused": false
|
||||
})
|
||||
Array.from(appGroups.entries()).forEach(([appId, group]) => {
|
||||
const firstWindow = group.windows.length > 0 ? group.windows[0] : null
|
||||
|
||||
const item = {
|
||||
uniqueKey: "grouped_" + appId,
|
||||
type: "grouped",
|
||||
appId: appId,
|
||||
toplevel: firstWindow ? firstWindow.toplevel : null,
|
||||
isPinned: group.isPinned,
|
||||
isRunning: group.windows.length > 0,
|
||||
windowCount: group.windows.length,
|
||||
allWindows: group.windows
|
||||
}
|
||||
|
||||
sortedToplevels.forEach((toplevel, index) => {
|
||||
const title = toplevel.title || "(Unnamed)"
|
||||
const truncatedTitle = title.length > 50 ? title.substring(0, 47) + "..." : title
|
||||
const uniqueId = toplevel.title + "|" + (toplevel.appId || "") + "|" + index
|
||||
if (group.isPinned) {
|
||||
pinnedGroups.push(item)
|
||||
} else {
|
||||
unpinnedGroups.push(item)
|
||||
}
|
||||
})
|
||||
|
||||
items.push({
|
||||
"type": "window",
|
||||
"appId": toplevel.appId,
|
||||
"windowId": index,
|
||||
"windowTitle": truncatedTitle,
|
||||
"workspaceId": -1,
|
||||
"isPinned": false,
|
||||
"isRunning": true,
|
||||
"uniqueId": uniqueId
|
||||
})
|
||||
pinnedGroups.forEach(item => items.push(item))
|
||||
|
||||
if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) {
|
||||
items.push({
|
||||
uniqueKey: "separator_grouped",
|
||||
type: "separator",
|
||||
appId: "__SEPARATOR__",
|
||||
toplevel: null,
|
||||
isPinned: false,
|
||||
isRunning: false
|
||||
})
|
||||
}
|
||||
|
||||
items.forEach(item => append(item))
|
||||
unpinnedGroups.forEach(item => items.push(item))
|
||||
root.pinnedAppCount = pinnedGroups.length
|
||||
} else {
|
||||
pinnedApps.forEach(appId => {
|
||||
items.push({
|
||||
uniqueKey: "pinned_" + appId,
|
||||
type: "pinned",
|
||||
appId: appId,
|
||||
toplevel: null,
|
||||
isPinned: true,
|
||||
isRunning: false
|
||||
})
|
||||
})
|
||||
|
||||
root.pinnedAppCount = pinnedApps.length
|
||||
|
||||
if (pinnedApps.length > 0 && sortedToplevels.length > 0) {
|
||||
items.push({
|
||||
uniqueKey: "separator_ungrouped",
|
||||
type: "separator",
|
||||
appId: "__SEPARATOR__",
|
||||
toplevel: null,
|
||||
isPinned: false,
|
||||
isRunning: false
|
||||
})
|
||||
}
|
||||
|
||||
sortedToplevels.forEach((toplevel, index) => {
|
||||
let uniqueKey = "window_" + index
|
||||
if (CompositorService.isHyprland && Hyprland.toplevels) {
|
||||
const hyprlandToplevels = Array.from(Hyprland.toplevels.values)
|
||||
for (let i = 0; i < hyprlandToplevels.length; i++) {
|
||||
if (hyprlandToplevels[i].wayland === toplevel) {
|
||||
uniqueKey = "window_" + hyprlandToplevels[i].address
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items.push({
|
||||
uniqueKey: uniqueKey,
|
||||
type: "window",
|
||||
appId: toplevel.appId,
|
||||
toplevel: toplevel,
|
||||
isPinned: false,
|
||||
isRunning: true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
dockItems = items
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: delegateItem
|
||||
property alias dockButton: button
|
||||
property var itemData: modelData
|
||||
clip: false
|
||||
|
||||
width: model.type === "separator" ? (root.isVertical ? root.iconSize : 8) : (root.isVertical ? root.iconSize : root.iconSize * 1.2)
|
||||
height: model.type === "separator" ? (root.isVertical ? 8 : root.iconSize) : (root.isVertical ? root.iconSize * 1.2 : root.iconSize)
|
||||
width: itemData.type === "separator" ? (root.isVertical ? root.iconSize : 8) : (root.isVertical ? root.iconSize : root.iconSize * 1.2)
|
||||
height: itemData.type === "separator" ? (root.isVertical ? 8 : root.iconSize) : (root.isVertical ? root.iconSize * 1.2 : root.iconSize)
|
||||
|
||||
Rectangle {
|
||||
visible: model.type === "separator"
|
||||
visible: itemData.type === "separator"
|
||||
width: root.isVertical ? root.iconSize * 0.5 : 2
|
||||
height: root.isVertical ? 2 : root.iconSize * 0.5
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||
@@ -215,21 +207,24 @@ Item {
|
||||
|
||||
DockAppButton {
|
||||
id: button
|
||||
visible: model.type !== "separator"
|
||||
visible: itemData.type !== "separator"
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: delegateItem.width
|
||||
height: delegateItem.height
|
||||
actualIconSize: root.iconSize
|
||||
|
||||
appData: model
|
||||
appData: itemData
|
||||
contextMenu: root.contextMenu
|
||||
dockApps: root
|
||||
index: model.index
|
||||
parentDockScreen: root.dockScreen
|
||||
|
||||
showWindowTitle: model.type === "window" || model.type === "grouped"
|
||||
windowTitle: model.windowTitle || ""
|
||||
showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped"
|
||||
windowTitle: {
|
||||
const title = itemData?.toplevel?.title || "(Unnamed)"
|
||||
return title.length > 50 ? title.substring(0, 47) + "..." : title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,19 +233,19 @@ Item {
|
||||
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onSortedToplevelsChanged() {
|
||||
dockModel.updateModel()
|
||||
function onToplevelsChanged() {
|
||||
repeater.updateModel()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
function onPinnedAppsChanged() {
|
||||
dockModel.updateModel()
|
||||
repeater.updateModel()
|
||||
}
|
||||
}
|
||||
|
||||
onGroupByAppChanged: {
|
||||
dockModel.updateModel()
|
||||
repeater.updateModel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
@@ -9,6 +10,8 @@ import qs.Widgets
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
WlrLayershell.namespace: "dms:dock-context-menu"
|
||||
|
||||
property var appData: null
|
||||
property var anchorItem: null
|
||||
property real dockVisibleHeight: 40
|
||||
@@ -91,7 +94,7 @@ PanelWindow {
|
||||
}
|
||||
|
||||
const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
|
||||
const dockMargin = 16
|
||||
const dockMargin = SettingsData.dockMargin + 16
|
||||
let buttonScreenX, buttonScreenY
|
||||
|
||||
if (isVertical) {
|
||||
@@ -475,24 +478,10 @@ PanelWindow {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
const sortedToplevels = CompositorService.sortedToplevels
|
||||
if (root.appData && root.appData.type === "window") {
|
||||
for (var i = 0; i < sortedToplevels.length; i++) {
|
||||
const toplevel = sortedToplevels[i]
|
||||
const checkId = toplevel.title + "|" + (toplevel.appId || "") + "|" + i
|
||||
if (checkId === root.appData.uniqueId) {
|
||||
toplevel.close()
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (root.appData && root.appData.type === "grouped") {
|
||||
const allToplevels = ToplevelManager.toplevels.values
|
||||
for (let i = 0; i < allToplevels.length; i++) {
|
||||
const toplevel = allToplevels[i]
|
||||
if (toplevel.appId === root.appData.appId) {
|
||||
toplevel.close()
|
||||
}
|
||||
}
|
||||
if (root.appData?.type === "window") {
|
||||
root.appData?.toplevel?.close()
|
||||
} else if (root.appData?.type === "grouped") {
|
||||
root.appData?.allWindows?.forEach(window => window.toplevel?.close())
|
||||
}
|
||||
root.close()
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ Singleton {
|
||||
property real fontScale: 1.0
|
||||
property real cornerRadius: 12
|
||||
property string widgetBackgroundColor: "sch"
|
||||
property string surfaceBase: "s"
|
||||
property string lockDateFormat: ""
|
||||
property bool lockScreenShowPowerActions: true
|
||||
property var screenPreferences: ({})
|
||||
@@ -74,7 +73,6 @@ Singleton {
|
||||
fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0
|
||||
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12
|
||||
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch"
|
||||
surfaceBase = settings.surfaceBase !== undefined ? settings.surfaceBase : "s"
|
||||
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : ""
|
||||
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true
|
||||
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({})
|
||||
|
||||
@@ -352,7 +352,7 @@ Item {
|
||||
}
|
||||
|
||||
onClicked: (event) => {
|
||||
if (!windowData) return
|
||||
if (!windowData || !windowData.address) return
|
||||
|
||||
if (event.button === Qt.LeftButton) {
|
||||
root.overviewOpen = false
|
||||
|
||||
@@ -14,6 +14,21 @@ DankActionButton {
|
||||
property bool isShift: false
|
||||
color: Theme.surface
|
||||
|
||||
property bool isIcon: text === "keyboard_hide" || text === "Backspace" || text === "Enter"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: {
|
||||
if (parent.text === "keyboard_hide") return "keyboard_hide"
|
||||
if (parent.text === "Backspace") return "backspace"
|
||||
if (parent.text === "Enter") return "keyboard_return"
|
||||
return ""
|
||||
}
|
||||
size: 20
|
||||
color: Theme.surfaceText
|
||||
visible: parent.isIcon
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: contentItem
|
||||
anchors.centerIn: parent
|
||||
@@ -21,5 +36,6 @@ DankActionButton {
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
font.weight: Font.Normal
|
||||
visible: !parent.isIcon
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,185 +10,157 @@ Rectangle {
|
||||
anchors.right: parent.right
|
||||
color: Theme.widgetBackground
|
||||
|
||||
property double rowSpacing: 0.01 * width // horizontal spacing between keyboard
|
||||
signal dismissed
|
||||
|
||||
property double rowSpacing: 0.01 * width // horizontal spacing between keyboard
|
||||
property double columnSpacing: 0.02 * height // vertical spacing between keyboard
|
||||
property bool shift: false //Boolean for the shift state
|
||||
property bool symbols: false //Boolean for the symbol state
|
||||
property double columns: 10 // Number of column
|
||||
property double rows: 4 // Number of row
|
||||
|
||||
property string strShift: '\u2191' // UPWARDS ARROW unicode
|
||||
property string strShift: '\u2191'
|
||||
property string strBackspace: "Backspace"
|
||||
property string strEnter: "Enter"
|
||||
property string strClose: "keyboard_hide"
|
||||
|
||||
property var modelKeyboard: {
|
||||
"row_1": [
|
||||
{
|
||||
text: 'q',
|
||||
symbol: '1',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 'w',
|
||||
symbol: '2',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 'e',
|
||||
symbol: '3',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 'r',
|
||||
symbol: '4',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 't',
|
||||
symbol: '5',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 'y',
|
||||
symbol: '6',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 'u',
|
||||
symbol: '7',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 'i',
|
||||
symbol: '8',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 'o',
|
||||
symbol: '9',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 'p',
|
||||
symbol: '0',
|
||||
width: 1
|
||||
},
|
||||
],
|
||||
"row_2": [
|
||||
{
|
||||
text: 'a',
|
||||
symbol: '-',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 's',
|
||||
symbol: '/',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 'd',
|
||||
symbol: ':',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 'f',
|
||||
symbol: ';',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 'g',
|
||||
symbol: '(',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 'h',
|
||||
symbol: ')',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 'j',
|
||||
symbol: '€',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 'k',
|
||||
symbol: '&',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 'l',
|
||||
symbol: '@',
|
||||
width: 1
|
||||
}
|
||||
],
|
||||
"row_3": [
|
||||
{
|
||||
text: strShift,
|
||||
symbol: strShift,
|
||||
width: 1.5
|
||||
},
|
||||
{
|
||||
text: 'z',
|
||||
symbol: '.',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 'x',
|
||||
symbol: ',',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 'c',
|
||||
symbol: '?',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 'v',
|
||||
symbol: '!',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 'b',
|
||||
symbol: "'",
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 'n',
|
||||
symbol: "%",
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: 'm',
|
||||
symbol: '"',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: "'",
|
||||
symbol: "*",
|
||||
width: 1.5
|
||||
}
|
||||
],
|
||||
"row_4": [
|
||||
{
|
||||
text: "123",
|
||||
symbol: 'ABC',
|
||||
width: 1.5
|
||||
},
|
||||
{
|
||||
text: ' ',
|
||||
symbol: ' ',
|
||||
width: 6
|
||||
},
|
||||
{
|
||||
text: '.',
|
||||
symbol: '.',
|
||||
width: 1
|
||||
},
|
||||
{
|
||||
text: strBackspace,
|
||||
symbol: strBackspace,
|
||||
width: 1.5
|
||||
}
|
||||
]
|
||||
"row_1": [{
|
||||
"text": 'q',
|
||||
"symbol": '1',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 'w',
|
||||
"symbol": '2',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 'e',
|
||||
"symbol": '3',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 'r',
|
||||
"symbol": '4',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 't',
|
||||
"symbol": '5',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 'y',
|
||||
"symbol": '6',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 'u',
|
||||
"symbol": '7',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 'i',
|
||||
"symbol": '8',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 'o',
|
||||
"symbol": '9',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 'p',
|
||||
"symbol": '0',
|
||||
"width": 1
|
||||
}],
|
||||
"row_2": [{
|
||||
"text": 'a',
|
||||
"symbol": '-',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 's',
|
||||
"symbol": '/',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 'd',
|
||||
"symbol": ':',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 'f',
|
||||
"symbol": ';',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 'g',
|
||||
"symbol": '(',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 'h',
|
||||
"symbol": ')',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 'j',
|
||||
"symbol": '€',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 'k',
|
||||
"symbol": '&',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 'l',
|
||||
"symbol": '@',
|
||||
"width": 1
|
||||
}],
|
||||
"row_3": [{
|
||||
"text": strShift,
|
||||
"symbol": strShift,
|
||||
"width": 1.5
|
||||
}, {
|
||||
"text": 'z',
|
||||
"symbol": '.',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 'x',
|
||||
"symbol": ',',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 'c',
|
||||
"symbol": '?',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 'v',
|
||||
"symbol": '!',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 'b',
|
||||
"symbol": "'",
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 'n',
|
||||
"symbol": "%",
|
||||
"width": 1
|
||||
}, {
|
||||
"text": 'm',
|
||||
"symbol": '"',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": strBackspace,
|
||||
"symbol": strBackspace,
|
||||
"width": 1.5
|
||||
}],
|
||||
"row_4": [{
|
||||
"text": strClose,
|
||||
"symbol": strClose,
|
||||
"width": 1.5
|
||||
}, {
|
||||
"text": "123",
|
||||
"symbol": 'ABC',
|
||||
"width": 1.5
|
||||
}, {
|
||||
"text": ' ',
|
||||
"symbol": ' ',
|
||||
"width": 4.5
|
||||
}, {
|
||||
"text": '.',
|
||||
"symbol": '.',
|
||||
"width": 1
|
||||
}, {
|
||||
"text": strEnter,
|
||||
"symbol": strEnter,
|
||||
"width": 1.5
|
||||
}]
|
||||
}
|
||||
|
||||
//Here is the corresponding table between the ascii and the key event
|
||||
@@ -229,7 +201,7 @@ Rectangle {
|
||||
"_x": Qt.Key_X,
|
||||
"_y": Qt.Key_Y,
|
||||
"_z": Qt.Key_Z,
|
||||
"_\u2190": Qt.Key_Backspace,
|
||||
"_←": Qt.Key_Backspace,
|
||||
"_return": Qt.Key_Return,
|
||||
"_ ": Qt.Key_Space,
|
||||
"_-": Qt.Key_Minus,
|
||||
@@ -238,7 +210,9 @@ Rectangle {
|
||||
"_;": Qt.Key_Semicolon,
|
||||
"_(": Qt.Key_BracketLeft,
|
||||
"_)": Qt.Key_BracketRight,
|
||||
"_€": parseInt("20ac", 16) // I didn't find the appropriate Qt event so I used the hex format
|
||||
"_€": parseInt(
|
||||
"20ac",
|
||||
16) // I didn't find the appropriate Qt event so I used the hex format
|
||||
,
|
||||
"_&": Qt.Key_Ampersand,
|
||||
"_@": Qt.Key_At,
|
||||
@@ -250,112 +224,121 @@ Rectangle {
|
||||
"_'": Qt.Key_Apostrophe,
|
||||
"_%": Qt.Key_Percent,
|
||||
"_*": Qt.Key_Asterisk
|
||||
}
|
||||
|
||||
Item {
|
||||
id: keyboard_container
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 5
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 5
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 5
|
||||
|
||||
//One column which contains 5 rows
|
||||
Column {
|
||||
spacing: columnSpacing
|
||||
|
||||
Row {
|
||||
id: row_1
|
||||
spacing: rowSpacing
|
||||
Repeater {
|
||||
model: modelKeyboard["row_1"]
|
||||
delegate: CustomButtonKeyboard {
|
||||
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
|
||||
width: modelData.width * keyboard_container.width / columns - rowSpacing
|
||||
height: keyboard_container.height / rows - columnSpacing
|
||||
|
||||
onClicked: root.clicked(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row {
|
||||
id: row_2
|
||||
spacing: rowSpacing
|
||||
Repeater {
|
||||
model: modelKeyboard["row_2"]
|
||||
delegate: CustomButtonKeyboard {
|
||||
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
|
||||
width: modelData.width * keyboard_container.width / columns - rowSpacing
|
||||
height: keyboard_container.height / rows - columnSpacing
|
||||
|
||||
onClicked: root.clicked(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row {
|
||||
id: row_3
|
||||
spacing: rowSpacing
|
||||
Repeater {
|
||||
model: modelKeyboard["row_3"]
|
||||
delegate: CustomButtonKeyboard {
|
||||
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
|
||||
width: modelData.width * keyboard_container.width / columns - rowSpacing
|
||||
height: keyboard_container.height / rows - columnSpacing
|
||||
isShift: shift && text === strShift
|
||||
|
||||
onClicked: root.clicked(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row {
|
||||
id: row_4
|
||||
spacing: rowSpacing
|
||||
Repeater {
|
||||
model: modelKeyboard["row_4"]
|
||||
delegate: CustomButtonKeyboard {
|
||||
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase() : modelData.text
|
||||
width: modelData.width * keyboard_container.width / columns - rowSpacing
|
||||
height: keyboard_container.height / rows - columnSpacing
|
||||
|
||||
onClicked: root.clicked(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
signal clicked(string text)
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onClicked(text) {
|
||||
if (!keyboard_controller.target)
|
||||
return;
|
||||
if (text === strShift) {
|
||||
root.shift = !root.shift; // toggle shift
|
||||
} else if (text === '123') {
|
||||
root.symbols = true;
|
||||
} else if (text === 'ABC') {
|
||||
root.symbols = false;
|
||||
} else {
|
||||
// insert text into target
|
||||
if (text === strBackspace) {
|
||||
var current = keyboard_controller.target.text;
|
||||
keyboard_controller.target.text = current.slice(0, current.length - 1);
|
||||
} else {
|
||||
// normal character
|
||||
var charToInsert = root.symbols ? text : (root.shift ? text.toUpperCase() : text);
|
||||
var current = keyboard_controller.target.text;
|
||||
var cursorPos = keyboard_controller.target.cursorPosition;
|
||||
keyboard_controller.target.text = current.slice(0, cursorPos) + charToInsert + current.slice(cursorPos);
|
||||
keyboard_controller.target.cursorPosition = cursorPos + 1;
|
||||
}
|
||||
|
||||
// shift is momentary
|
||||
if (root.shift && text !== strShift)
|
||||
root.shift = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: keyboard_container
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 5
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 5
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 5
|
||||
|
||||
//One column which contains 5 rows
|
||||
Column {
|
||||
spacing: columnSpacing
|
||||
|
||||
Row {
|
||||
id: row_1
|
||||
spacing: rowSpacing
|
||||
Repeater {
|
||||
model: modelKeyboard["row_1"]
|
||||
delegate: CustomButtonKeyboard {
|
||||
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase
|
||||
() : modelData.text
|
||||
width: modelData.width * keyboard_container.width / columns - rowSpacing
|
||||
height: keyboard_container.height / rows - columnSpacing
|
||||
|
||||
onClicked: root.clicked(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row {
|
||||
id: row_2
|
||||
spacing: rowSpacing
|
||||
Repeater {
|
||||
model: modelKeyboard["row_2"]
|
||||
delegate: CustomButtonKeyboard {
|
||||
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase
|
||||
() : modelData.text
|
||||
width: modelData.width * keyboard_container.width / columns - rowSpacing
|
||||
height: keyboard_container.height / rows - columnSpacing
|
||||
|
||||
onClicked: root.clicked(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row {
|
||||
id: row_3
|
||||
spacing: rowSpacing
|
||||
Repeater {
|
||||
model: modelKeyboard["row_3"]
|
||||
delegate: CustomButtonKeyboard {
|
||||
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase
|
||||
() : modelData.text
|
||||
width: modelData.width * keyboard_container.width / columns - rowSpacing
|
||||
height: keyboard_container.height / rows - columnSpacing
|
||||
isShift: shift && text === strShift
|
||||
|
||||
onClicked: root.clicked(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row {
|
||||
id: row_4
|
||||
spacing: rowSpacing
|
||||
Repeater {
|
||||
model: modelKeyboard["row_4"]
|
||||
delegate: CustomButtonKeyboard {
|
||||
text: symbols ? modelData.symbol : shift ? modelData.text.toUpperCase
|
||||
() : modelData.text
|
||||
width: modelData.width * keyboard_container.width / columns - rowSpacing
|
||||
height: keyboard_container.height / rows - columnSpacing
|
||||
|
||||
onClicked: root.clicked(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
signal clicked(string text)
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onClicked(text) {
|
||||
if (!keyboard_controller.target)
|
||||
return
|
||||
if (text === strShift) {
|
||||
root.shift = !root.shift
|
||||
} else if (text === '123') {
|
||||
root.symbols = true
|
||||
} else if (text === 'ABC') {
|
||||
root.symbols = false
|
||||
} else if (text === strEnter) {
|
||||
if (keyboard_controller.target.accepted) {
|
||||
keyboard_controller.target.accepted()
|
||||
}
|
||||
} else if (text === strClose) {
|
||||
root.dismissed()
|
||||
} else {
|
||||
if (text === strBackspace) {
|
||||
var current = keyboard_controller.target.text
|
||||
keyboard_controller.target.text = current.slice(0, current.length - 1)
|
||||
} else {
|
||||
var charToInsert = root.symbols ? text : (root.shift ? text.toUpperCase
|
||||
() : text)
|
||||
var current = keyboard_controller.target.text
|
||||
var cursorPos = keyboard_controller.target.cursorPosition
|
||||
keyboard_controller.target.text = current.slice(0,
|
||||
cursorPos) + charToInsert + current.slice(cursorPos)
|
||||
keyboard_controller.target.cursorPosition = cursorPos + 1
|
||||
}
|
||||
|
||||
if (root.shift && text !== strShift)
|
||||
root.shift = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,19 +14,21 @@ Item {
|
||||
|
||||
function show() {
|
||||
if (!isKeyboardActive && keyboard === null) {
|
||||
keyboard = keyboardComponent.createObject(keyboard_controller.rootObject);
|
||||
keyboard.target = keyboard_controller.target;
|
||||
isKeyboardActive = true;
|
||||
keyboard = keyboardComponent.createObject(
|
||||
keyboard_controller.rootObject)
|
||||
keyboard.target = keyboard_controller.target
|
||||
keyboard.dismissed.connect(hide)
|
||||
isKeyboardActive = true
|
||||
} else
|
||||
console.log("The keyboard is already shown");
|
||||
console.log("The keyboard is already shown")
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (isKeyboardActive && keyboard !== null) {
|
||||
keyboard.destroy();
|
||||
isKeyboardActive = false;
|
||||
keyboard.destroy()
|
||||
isKeyboardActive = false
|
||||
} else
|
||||
console.log("The keyboard is already hidden");
|
||||
console.log("The keyboard is already hidden")
|
||||
}
|
||||
|
||||
// private
|
||||
|
||||
@@ -34,7 +34,6 @@ Rectangle {
|
||||
}
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
@@ -266,6 +265,7 @@ Rectangle {
|
||||
|
||||
Column {
|
||||
id: expandedContent
|
||||
objectName: "expandedContent"
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
@@ -321,7 +321,11 @@ Rectangle {
|
||||
spacing: 16
|
||||
|
||||
Repeater {
|
||||
model: notificationGroup?.notifications?.slice(0, 10) || []
|
||||
id: notificationRepeater
|
||||
objectName: "notificationRepeater"
|
||||
model: ScriptModel {
|
||||
values: notificationGroup?.notifications?.slice(0, 10) || []
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
@@ -346,7 +350,6 @@ Rectangle {
|
||||
border.color: isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
|
||||
border.width: isSelected ? 1 : 1
|
||||
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
|
||||
@@ -12,7 +12,7 @@ import qs.Modules.Notifications.Center
|
||||
DankPopout {
|
||||
id: root
|
||||
|
||||
layerNamespace: "dms:notification-center"
|
||||
layerNamespace: "dms:notification-center-popout"
|
||||
|
||||
property bool notificationHistoryVisible: false
|
||||
property var triggerScreen: null
|
||||
|
||||
@@ -28,7 +28,7 @@ QtObject {
|
||||
const nav = []
|
||||
const groups = NotificationService.groupedNotifications
|
||||
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
for (var i = 0; i < groups.length; i++) {
|
||||
const group = groups[i]
|
||||
const isExpanded = NotificationService.expandedGroups[group.key] || false
|
||||
|
||||
@@ -42,7 +42,8 @@ QtObject {
|
||||
|
||||
if (isExpanded) {
|
||||
const notifications = group.notifications || []
|
||||
for (let j = 0; j < notifications.length; j++) {
|
||||
const maxNotifications = Math.min(notifications.length, 10)
|
||||
for (var j = 0; j < maxNotifications; j++) {
|
||||
const notifId = String(notifications[j] && notifications[j].notification && notifications[j].notification.id ? notifications[j].notification.id : "")
|
||||
nav.push({
|
||||
"type": "notification",
|
||||
@@ -65,7 +66,7 @@ QtObject {
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < flatNavigation.length; i++) {
|
||||
for (var i = 0; i < flatNavigation.length; i++) {
|
||||
const item = flatNavigation[i]
|
||||
|
||||
if (selectedItemType === "group" && item.type === "group" && item.groupKey === selectedGroupKey) {
|
||||
@@ -81,7 +82,7 @@ QtObject {
|
||||
|
||||
// If not found, try to find the same group but select the group header instead
|
||||
if (selectedItemType === "notification") {
|
||||
for (let j = 0; j < flatNavigation.length; j++) {
|
||||
for (var j = 0; j < flatNavigation.length; j++) {
|
||||
const groupItem = flatNavigation[j]
|
||||
if (groupItem.type === "group" && groupItem.groupKey === selectedGroupKey) {
|
||||
selectedFlatIndex = j
|
||||
@@ -195,7 +196,7 @@ QtObject {
|
||||
// Smart selection after toggle
|
||||
if (!wasExpanded) {
|
||||
// Just expanded - move to first notification in the group
|
||||
for (let i = 0; i < flatNavigation.length; i++) {
|
||||
for (var i = 0; i < flatNavigation.length; i++) {
|
||||
if (flatNavigation[i].type === "notification" && flatNavigation[i].groupIndex === groupIndex) {
|
||||
selectedFlatIndex = i
|
||||
break
|
||||
@@ -203,7 +204,7 @@ QtObject {
|
||||
}
|
||||
} else {
|
||||
// Just collapsed - stay on the group header
|
||||
for (let i = 0; i < flatNavigation.length; i++) {
|
||||
for (var i = 0; i < flatNavigation.length; i++) {
|
||||
if (flatNavigation[i].type === "group" && flatNavigation[i].groupIndex === groupIndex) {
|
||||
selectedFlatIndex = i
|
||||
break
|
||||
@@ -319,6 +320,23 @@ QtObject {
|
||||
}
|
||||
}
|
||||
|
||||
function findRepeater(parent) {
|
||||
if (!parent || !parent.children) {
|
||||
return null
|
||||
}
|
||||
for (var i = 0; i < parent.children.length; i++) {
|
||||
const child = parent.children[i]
|
||||
if (child.objectName === "notificationRepeater") {
|
||||
return child
|
||||
}
|
||||
const found = findRepeater(child)
|
||||
if (found) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function ensureVisible() {
|
||||
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length || !listView)
|
||||
return
|
||||
@@ -326,17 +344,34 @@ QtObject {
|
||||
const currentItem = flatNavigation[selectedFlatIndex]
|
||||
|
||||
if (keyboardNavigationActive && currentItem && currentItem.groupIndex >= 0) {
|
||||
// Always center the selected item for better visibility
|
||||
// This ensures the selected item stays in view even when new notifications arrive
|
||||
if (currentItem.type === "notification") {
|
||||
// For individual notifications, center on the group but bias towards the notification
|
||||
listView.positionViewAtIndex(currentItem.groupIndex, ListView.Center)
|
||||
const groupDelegate = listView.itemAtIndex(currentItem.groupIndex)
|
||||
if (groupDelegate && groupDelegate.children && groupDelegate.children.length > 0) {
|
||||
const notificationCard = groupDelegate.children[0]
|
||||
const repeater = findRepeater(notificationCard)
|
||||
|
||||
if (repeater && currentItem.notificationIndex < repeater.count) {
|
||||
const notificationItem = repeater.itemAt(currentItem.notificationIndex)
|
||||
if (notificationItem) {
|
||||
const itemPos = notificationItem.mapToItem(listView.contentItem, 0, 0)
|
||||
const itemY = itemPos.y
|
||||
const itemHeight = notificationItem.height
|
||||
|
||||
const viewportTop = listView.contentY
|
||||
const viewportBottom = listView.contentY + listView.height
|
||||
|
||||
if (itemY < viewportTop) {
|
||||
listView.contentY = itemY - 20
|
||||
} else if (itemY + itemHeight > viewportBottom) {
|
||||
listView.contentY = itemY + itemHeight - listView.height + 20
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For group headers, center on the group
|
||||
listView.positionViewAtIndex(currentItem.groupIndex, ListView.Center)
|
||||
listView.positionViewAtIndex(currentItem.groupIndex, ListView.Contain)
|
||||
}
|
||||
|
||||
// Force immediate update
|
||||
listView.forceLayout()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Shapes
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
@@ -63,6 +64,20 @@ PanelWindow {
|
||||
|
||||
visible: hasValidData
|
||||
WlrLayershell.layer: {
|
||||
const envLayer = Quickshell.env("DMS_NOTIFICATION_LAYER")
|
||||
if (envLayer) {
|
||||
switch (envLayer) {
|
||||
case "bottom":
|
||||
return WlrLayershell.Bottom
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay
|
||||
case "background":
|
||||
return WlrLayershell.Background
|
||||
case "top":
|
||||
return WlrLayershell.Top
|
||||
}
|
||||
}
|
||||
|
||||
if (!notificationData)
|
||||
return WlrLayershell.Top
|
||||
|
||||
@@ -198,65 +213,81 @@ PanelWindow {
|
||||
return Theme.popupDistance
|
||||
}
|
||||
|
||||
readonly property real dpr: CompositorService.getScreenScale(win.screen)
|
||||
readonly property real alignedWidth: Theme.px(implicitWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(implicitHeight, dpr)
|
||||
|
||||
Item {
|
||||
id: content
|
||||
|
||||
anchors.fill: parent
|
||||
x: Theme.snap((win.width - alignedWidth) / 2, dpr)
|
||||
y: Theme.snap((win.height - alignedHeight) / 2, dpr)
|
||||
width: alignedWidth
|
||||
height: alignedHeight
|
||||
visible: win.hasValidData
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
|
||||
Rectangle {
|
||||
property var shadowLayers: [shadowLayer1, shadowLayer2, shadowLayer3]
|
||||
property real shadowBlurPx: 10
|
||||
property real shadowSpreadPx: 0
|
||||
property real shadowBaseAlpha: 0.60
|
||||
readonly property real popupSurfaceAlpha: SettingsData.popupTransparency
|
||||
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha))
|
||||
|
||||
Item {
|
||||
id: bgShadowLayer
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
border.color: notificationData && notificationData.urgency === NotificationUrgency.Critical ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: notificationData && notificationData.urgency === NotificationUrgency.Critical ? 2 : 0
|
||||
clip: true
|
||||
anchors.margins: Theme.snap(4, win.dpr)
|
||||
layer.enabled: true
|
||||
layer.smooth: false
|
||||
layer.textureSize: Qt.size(Math.round(width * win.dpr), Math.round(height * win.dpr))
|
||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||
|
||||
Rectangle {
|
||||
id: shadowLayer1
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: -3
|
||||
color: "transparent"
|
||||
radius: parent.radius + 3
|
||||
border.color: Qt.rgba(0, 0, 0, 0.05)
|
||||
border.width: 1
|
||||
z: -3
|
||||
layer.effect: MultiEffect {
|
||||
id: shadowFx
|
||||
autoPaddingEnabled: true
|
||||
shadowEnabled: true
|
||||
blurEnabled: false
|
||||
maskEnabled: false
|
||||
property int blurMax: 64
|
||||
shadowBlur: Math.max(0, Math.min(1, content.shadowBlurPx / blurMax))
|
||||
shadowScale: 1 + (2 * content.shadowSpreadPx) / Math.max(1, Math.min(bgShadowLayer.width, bgShadowLayer.height))
|
||||
shadowColor: Qt.rgba(0, 0, 0, content.effectiveShadowAlpha)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: shadowLayer2
|
||||
|
||||
Shape {
|
||||
id: backgroundShape
|
||||
anchors.fill: parent
|
||||
anchors.margins: -2
|
||||
color: "transparent"
|
||||
radius: parent.radius + 2
|
||||
border.color: Qt.rgba(0, 0, 0, 0.08)
|
||||
border.width: 1
|
||||
z: -2
|
||||
}
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
Rectangle {
|
||||
id: shadowLayer3
|
||||
readonly property real radius: Theme.cornerRadius
|
||||
readonly property color fillColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
readonly property color strokeColor: notificationData && notificationData.urgency === NotificationUrgency.Critical ? Theme.withAlpha(Theme.primary, 0.3) : Theme.withAlpha(Theme.outline, 0.08)
|
||||
readonly property real strokeWidth: notificationData && notificationData.urgency === NotificationUrgency.Critical ? 2 : 0
|
||||
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: 1
|
||||
radius: parent.radius
|
||||
z: -1
|
||||
ShapePath {
|
||||
fillColor: backgroundShape.fillColor
|
||||
strokeColor: backgroundShape.strokeColor
|
||||
strokeWidth: backgroundShape.strokeWidth
|
||||
|
||||
startX: backgroundShape.radius
|
||||
startY: 0
|
||||
|
||||
PathLine { x: backgroundShape.width - backgroundShape.radius; y: 0 }
|
||||
PathQuad { x: backgroundShape.width; y: backgroundShape.radius; controlX: backgroundShape.width; controlY: 0 }
|
||||
PathLine { x: backgroundShape.width; y: backgroundShape.height - backgroundShape.radius }
|
||||
PathQuad { x: backgroundShape.width - backgroundShape.radius; y: backgroundShape.height; controlX: backgroundShape.width; controlY: backgroundShape.height }
|
||||
PathLine { x: backgroundShape.radius; y: backgroundShape.height }
|
||||
PathQuad { x: 0; y: backgroundShape.height - backgroundShape.radius; controlX: 0; controlY: backgroundShape.height }
|
||||
PathLine { x: 0; y: backgroundShape.radius }
|
||||
PathQuad { x: backgroundShape.radius; y: 0; controlX: 0; controlY: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
radius: backgroundShape.radius
|
||||
visible: notificationData && notificationData.urgency === NotificationUrgency.Critical
|
||||
opacity: 1
|
||||
clip: true
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
@@ -277,6 +308,13 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: backgroundContainer
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.snap(4, win.dpr)
|
||||
clip: true
|
||||
|
||||
Item {
|
||||
id: notificationContent
|
||||
|
||||
103
Modules/Plugins/ColorSetting.qml
Normal file
103
Modules/Plugins/ColorSetting.qml
Normal file
@@ -0,0 +1,103 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
required property string settingKey
|
||||
required property string label
|
||||
property string description: ""
|
||||
property color defaultValue: Theme.primary
|
||||
property color value: defaultValue
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
property bool isInitialized: false
|
||||
|
||||
function loadValue() {
|
||||
const settings = findSettings()
|
||||
if (settings && settings.pluginService) {
|
||||
const loadedValue = settings.loadValue(settingKey, defaultValue)
|
||||
value = loadedValue
|
||||
isInitialized = true
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(loadValue)
|
||||
}
|
||||
|
||||
onValueChanged: {
|
||||
if (!isInitialized) return
|
||||
const settings = findSettings()
|
||||
if (settings) {
|
||||
settings.saveValue(settingKey, value)
|
||||
}
|
||||
}
|
||||
|
||||
function findSettings() {
|
||||
let item = parent
|
||||
while (item) {
|
||||
if (item.saveValue !== undefined && item.loadValue !== undefined) {
|
||||
return item
|
||||
}
|
||||
item = item.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.label
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.description
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
visible: root.description !== ""
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
width: 100
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: root.value
|
||||
border.color: Theme.outlineStrong
|
||||
border.width: 2
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (PopoutService && PopoutService.colorPickerModal) {
|
||||
PopoutService.colorPickerModal.selectedColor = root.value
|
||||
PopoutService.colorPickerModal.pickerTitle = root.label
|
||||
PopoutService.colorPickerModal.onColorSelectedCallback = function(selectedColor) {
|
||||
root.value = selectedColor
|
||||
}
|
||||
PopoutService.colorPickerModal.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.value.toString()
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ Item {
|
||||
property real popoutWidth: 400
|
||||
property real popoutHeight: 0
|
||||
property var pillClickAction: null
|
||||
property var pillRightClickAction: null
|
||||
|
||||
property Component controlCenterWidget: null
|
||||
property string ccWidgetIcon: ""
|
||||
@@ -120,6 +121,18 @@ Item {
|
||||
pluginPopout.toggle()
|
||||
}
|
||||
}
|
||||
onRightClicked: {
|
||||
if (pillRightClickAction) {
|
||||
if (pillRightClickAction.length === 0) {
|
||||
pillRightClickAction()
|
||||
} else {
|
||||
const globalPos = mapToGlobal(0, 0)
|
||||
const currentScreen = parentScreen || Screen
|
||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, width)
|
||||
pillRightClickAction(pos.x, pos.y, pos.width, section, currentScreen)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasePill {
|
||||
@@ -147,6 +160,18 @@ Item {
|
||||
pluginPopout.toggle()
|
||||
}
|
||||
}
|
||||
onRightClicked: {
|
||||
if (pillRightClickAction) {
|
||||
if (pillRightClickAction.length === 0) {
|
||||
pillRightClickAction()
|
||||
} else {
|
||||
const globalPos = mapToGlobal(0, 0)
|
||||
const currentScreen = parentScreen || Screen
|
||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, width)
|
||||
pillRightClickAction(pos.x, pos.y, pos.width, section, currentScreen)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closePopout() {
|
||||
|
||||
@@ -27,6 +27,10 @@ Column {
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(loadValue)
|
||||
}
|
||||
|
||||
onValueChanged: {
|
||||
if (!isInitialized) return
|
||||
const settings = findSettings()
|
||||
|
||||
@@ -26,7 +26,7 @@ Row {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
loadValue()
|
||||
Qt.callLater(loadValue)
|
||||
}
|
||||
|
||||
onValueChanged: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -248,7 +249,10 @@ Column {
|
||||
height: parent.height - columnHeaders.height
|
||||
clip: true
|
||||
spacing: 4
|
||||
model: DgopService.processes
|
||||
model: ScriptModel {
|
||||
values: DgopService.processes
|
||||
objectProp: "pid"
|
||||
}
|
||||
|
||||
delegate: ProcessListItem {
|
||||
process: modelData
|
||||
|
||||
@@ -13,11 +13,13 @@ Item {
|
||||
property bool isNiri: CompositorService.isNiri
|
||||
property bool isSway: CompositorService.isSway
|
||||
property bool isDwl: CompositorService.isDwl
|
||||
property bool isLabwc: CompositorService.isLabwc
|
||||
|
||||
property string compositorName: {
|
||||
if (isHyprland) return "hyprland"
|
||||
if (isSway) return "sway"
|
||||
if (isDwl) return "mangowc"
|
||||
if (isLabwc) return "labwc"
|
||||
return "niri"
|
||||
}
|
||||
|
||||
@@ -25,6 +27,7 @@ Item {
|
||||
if (isHyprland) return "/assets/hyprland.svg"
|
||||
if (isSway) return "/assets/sway.svg"
|
||||
if (isDwl) return "/assets/mango.png"
|
||||
if (isLabwc) return "/assets/labwc.png"
|
||||
return "/assets/niri.svg"
|
||||
}
|
||||
|
||||
@@ -32,6 +35,7 @@ Item {
|
||||
if (isHyprland) return "https://hypr.land"
|
||||
if (isSway) return "https://swaywm.org"
|
||||
if (isDwl) return "https://github.com/DreamMaoMao/mangowc"
|
||||
if (isLabwc) return "https://labwc.github.io/"
|
||||
return "https://github.com/YaLTeR/niri"
|
||||
}
|
||||
|
||||
@@ -39,10 +43,11 @@ Item {
|
||||
if (isHyprland) return "Hyprland Website"
|
||||
if (isSway) return "Sway Website"
|
||||
if (isDwl) return "mangowc GitHub"
|
||||
if (isLabwc) return "LabWC Website"
|
||||
return "niri GitHub"
|
||||
}
|
||||
|
||||
property string dmsDiscordUrl: "https://discord.gg/vT8Sfjy7sx"
|
||||
property string dmsDiscordUrl: "https://discord.gg/ppWTpKmPgT"
|
||||
property string dmsDiscordTooltip: "niri/dms Discord"
|
||||
|
||||
property string compositorDiscordUrl: {
|
||||
@@ -60,9 +65,13 @@ Item {
|
||||
property string redditUrl: "https://reddit.com/r/niri"
|
||||
property string redditTooltip: "r/niri Subreddit"
|
||||
|
||||
property bool showMatrix: isNiri && !isHyprland && !isSway && !isDwl
|
||||
property string ircUrl: "https://web.libera.chat/gamja/?channels=#labwc"
|
||||
property string ircTooltip: "LabWC IRC Channel"
|
||||
|
||||
property bool showMatrix: isNiri && !isHyprland && !isSway && !isDwl && !isLabwc
|
||||
property bool showCompositorDiscord: isHyprland || isDwl
|
||||
property bool showReddit: isNiri && !isHyprland && !isSway && !isDwl
|
||||
property bool showReddit: isNiri && !isHyprland && !isSway && !isDwl && !isLabwc
|
||||
property bool showIrc: isLabwc
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
@@ -153,6 +162,9 @@ Item {
|
||||
if (showMatrix) {
|
||||
baseWidth += matrixButton.width + 4
|
||||
}
|
||||
if (showIrc) {
|
||||
baseWidth += ircButton.width + Theme.spacingM
|
||||
}
|
||||
if (showCompositorDiscord) {
|
||||
baseWidth += compositorDiscordButton.width + Theme.spacingM
|
||||
}
|
||||
@@ -232,11 +244,43 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: ircButton
|
||||
width: 24
|
||||
height: 24
|
||||
x: compositorButton.x + compositorButton.width + Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: showIrc
|
||||
|
||||
property bool hovered: false
|
||||
property string tooltipText: ircTooltip
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "forum"
|
||||
size: 20
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
onEntered: parent.hovered = true
|
||||
onExited: parent.hovered = false
|
||||
onClicked: Qt.openUrlExternally(ircUrl)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: dmsDiscordButton
|
||||
width: 20
|
||||
height: 20
|
||||
x: showMatrix ? matrixButton.x + matrixButton.width + Theme.spacingM : compositorButton.x + compositorButton.width + Theme.spacingM
|
||||
x: {
|
||||
if (showMatrix) return matrixButton.x + matrixButton.width + Theme.spacingM
|
||||
if (showIrc) return ircButton.x + ircButton.width + Theme.spacingM
|
||||
return compositorButton.x + compositorButton.width + Theme.spacingM
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
property bool hovered: false
|
||||
@@ -618,6 +662,7 @@ Item {
|
||||
property var hoveredButton: {
|
||||
if (compositorButton.hovered) return compositorButton
|
||||
if (matrixButton.visible && matrixButton.hovered) return matrixButton
|
||||
if (ircButton.visible && ircButton.hovered) return ircButton
|
||||
if (dmsDiscordButton.hovered) return dmsDiscordButton
|
||||
if (compositorDiscordButton.visible && compositorDiscordButton.hovered) return compositorDiscordButton
|
||||
if (redditButton.visible && redditButton.hovered) return redditButton
|
||||
|
||||
@@ -1331,15 +1331,103 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
Column {
|
||||
width: parent.width
|
||||
text: I18n.tr("Goth Corners")
|
||||
description: "Add curved swooping tips at the bottom of the bar."
|
||||
checked: SettingsData.dankBarGothCornersEnabled
|
||||
onToggled: checked => {
|
||||
SettingsData.set("dankBarGothCornersEnabled",
|
||||
checked)
|
||||
}
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Goth Corners")
|
||||
description: "Add curved swooping tips at the bottom of the bar."
|
||||
checked: SettingsData.dankBarGothCornersEnabled
|
||||
onToggled: checked => {
|
||||
SettingsData.set("dankBarGothCornersEnabled",
|
||||
checked)
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Corner Radius Override")
|
||||
description: "Customize the goth corner radius independently."
|
||||
checked: SettingsData.dankBarGothCornerRadiusOverride
|
||||
visible: SettingsData.dankBarGothCornersEnabled
|
||||
onToggled: checked => {
|
||||
SettingsData.set("dankBarGothCornerRadiusOverride", checked)
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
visible: SettingsData.dankBarGothCornersEnabled && SettingsData.dankBarGothCornerRadiusOverride
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Goth Corner Radius")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - gothCornerRadiusText.implicitWidth - resetGothCornerRadiusBtn.width - Theme.spacingS - Theme.spacingM
|
||||
height: 1
|
||||
|
||||
StyledText {
|
||||
id: gothCornerRadiusText
|
||||
visible: false
|
||||
text: I18n.tr("Goth Corner Radius")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: resetGothCornerRadiusBtn
|
||||
buttonSize: 20
|
||||
iconName: "refresh"
|
||||
iconSize: 12
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
iconColor: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: {
|
||||
SettingsData.set("dankBarGothCornerRadiusValue", 12)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: Theme.spacingS
|
||||
height: 1
|
||||
}
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
id: gothCornerRadiusSlider
|
||||
width: parent.width
|
||||
height: 24
|
||||
value: SettingsData.dankBarGothCornerRadiusValue
|
||||
minimum: 0
|
||||
maximum: 64
|
||||
unit: ""
|
||||
showValue: true
|
||||
wheelEnabled: false
|
||||
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
onSliderValueChanged: newValue => {
|
||||
SettingsData.set("dankBarGothCornerRadiusValue", newValue)
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: gothCornerRadiusSlider
|
||||
property: "value"
|
||||
value: SettingsData.dankBarGothCornerRadiusValue
|
||||
restoreMode: Binding.RestoreBinding
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
|
||||
@@ -111,6 +111,60 @@ Item {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "dock_to_bottom"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||
- enableToggle.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Show Dock")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Display a dock with pinned and running applications that can be positioned at the top, bottom, left, or right edge of the screen")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: enableToggle
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.showDock
|
||||
onToggled: checked => {
|
||||
SettingsData.setShowDock(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.2
|
||||
visible: SettingsData.showDock
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: SettingsData.showDock
|
||||
opacity: visible ? 1 : 0
|
||||
|
||||
DankIcon {
|
||||
name: "visibility_off"
|
||||
size: Theme.iconSize
|
||||
@@ -149,56 +203,12 @@ Item {
|
||||
SettingsData.set("dockAutoHide", checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.2
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "dock_to_bottom"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||
- enableToggle.width - Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Show Dock")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Display a dock with pinned and running applications that can be positioned at the top, bottom, left, or right edge of the screen")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: enableToggle
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.showDock
|
||||
onToggled: checked => {
|
||||
SettingsData.setShowDock(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,8 +276,6 @@ Item {
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
visible: SettingsData.showDock
|
||||
opacity: visible ? 1 : 0
|
||||
|
||||
Column {
|
||||
id: groupByAppSection
|
||||
@@ -320,13 +328,6 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Indicator Style Section
|
||||
@@ -338,8 +339,6 @@ Item {
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
visible: SettingsData.showDock
|
||||
opacity: visible ? 1 : 0
|
||||
|
||||
Column {
|
||||
id: indicatorStyleSection
|
||||
@@ -386,13 +385,6 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Icon Size Section
|
||||
@@ -404,8 +396,6 @@ Item {
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
visible: SettingsData.showDock
|
||||
opacity: visible ? 1 : 0
|
||||
|
||||
Column {
|
||||
id: iconSizeSection
|
||||
@@ -449,13 +439,6 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dock Spacing Section
|
||||
@@ -467,8 +450,6 @@ Item {
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
visible: SettingsData.showDock
|
||||
opacity: visible ? 1 : 0
|
||||
|
||||
Column {
|
||||
id: dockSpacingSection
|
||||
@@ -547,17 +528,37 @@ Item {
|
||||
wheelEnabled: false
|
||||
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
onSliderValueChanged: newValue => {
|
||||
SettingsData.set("dockBottomGap",
|
||||
SettingsData.set("dockBottomGap",
|
||||
newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Margin")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
width: parent.width
|
||||
height: 24
|
||||
value: SettingsData.dockMargin
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
unit: ""
|
||||
showValue: true
|
||||
wheelEnabled: false
|
||||
thumbOutlineColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
onSliderValueChanged: newValue => {
|
||||
SettingsData.set("dockMargin", newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -571,8 +572,6 @@ Item {
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
visible: SettingsData.showDock
|
||||
opacity: visible ? 1 : 0
|
||||
|
||||
Column {
|
||||
id: transparencySection
|
||||
@@ -617,13 +616,6 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
@@ -109,6 +110,11 @@ DankModal {
|
||||
parentModal.shouldHaveFocus = Qt.binding(() => {
|
||||
return parentModal.shouldBeVisible
|
||||
})
|
||||
Qt.callLater(() => {
|
||||
if (parentModal.modalFocusScope) {
|
||||
parentModal.modalFocusScope.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +122,17 @@ DankModal {
|
||||
refreshPlugins()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: contentLoader
|
||||
function onLoaded() {
|
||||
Qt.callLater(() => {
|
||||
if (contentLoader.item && contentLoader.item.searchField) {
|
||||
contentLoader.item.searchField.forceActiveFocus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onDialogClosed: () => {
|
||||
allPlugins = []
|
||||
searchQuery = ""
|
||||
@@ -137,6 +154,10 @@ DankModal {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Component.onCompleted: {
|
||||
browserSearchField.forceActiveFocus()
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.close()
|
||||
@@ -312,7 +333,9 @@ DankModal {
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
model: root.filteredPlugins
|
||||
model: ScriptModel {
|
||||
values: root.filteredPlugins
|
||||
}
|
||||
clip: true
|
||||
visible: !root.isLoading
|
||||
|
||||
|
||||
@@ -111,40 +111,6 @@ Item {
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - parent.children[0].width - parent.children[1].width - surfaceBaseGroup.width - Theme.spacingM * 3
|
||||
height: 1
|
||||
}
|
||||
|
||||
DankButtonGroup {
|
||||
id: surfaceBaseGroup
|
||||
property int currentSurfaceIndex: {
|
||||
switch (SettingsData.surfaceBase) {
|
||||
case "sc": return 0
|
||||
case "s": return 1
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
|
||||
model: ["Container", "Surface"]
|
||||
currentIndex: currentSurfaceIndex
|
||||
selectionMode: "single"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
buttonHeight: 20
|
||||
minButtonWidth: 48
|
||||
buttonPadding: Theme.spacingS
|
||||
checkIconSize: Theme.iconSizeSmall - 2
|
||||
textSize: Theme.fontSizeSmall - 2
|
||||
spacing: 1
|
||||
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected) return
|
||||
const surfaceOptions = ["sc", "s"]
|
||||
SettingsData.setSurfaceBase(surfaceOptions[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
|
||||
@@ -556,7 +556,7 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.useFahrenheit
|
||||
onToggled: checked => {
|
||||
return SettingsData.set("temperatureUnit",
|
||||
return SettingsData.set("useFahrenheit",
|
||||
checked)
|
||||
}
|
||||
}
|
||||
@@ -628,7 +628,7 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: SettingsData.useAutoLocation
|
||||
onToggled: checked => {
|
||||
return SettingsData.set("autoLocation",
|
||||
return SettingsData.set("useAutoLocation",
|
||||
checked)
|
||||
}
|
||||
}
|
||||
@@ -966,7 +966,7 @@ Item {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (WeatherService.weather.available) {
|
||||
SettingsData.set("temperatureUnit", !SettingsData.useFahrenheit)
|
||||
SettingsData.set("useFahrenheit", !SettingsData.useFahrenheit)
|
||||
}
|
||||
}
|
||||
enabled: WeatherService.weather.available
|
||||
|
||||
@@ -81,8 +81,9 @@ Item {
|
||||
text: I18n.tr("Show Workspace Apps")
|
||||
description: I18n.tr("Display application icons in workspace indicators")
|
||||
checked: SettingsData.showWorkspaceApps
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland
|
||||
onToggled: checked => {
|
||||
return SettingsData.set("showWorkspaceApps",
|
||||
return SettingsData.set("showWorkspaceApps",
|
||||
checked)
|
||||
}
|
||||
}
|
||||
@@ -247,7 +248,7 @@ Item {
|
||||
SettingsData.set("updaterCustomCommand", "");
|
||||
SettingsData.set("updaterTerminalAdditionalParams", "");
|
||||
}
|
||||
return SettingsData.set("updaterUseCustomCommandEnabled", checked);
|
||||
return SettingsData.set("updaterUseCustomCommand", checked);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,8 +274,8 @@ Item {
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "myPkgMngr --sysupdate"
|
||||
backgroundColor: Theme.surfaceVariant
|
||||
normalBorderColor: Theme.primarySelected
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
@@ -320,8 +321,8 @@ Item {
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "-T udpClass"
|
||||
backgroundColor: Theme.surfaceVariant
|
||||
normalBorderColor: Theme.primarySelected
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
@@ -355,6 +356,7 @@ Item {
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
||||
Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland
|
||||
|
||||
Column {
|
||||
id: runningAppsSection
|
||||
|
||||
@@ -11,6 +11,8 @@ import qs.Widgets
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
WlrLayershell.namespace: "dms:toast"
|
||||
|
||||
property var modelData
|
||||
property bool shouldBeVisible: false
|
||||
property real frozenWidth: 0
|
||||
|
||||
32
PLUGINS/ColorDemoPlugin/ColorDemoSettings.qml
Normal file
32
PLUGINS/ColorDemoPlugin/ColorDemoSettings.qml
Normal file
@@ -0,0 +1,32 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modules.Plugins
|
||||
import qs.Widgets
|
||||
|
||||
PluginSettings {
|
||||
id: root
|
||||
pluginId: "colorDemo"
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: "Color Demo Settings"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: "Choose a custom color to display in the bar widget"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
ColorSetting {
|
||||
settingKey: "customColor"
|
||||
label: "Custom Color"
|
||||
description: "Choose a custom color to display in the widget"
|
||||
defaultValue: Theme.primary
|
||||
}
|
||||
}
|
||||
57
PLUGINS/ColorDemoPlugin/ColorDemoWidget.qml
Normal file
57
PLUGINS/ColorDemoPlugin/ColorDemoWidget.qml
Normal file
@@ -0,0 +1,57 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginComponent {
|
||||
id: root
|
||||
|
||||
property color customColor: pluginData.customColor || Theme.primary
|
||||
|
||||
horizontalBarPill: Component {
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 4
|
||||
color: root.customColor
|
||||
border.color: Theme.outlineStrong
|
||||
border.width: 1
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.customColor.toString()
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
verticalBarPill: Component {
|
||||
Column {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 4
|
||||
color: root.customColor
|
||||
border.color: Theme.outlineStrong
|
||||
border.width: 1
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.customColor.toString()
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
PLUGINS/ColorDemoPlugin/plugin.json
Normal file
12
PLUGINS/ColorDemoPlugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"id": "colorDemo",
|
||||
"name": "Color Demo",
|
||||
"description": "Demonstrates color picker plugin setting",
|
||||
"version": "1.0.0",
|
||||
"author": "DMS",
|
||||
"icon": "palette",
|
||||
"type": "widget",
|
||||
"component": "./ColorDemoWidget.qml",
|
||||
"settings": "./ColorDemoSettings.qml",
|
||||
"permissions": ["settings_read", "settings_write"]
|
||||
}
|
||||
@@ -177,6 +177,7 @@ PluginComponent {
|
||||
- `popoutWidth`: Popout window width
|
||||
- `popoutHeight`: Popout window height
|
||||
- `pillClickAction`: Custom click handler function (overrides popout)
|
||||
- `pillRightClickAction`: Custom right click handler function
|
||||
|
||||
### Control Center Integration
|
||||
|
||||
@@ -227,7 +228,7 @@ PluginComponent {
|
||||
|
||||
**Custom Click Actions:**
|
||||
|
||||
Override default popout with `pillClickAction`:
|
||||
Override default popout with `pillClickAction` and `pillRightClickAction`:
|
||||
|
||||
```qml
|
||||
pillClickAction: () => {
|
||||
@@ -238,6 +239,14 @@ pillClickAction: () => {
|
||||
pillClickAction: (x, y, width, section, screen) => {
|
||||
popoutService?.toggleControlCenter(x, y, width, section, screen)
|
||||
}
|
||||
|
||||
pillRightClickAction: () => {
|
||||
Process.exec("bash", ["-c", "notify-send 'Right clicked!'"])
|
||||
}
|
||||
|
||||
pillRightClickAction: (x, y, width, section, screen) => {
|
||||
popoutService?.toggleControlCenter(x, y, width, section, screen)
|
||||
}
|
||||
```
|
||||
|
||||
The PluginComponent automatically handles:
|
||||
|
||||
47
README.md
47
README.md
@@ -62,7 +62,7 @@ https://github.com/user-attachments/assets/1200a739-7770-4601-8b85-695ca527819a
|
||||
curl -fsSL https://install.danklinux.com | sh
|
||||
```
|
||||
|
||||
That's it. One command installs dms and all dependencies on Arch, Fedora, Debian, Ubuntu, or openSUSE.
|
||||
That's it. One command installs dms and all dependencies on Arch, Fedora, Debian, Ubuntu, openSUSE, or Gentoo.
|
||||
|
||||
**[Manual Installation Guide →](https://danklinux.com/docs/dankmaterialshell/installation)**
|
||||
|
||||
@@ -100,7 +100,7 @@ Endless customization with the [plugin registry](https://plugins.danklinux.com).
|
||||
|
||||
## Supported Compositors
|
||||
|
||||
DankMaterialShell works best with **[niri](https://github.com/YaLTeR/niri)**, **[Hyprland](https://hyprland.org/)**, **[sway](https://swaywm.org/)**, and **[dwl/MangoWC](https://github.com/DreamMaoMao/mangowc)** - with full workspace switching, overview integration, and monitor management.
|
||||
DankMaterialShell works best with **[niri](https://github.com/YaLTeR/niri)**, **[Hyprland](https://hyprland.org/)**, **[sway](https://swaywm.org/)**, and **[dwl/MangoWC](https://github.com/DreamMaoMao/mangowc)**. - with full workspace switching, overview integration, and monitor management.
|
||||
|
||||
Other Wayland compositors work too, just with a reduced feature set.
|
||||
|
||||
@@ -159,14 +159,53 @@ Contributions welcome! Bug fixes, new widgets, theme improvements, or docs - it
|
||||
|
||||
**Contributing Code:**
|
||||
1. Fork the repository
|
||||
2. Make your changes
|
||||
3. Open a pull request
|
||||
2. Set up the development environment
|
||||
3. Make your changes
|
||||
4. Open a pull request
|
||||
|
||||
**Contributing Documentation:**
|
||||
1. Fork the [DankLinux-Docs](https://github.com/AvengeMedia/DankLinux-Docs) repository
|
||||
2. Update files in the `docs/` folder
|
||||
3. Open a pull request
|
||||
|
||||
### Development Setup
|
||||
|
||||
**Requirements:**
|
||||
- `python3` - Translation management
|
||||
|
||||
**Git Hooks:**
|
||||
|
||||
Enable the pre-commit hook to check translation sync status:
|
||||
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
**Translation Workflow**
|
||||
|
||||
Set POEditor credentials:
|
||||
|
||||
```bash
|
||||
export POEDITOR_API_TOKEN="your_api_token"
|
||||
export POEDITOR_PROJECT_ID="your_project_id"
|
||||
```
|
||||
|
||||
Sync translations before committing:
|
||||
|
||||
```bash
|
||||
python3 scripts/i18nsync.py sync
|
||||
```
|
||||
|
||||
This script:
|
||||
- Extracts strings from QML files
|
||||
- Uploads changed English terms to POEditor
|
||||
- Downloads updated translations from POEditor
|
||||
- Stages all changes for commit
|
||||
|
||||
The pre-commit hook will block commits if translations are out of sync and remind you to run the sync script.
|
||||
|
||||
Without POEditor credentials, the hook is skipped and commits proceed normally.
|
||||
|
||||
Check the [issues](https://github.com/AvengeMedia/DankMaterialShell/issues) or join the community.
|
||||
|
||||
---
|
||||
|
||||
@@ -43,11 +43,11 @@ Singleton {
|
||||
readonly property real batteryLevel: {
|
||||
if (!batteryAvailable) return 0
|
||||
if (batteryCapacity === 0) {
|
||||
if (usePreferred && device && device.ready) return Math.round(device.percentage)
|
||||
if (usePreferred && device && device.ready) return Math.round(device.percentage * 100)
|
||||
const validBatteries = batteries.filter(b => b.ready && b.percentage >= 0)
|
||||
if (validBatteries.length === 0) return 0
|
||||
const avgPercentage = validBatteries.reduce((sum, b) => sum + b.percentage, 0) / validBatteries.length
|
||||
return Math.round(avgPercentage)
|
||||
return Math.round(avgPercentage * 100)
|
||||
}
|
||||
return Math.round((batteryEnergy * 100) / batteryCapacity)
|
||||
}
|
||||
@@ -134,7 +134,7 @@ Singleton {
|
||||
if (dev && dev.ready && bluetoothTypes.includes(dev.type)) {
|
||||
btDevices.push({
|
||||
"name": dev.model || UPowerDeviceType.toString(dev.type),
|
||||
"percentage": Math.round(dev.percentage),
|
||||
"percentage": Math.round(dev.percentage * 100),
|
||||
"type": dev.type
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,28 +14,34 @@ Singleton {
|
||||
property bool isNiri: false
|
||||
property bool isDwl: false
|
||||
property bool isSway: false
|
||||
property bool isLabwc: false
|
||||
property string compositor: "unknown"
|
||||
|
||||
readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE")
|
||||
readonly property string niriSocket: Quickshell.env("NIRI_SOCKET")
|
||||
readonly property string swaySocket: Quickshell.env("SWAYSOCK")
|
||||
readonly property string labwcPid: Quickshell.env("LABWC_PID")
|
||||
property bool useNiriSorting: isNiri && NiriService
|
||||
|
||||
property var sortedToplevels: sortedToplevelsCache
|
||||
property var sortedToplevelsCache: []
|
||||
|
||||
property var sortedToplevels: []
|
||||
property bool _sortScheduled: false
|
||||
property bool _refreshScheduled: false
|
||||
property bool _hasRefreshedOnce: false
|
||||
|
||||
property var _coordCache: ({})
|
||||
property int _refreshCount: 0
|
||||
property real _refreshWindowStart: 0
|
||||
readonly property int _maxRefreshesPerSecond: 3
|
||||
signal toplevelsChanged()
|
||||
|
||||
function getScreenScale(screen) {
|
||||
if (!screen) return 1
|
||||
|
||||
if (Quickshell.env("QT_WAYLAND_FORCE_DPI") || Quickshell.env("QT_SCALE_FACTOR")) {
|
||||
return screen.devicePixelRatio || 1
|
||||
}
|
||||
|
||||
if (WlrOutputService.wlrOutputAvailable && screen) {
|
||||
const wlrOutput = WlrOutputService.getOutput(screen.name)
|
||||
if (wlrOutput?.enabled && wlrOutput.scale !== undefined && wlrOutput.scale > 0) {
|
||||
return wlrOutput.scale
|
||||
}
|
||||
}
|
||||
|
||||
if (isNiri && screen) {
|
||||
const niriScale = NiriService.displayScales[screen.name]
|
||||
if (niriScale !== undefined) return niriScale
|
||||
@@ -55,46 +61,20 @@ Singleton {
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: refreshTimer
|
||||
interval: 40
|
||||
id: sortDebounceTimer
|
||||
interval: 100
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
try {
|
||||
Hyprland.refreshToplevels()
|
||||
} catch(e) {}
|
||||
_refreshScheduled = false
|
||||
_hasRefreshedOnce = true
|
||||
scheduleSort()
|
||||
_sortScheduled = false
|
||||
sortedToplevels = computeSortedToplevels()
|
||||
toplevelsChanged()
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleSort() {
|
||||
if (_sortScheduled) return
|
||||
_sortScheduled = true
|
||||
Qt.callLater(function() {
|
||||
_sortScheduled = false
|
||||
sortedToplevelsCache = computeSortedToplevels()
|
||||
})
|
||||
}
|
||||
|
||||
function scheduleRefresh() {
|
||||
if (!isHyprland) return
|
||||
if (_refreshScheduled) return
|
||||
|
||||
const now = Date.now()
|
||||
if (now - _refreshWindowStart > 1000) {
|
||||
_refreshCount = 0
|
||||
_refreshWindowStart = now
|
||||
}
|
||||
|
||||
if (_refreshCount >= _maxRefreshesPerSecond) {
|
||||
console.warn("CompositorService: Refresh rate limit exceeded, skipping refresh")
|
||||
return
|
||||
}
|
||||
|
||||
_refreshCount++
|
||||
_refreshScheduled = true
|
||||
refreshTimer.restart()
|
||||
sortDebounceTimer.restart()
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -102,20 +82,31 @@ Singleton {
|
||||
function onValuesChanged() { root.scheduleSort() }
|
||||
}
|
||||
Connections {
|
||||
target: Hyprland.toplevels
|
||||
function onValuesChanged() {
|
||||
root._hasRefreshedOnce = false
|
||||
root.scheduleSort()
|
||||
target: isHyprland ? Hyprland : null
|
||||
enabled: isHyprland
|
||||
|
||||
function onRawEvent(event) {
|
||||
if (event.name === "openwindow" ||
|
||||
event.name === "closewindow" ||
|
||||
event.name === "movewindow" ||
|
||||
event.name === "movewindowv2" ||
|
||||
event.name === "workspace" ||
|
||||
event.name === "workspacev2" ||
|
||||
event.name === "focusedmon" ||
|
||||
event.name === "focusedmonv2" ||
|
||||
event.name === "activewindow" ||
|
||||
event.name === "activewindowv2" ||
|
||||
event.name === "changefloatingmode" ||
|
||||
event.name === "fullscreen" ||
|
||||
event.name === "moveintogroup" ||
|
||||
event.name === "moveoutofgroup") {
|
||||
try {
|
||||
Hyprland.refreshToplevels()
|
||||
} catch(e) {}
|
||||
root.scheduleSort()
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: Hyprland.workspaces
|
||||
function onValuesChanged() { root.scheduleSort() }
|
||||
}
|
||||
Connections {
|
||||
target: Hyprland
|
||||
function onFocusedWorkspaceChanged() { root.scheduleSort() }
|
||||
}
|
||||
Connections {
|
||||
target: NiriService
|
||||
function onWindowsChanged() { root.scheduleSort() }
|
||||
@@ -162,7 +153,6 @@ Singleton {
|
||||
|
||||
function sortHyprlandToplevelsSafe() {
|
||||
if (!Hyprland.toplevels || !Hyprland.toplevels.values) return []
|
||||
if (_refreshScheduled) return sortedToplevelsCache
|
||||
|
||||
const items = Array.from(Hyprland.toplevels.values)
|
||||
|
||||
@@ -174,26 +164,14 @@ Singleton {
|
||||
} catch(e) { return fb }
|
||||
}
|
||||
|
||||
let currentAddresses = new Set()
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const addr = items[i]?.address
|
||||
if (addr) currentAddresses.add(addr)
|
||||
}
|
||||
|
||||
for (let cachedAddr in _coordCache) {
|
||||
if (!currentAddresses.has(cachedAddr)) {
|
||||
delete _coordCache[cachedAddr]
|
||||
}
|
||||
}
|
||||
|
||||
let snap = []
|
||||
let missingAnyPosition = false
|
||||
let hasNewWindow = false
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const t = items[i]
|
||||
if (!t) continue
|
||||
|
||||
const addr = t.address || ""
|
||||
if (!addr) continue
|
||||
|
||||
const li = t.lastIpcObject || null
|
||||
|
||||
const monName = _get(li, ["monitor"], null) ?? _get(t, ["monitor", "name"], "")
|
||||
@@ -203,23 +181,8 @@ Singleton {
|
||||
const wsId = _get(li, ["workspace", "id"], null) ?? _get(t, ["workspace", "id"], Number.MAX_SAFE_INTEGER)
|
||||
|
||||
const at = _get(li, ["at"], null)
|
||||
let atX = (at !== null && at !== undefined && typeof at[0] === "number") ? at[0] : NaN
|
||||
let atY = (at !== null && at !== undefined && typeof at[1] === "number") ? at[1] : NaN
|
||||
|
||||
if (!(atX === atX) || !(atY === atY)) {
|
||||
const cached = _coordCache[addr]
|
||||
if (cached) {
|
||||
atX = cached.x
|
||||
atY = cached.y
|
||||
} else {
|
||||
if (addr) hasNewWindow = true
|
||||
missingAnyPosition = true
|
||||
atX = 1e9
|
||||
atY = 1e9
|
||||
}
|
||||
} else if (addr) {
|
||||
_coordCache[addr] = { x: atX, y: atY }
|
||||
}
|
||||
let atX = (at !== null && at !== undefined && typeof at[0] === "number") ? at[0] : 1e9
|
||||
let atY = (at !== null && at !== undefined && typeof at[1] === "number") ? at[1] : 1e9
|
||||
|
||||
const relX = Number.isFinite(monX) ? (atX - monX) : atX
|
||||
const relY = Number.isFinite(monY) ? (atY - monY) : atY
|
||||
@@ -237,11 +200,6 @@ Singleton {
|
||||
})
|
||||
}
|
||||
|
||||
if (missingAnyPosition && hasNewWindow) {
|
||||
_hasRefreshedOnce = false
|
||||
scheduleRefresh()
|
||||
}
|
||||
|
||||
const groups = new Map()
|
||||
for (const it of snap) {
|
||||
const key = it.monKey + "::" + it.wsId
|
||||
@@ -389,16 +347,15 @@ Singleton {
|
||||
}
|
||||
|
||||
function detectCompositor() {
|
||||
if (hyprlandSignature && hyprlandSignature.length > 0) {
|
||||
if (hyprlandSignature && hyprlandSignature.length > 0 &&
|
||||
!niriSocket && !swaySocket && !labwcPid) {
|
||||
isHyprland = true
|
||||
isNiri = false
|
||||
isDwl = false
|
||||
isSway = false
|
||||
isLabwc = false
|
||||
compositor = "hyprland"
|
||||
console.info("CompositorService: Detected Hyprland")
|
||||
try {
|
||||
Hyprland.refreshToplevels()
|
||||
} catch(e) {}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -409,6 +366,7 @@ Singleton {
|
||||
isHyprland = false
|
||||
isDwl = false
|
||||
isSway = false
|
||||
isLabwc = false
|
||||
compositor = "niri"
|
||||
console.info("CompositorService: Detected Niri with socket:", niriSocket)
|
||||
NiriService.generateNiriBinds()
|
||||
@@ -425,13 +383,25 @@ Singleton {
|
||||
isHyprland = false
|
||||
isDwl = false
|
||||
isSway = true
|
||||
isLabwc = false
|
||||
compositor = "sway"
|
||||
console.info("CompositorService: Detected Sway with socket:", swaySocket)
|
||||
}
|
||||
}, 0)
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (labwcPid && labwcPid.length > 0) {
|
||||
isHyprland = false
|
||||
isNiri = false
|
||||
isDwl = false
|
||||
isSway = false
|
||||
isLabwc = true
|
||||
compositor = "labwc"
|
||||
console.info("CompositorService: Detected LabWC with PID:", labwcPid)
|
||||
return
|
||||
}
|
||||
|
||||
if (DMSService.dmsAvailable) {
|
||||
Qt.callLater(checkForDwl)
|
||||
} else {
|
||||
@@ -439,6 +409,7 @@ Singleton {
|
||||
isNiri = false
|
||||
isDwl = false
|
||||
isSway = false
|
||||
isLabwc = false
|
||||
compositor = "unknown"
|
||||
console.warn("CompositorService: No compositor detected")
|
||||
}
|
||||
@@ -447,7 +418,7 @@ Singleton {
|
||||
Connections {
|
||||
target: DMSService
|
||||
function onCapabilitiesReceived() {
|
||||
if (!isHyprland && !isNiri && !isDwl) {
|
||||
if (!isHyprland && !isNiri && !isDwl && !isLabwc) {
|
||||
checkForDwl()
|
||||
}
|
||||
}
|
||||
@@ -458,6 +429,8 @@ Singleton {
|
||||
isHyprland = false
|
||||
isNiri = false
|
||||
isDwl = true
|
||||
isSway = false
|
||||
isLabwc = false
|
||||
compositor = "dwl"
|
||||
console.info("CompositorService: Detected DWL via DMS capability")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
@@ -20,6 +20,7 @@ Singleton {
|
||||
property bool isConnected: false
|
||||
property bool isConnecting: false
|
||||
property bool subscribeConnected: false
|
||||
readonly property bool forceExtWorkspace: false
|
||||
|
||||
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
|
||||
|
||||
@@ -46,8 +47,10 @@ Singleton {
|
||||
signal dwlStateUpdate(var data)
|
||||
signal brightnessStateUpdate(var data)
|
||||
signal brightnessDeviceUpdate(var device)
|
||||
signal extWorkspaceStateUpdate(var data)
|
||||
signal wlrOutputStateUpdate(var data)
|
||||
|
||||
property var activeSubscriptions: ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "dwl", "brightness"]
|
||||
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "gamma", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput"]
|
||||
|
||||
Component.onCompleted: {
|
||||
if (socketPath && socketPath.length > 0) {
|
||||
@@ -263,7 +266,7 @@ Singleton {
|
||||
|
||||
function removeSubscription(service) {
|
||||
if (activeSubscriptions.includes("all")) {
|
||||
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "dwl", "brightness"]
|
||||
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "dwl", "brightness", "extworkspace"]
|
||||
const filtered = allServices.filter(s => s !== service)
|
||||
subscribe(filtered)
|
||||
} else {
|
||||
@@ -285,7 +288,7 @@ Singleton {
|
||||
excludeServices = [excludeServices]
|
||||
}
|
||||
|
||||
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "cups", "dwl", "brightness"]
|
||||
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "cups", "dwl", "brightness", "extworkspace"]
|
||||
const filtered = allServices.filter(s => !excludeServices.includes(s))
|
||||
subscribe(filtered)
|
||||
}
|
||||
@@ -342,6 +345,10 @@ Singleton {
|
||||
if (data.device) {
|
||||
brightnessDeviceUpdate(data.device)
|
||||
}
|
||||
} else if (service === "extworkspace") {
|
||||
extWorkspaceStateUpdate(data)
|
||||
} else if (service === "wlroutput") {
|
||||
wlrOutputStateUpdate(data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
|
||||
59
Services/DesktopService.qml
Normal file
59
Services/DesktopService.qml
Normal file
@@ -0,0 +1,59 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
function resolveIconPath(moddedAppId) {
|
||||
const entry = DesktopEntries.heuristicLookup(moddedAppId)
|
||||
const appIds = [moddedAppId, moddedAppId.toLowerCase()];
|
||||
|
||||
const lastPart = moddedAppId.split('.').pop();
|
||||
if (lastPart && lastPart !== moddedAppId) {
|
||||
appIds.push(lastPart);
|
||||
|
||||
const firstChar = lastPart.charAt(0);
|
||||
const rest = lastPart.slice(1);
|
||||
let toggled;
|
||||
|
||||
if (firstChar === firstChar.toLowerCase()) {
|
||||
toggled = firstChar.toUpperCase() + rest;
|
||||
} else {
|
||||
toggled = firstChar.toLowerCase() + rest;
|
||||
}
|
||||
|
||||
if (toggled !== lastPart) {
|
||||
appIds.push(toggled);
|
||||
}
|
||||
}
|
||||
for (const appId of appIds){
|
||||
let icon = Quickshell.iconPath(entry?.icon, true)
|
||||
if (icon && icon !== "") return icon
|
||||
|
||||
let execPath = entry?.execString?.replace(/\/bin.*/, "")
|
||||
if (!execPath) continue
|
||||
|
||||
//Check that the app is installed with nix/guix
|
||||
if (execPath.startsWith("/nix/store/") || execPath.startsWith("/gnu/store/")) {
|
||||
const basePath = execPath
|
||||
const sizes = ["256x256", "128x128", "64x64", "48x48", "32x32", "24x24", "16x16"]
|
||||
|
||||
let iconPath = `${basePath}/share/icons/hicolor/scalable/apps/${appId}.svg`
|
||||
icon = Quickshell.iconPath(iconPath, true)
|
||||
if (icon && icon !== "") return icon
|
||||
|
||||
for (const size of sizes) {
|
||||
iconPath = `${basePath}/share/icons/hicolor/${size}/apps/${appId}.png`
|
||||
icon = Quickshell.iconPath(iconPath, true)
|
||||
if (icon && icon !== "") return icon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
}
|
||||
259
Services/ExtWorkspaceService.qml
Normal file
259
Services/ExtWorkspaceService.qml
Normal file
@@ -0,0 +1,259 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool extWorkspaceAvailable: false
|
||||
property var groups: []
|
||||
property var _cachedWorkspaces: ({})
|
||||
|
||||
signal stateChanged()
|
||||
|
||||
Connections {
|
||||
target: DMSService
|
||||
function onCapabilitiesReceived() {
|
||||
checkCapabilities()
|
||||
}
|
||||
function onConnectionStateChanged() {
|
||||
if (DMSService.isConnected) {
|
||||
checkCapabilities()
|
||||
} else {
|
||||
extWorkspaceAvailable = false
|
||||
}
|
||||
}
|
||||
function onExtWorkspaceStateUpdate(data) {
|
||||
if (extWorkspaceAvailable) {
|
||||
handleStateUpdate(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (DMSService.dmsAvailable) {
|
||||
checkCapabilities()
|
||||
}
|
||||
}
|
||||
|
||||
function checkCapabilities() {
|
||||
if (!DMSService.capabilities || !Array.isArray(DMSService.capabilities)) {
|
||||
extWorkspaceAvailable = false
|
||||
return
|
||||
}
|
||||
|
||||
const hasExtWorkspace = DMSService.capabilities.includes("extworkspace")
|
||||
if (hasExtWorkspace && !extWorkspaceAvailable) {
|
||||
extWorkspaceAvailable = true
|
||||
console.info("ExtWorkspaceService: ext-workspace capability detected")
|
||||
requestState()
|
||||
} else if (!hasExtWorkspace) {
|
||||
extWorkspaceAvailable = false
|
||||
}
|
||||
}
|
||||
|
||||
function requestState() {
|
||||
if (!DMSService.isConnected || !extWorkspaceAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("extworkspace.getState", null, response => {
|
||||
if (response.result) {
|
||||
handleStateUpdate(response.result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleStateUpdate(state) {
|
||||
groups = state.groups || []
|
||||
if (groups.length === 0) {
|
||||
console.warn("ExtWorkspaceService: Received empty workspace groups from backend")
|
||||
} else {
|
||||
console.log("ExtWorkspaceService: Updated with", groups.length, "workspace groups")
|
||||
}
|
||||
stateChanged()
|
||||
}
|
||||
|
||||
function activateWorkspace(workspaceID, groupID = "") {
|
||||
if (!DMSService.isConnected || !extWorkspaceAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("extworkspace.activateWorkspace", {
|
||||
"workspaceID": workspaceID,
|
||||
"groupID": groupID
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.warn("ExtWorkspaceService: activateWorkspace error:", response.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function deactivateWorkspace(workspaceID, groupID = "") {
|
||||
if (!DMSService.isConnected || !extWorkspaceAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("extworkspace.deactivateWorkspace", {
|
||||
"workspaceID": workspaceID,
|
||||
"groupID": groupID
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.warn("ExtWorkspaceService: deactivateWorkspace error:", response.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function removeWorkspace(workspaceID, groupID = "") {
|
||||
if (!DMSService.isConnected || !extWorkspaceAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("extworkspace.removeWorkspace", {
|
||||
"workspaceID": workspaceID,
|
||||
"groupID": groupID
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.warn("ExtWorkspaceService: removeWorkspace error:", response.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function createWorkspace(groupID, name) {
|
||||
if (!DMSService.isConnected || !extWorkspaceAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("extworkspace.createWorkspace", {
|
||||
"groupID": groupID,
|
||||
"name": name
|
||||
}, response => {
|
||||
if (response.error) {
|
||||
console.warn("ExtWorkspaceService: createWorkspace error:", response.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getGroupForOutput(outputName) {
|
||||
for (const group of groups) {
|
||||
if (group.outputs && group.outputs.includes(outputName)) {
|
||||
return group
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getWorkspacesForOutput(outputName) {
|
||||
const group = getGroupForOutput(outputName)
|
||||
return group ? (group.workspaces || []) : []
|
||||
}
|
||||
|
||||
function getActiveWorkspaces() {
|
||||
const active = []
|
||||
for (const group of groups) {
|
||||
if (!group.workspaces) continue
|
||||
for (const ws of group.workspaces) {
|
||||
if (ws.active) {
|
||||
active.push({
|
||||
workspace: ws,
|
||||
group: group,
|
||||
outputs: group.outputs || []
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return active
|
||||
}
|
||||
|
||||
function getActiveWorkspaceForOutput(outputName) {
|
||||
const group = getGroupForOutput(outputName)
|
||||
if (!group || !group.workspaces) return null
|
||||
|
||||
for (const ws of group.workspaces) {
|
||||
if (ws.active) {
|
||||
return ws
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getVisibleWorkspaces(outputName) {
|
||||
const workspaces = getWorkspacesForOutput(outputName)
|
||||
const visible = workspaces.filter(ws => !ws.hidden).sort((a, b) => {
|
||||
const coordsA = a.coordinates || [0, 0]
|
||||
const coordsB = b.coordinates || [0, 0]
|
||||
if (coordsA[0] !== coordsB[0]) return coordsA[0] - coordsB[0]
|
||||
return coordsA[1] - coordsB[1]
|
||||
})
|
||||
|
||||
const cacheKey = outputName
|
||||
if (!_cachedWorkspaces[cacheKey]) {
|
||||
_cachedWorkspaces[cacheKey] = {
|
||||
workspaces: [],
|
||||
lastNames: []
|
||||
}
|
||||
}
|
||||
|
||||
const cache = _cachedWorkspaces[cacheKey]
|
||||
const currentNames = visible.map(ws => ws.name || ws.id)
|
||||
const namesChanged = JSON.stringify(cache.lastNames) !== JSON.stringify(currentNames)
|
||||
|
||||
if (namesChanged || cache.workspaces.length !== visible.length) {
|
||||
cache.workspaces = visible.map(ws => ({
|
||||
id: ws.id,
|
||||
name: ws.name,
|
||||
coordinates: ws.coordinates,
|
||||
state: ws.state,
|
||||
active: ws.active,
|
||||
urgent: ws.urgent,
|
||||
hidden: ws.hidden
|
||||
}))
|
||||
cache.lastNames = currentNames
|
||||
return cache.workspaces
|
||||
}
|
||||
|
||||
for (let i = 0; i < visible.length; i++) {
|
||||
const src = visible[i]
|
||||
const dst = cache.workspaces[i]
|
||||
dst.id = src.id
|
||||
dst.name = src.name
|
||||
dst.coordinates = src.coordinates
|
||||
dst.state = src.state
|
||||
dst.active = src.active
|
||||
dst.urgent = src.urgent
|
||||
dst.hidden = src.hidden
|
||||
}
|
||||
|
||||
return cache.workspaces
|
||||
}
|
||||
|
||||
function getUrgentWorkspaces() {
|
||||
const urgent = []
|
||||
for (const group of groups) {
|
||||
if (!group.workspaces) continue
|
||||
for (const ws of group.workspaces) {
|
||||
if (ws.urgent) {
|
||||
urgent.push({
|
||||
workspace: ws,
|
||||
group: group,
|
||||
outputs: group.outputs || []
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return urgent
|
||||
}
|
||||
|
||||
function switchToWorkspace(outputName, workspaceName) {
|
||||
const workspaces = getWorkspacesForOutput(outputName)
|
||||
for (const ws of workspaces) {
|
||||
if (ws.name === workspaceName || ws.id === workspaceName) {
|
||||
activateWorkspace(ws.name || ws.id)
|
||||
return
|
||||
}
|
||||
}
|
||||
console.warn("ExtWorkspaceService: workspace not found:", workspaceName)
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtCore
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string _configUrl: StandardPaths.writableLocation(StandardPaths.ConfigLocation)
|
||||
readonly property string _configDir: Paths.strip(_configUrl)
|
||||
property string hyprConfigPath: `${_configDir}/hypr`
|
||||
property var keybinds: ({"children": [], "keybinds": []})
|
||||
|
||||
Process {
|
||||
id: getKeybinds
|
||||
running: false
|
||||
command: ["dms", "hyprland", "keybinds", "--path", root.hyprConfigPath]
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
try {
|
||||
root.keybinds = JSON.parse(data)
|
||||
} catch (e) {
|
||||
console.error("[HyprKeybindsService] Error parsing keybinds:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (code) => {
|
||||
if (code !== 0) {
|
||||
console.warn("[HyprKeybindsService] Process exited with code:", code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
getKeybinds.running = true
|
||||
}
|
||||
|
||||
function reload() {
|
||||
getKeybinds.running = false
|
||||
Qt.callLater(function() {
|
||||
getKeybinds.running = true
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.Mpris
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
@@ -18,6 +19,14 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
readonly property bool idleInhibitorAvailable: {
|
||||
try {
|
||||
return typeof IdleInhibitor !== "undefined"
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
property bool enabled: true
|
||||
property bool respectInhibitors: true
|
||||
property bool _enableGate: true
|
||||
@@ -28,6 +37,8 @@ Singleton {
|
||||
readonly property int suspendTimeout: isOnBattery ? SettingsData.batterySuspendTimeout : SettingsData.acSuspendTimeout
|
||||
readonly property int suspendBehavior: isOnBattery ? SettingsData.batterySuspendBehavior : SettingsData.acSuspendBehavior
|
||||
|
||||
readonly property bool mediaPlaying: MprisController.activePlayer !== null && MprisController.activePlayer.isPlaying
|
||||
|
||||
onMonitorTimeoutChanged: _rearmIdleMonitors()
|
||||
onLockTimeoutChanged: _rearmIdleMonitors()
|
||||
onSuspendTimeoutChanged: _rearmIdleMonitors()
|
||||
@@ -45,11 +56,42 @@ Singleton {
|
||||
property var monitorOffMonitor: null
|
||||
property var lockMonitor: null
|
||||
property var suspendMonitor: null
|
||||
property var mediaInhibitor: null
|
||||
|
||||
function wake() {
|
||||
requestMonitorOn()
|
||||
}
|
||||
|
||||
function createMediaInhibitor() {
|
||||
if (!idleInhibitorAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
if (mediaInhibitor) {
|
||||
mediaInhibitor.destroy()
|
||||
mediaInhibitor = null
|
||||
}
|
||||
|
||||
const inhibitorString = `
|
||||
import QtQuick
|
||||
import Quickshell.Wayland
|
||||
|
||||
IdleInhibitor {
|
||||
active: false
|
||||
}
|
||||
`
|
||||
|
||||
mediaInhibitor = Qt.createQmlObject(inhibitorString, root, "IdleService.MediaInhibitor")
|
||||
mediaInhibitor.active = Qt.binding(() => root.mediaPlaying)
|
||||
}
|
||||
|
||||
function destroyMediaInhibitor() {
|
||||
if (mediaInhibitor) {
|
||||
mediaInhibitor.destroy()
|
||||
mediaInhibitor = null
|
||||
}
|
||||
}
|
||||
|
||||
function createIdleMonitors() {
|
||||
if (!idleMonitorAvailable) {
|
||||
console.info("IdleService: IdleMonitor not available, skipping creation")
|
||||
@@ -99,6 +141,10 @@ Singleton {
|
||||
root.requestSuspend()
|
||||
}
|
||||
})
|
||||
|
||||
if (SettingsData.preventIdleForMedia) {
|
||||
createMediaInhibitor()
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("IdleService: Error creating IdleMonitors:", e)
|
||||
}
|
||||
@@ -128,6 +174,17 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onPreventIdleForMediaChanged() {
|
||||
if (SettingsData.preventIdleForMedia) {
|
||||
createMediaInhibitor()
|
||||
} else {
|
||||
destroyMediaInhibitor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!idleMonitorAvailable) {
|
||||
console.warn("IdleService: IdleMonitor not available - power management disabled. This requires a newer version of Quickshell.")
|
||||
|
||||
60
Services/KeybindsService.qml
Normal file
60
Services/KeybindsService.qml
Normal file
@@ -0,0 +1,60 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtCore
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string currentProvider: "hyprland"
|
||||
property var keybinds: ({"title": "", "provider": "", "binds": []})
|
||||
|
||||
Process {
|
||||
id: getKeybinds
|
||||
running: false
|
||||
command: ["dms", "keybinds", "show", root.currentProvider]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
root.keybinds = JSON.parse(text)
|
||||
} catch (e) {
|
||||
console.error("[KeybindsService] Error parsing keybinds:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (code) => {
|
||||
if (code !== 0 && code !== 15) {
|
||||
console.warn("[KeybindsService] Process exited with code:", code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 500
|
||||
running: true
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
getKeybinds.running = true
|
||||
}
|
||||
}
|
||||
|
||||
function loadProvider(provider) {
|
||||
root.currentProvider = provider
|
||||
reload()
|
||||
}
|
||||
|
||||
function reload() {
|
||||
if (getKeybinds.running) {
|
||||
getKeybinds.running = false
|
||||
}
|
||||
Qt.callLater(function() {
|
||||
getKeybinds.running = true
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -670,7 +670,7 @@ Singleton {
|
||||
wifiConnector.connectionSucceeded = false
|
||||
root.isConnecting = false
|
||||
root.connectingSSID = ""
|
||||
refreshNetworkState()
|
||||
doRefreshNetworkState()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -693,7 +693,7 @@ Singleton {
|
||||
root.currentWifiSSID = ""
|
||||
root.connectionStatus = ""
|
||||
}
|
||||
refreshNetworkState()
|
||||
doRefreshNetworkState()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -727,7 +727,7 @@ Singleton {
|
||||
}
|
||||
root.wifiNetworks = updated
|
||||
root.networksUpdated()
|
||||
refreshNetworkState()
|
||||
doRefreshNetworkState()
|
||||
}
|
||||
root.forgetSSID = ""
|
||||
}
|
||||
@@ -756,7 +756,7 @@ Singleton {
|
||||
if (exitCode === 0) {
|
||||
ToastService.showInfo(targetState === "on" ? "WiFi enabled" : "WiFi disabled")
|
||||
}
|
||||
refreshNetworkState()
|
||||
doRefreshNetworkState()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -802,7 +802,7 @@ Singleton {
|
||||
onExited: {
|
||||
root.changingPreference = false
|
||||
root.targetPreference = ""
|
||||
refreshNetworkState()
|
||||
doRefreshNetworkState()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -930,7 +930,7 @@ Singleton {
|
||||
} else {
|
||||
ToastService.showError("Failed to enable WiFi")
|
||||
}
|
||||
refreshNetworkState()
|
||||
doRefreshNetworkState()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -955,7 +955,7 @@ Singleton {
|
||||
running: false
|
||||
|
||||
onExited: function (exitCode) {
|
||||
refreshNetworkState()
|
||||
doRefreshNetworkState()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -965,7 +965,7 @@ Singleton {
|
||||
running: false
|
||||
|
||||
onExited: function (exitCode) {
|
||||
refreshNetworkState()
|
||||
doRefreshNetworkState()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
|
||||
275
Services/WlrOutputService.qml
Normal file
275
Services/WlrOutputService.qml
Normal file
@@ -0,0 +1,275 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool wlrOutputAvailable: false
|
||||
property var outputs: []
|
||||
property int serial: 0
|
||||
|
||||
signal stateChanged
|
||||
signal configurationApplied(bool success, string message)
|
||||
|
||||
Connections {
|
||||
target: DMSService
|
||||
|
||||
function onCapabilitiesReceived() {
|
||||
checkCapabilities()
|
||||
}
|
||||
|
||||
function onConnectionStateChanged() {
|
||||
if (DMSService.isConnected) {
|
||||
checkCapabilities()
|
||||
return
|
||||
}
|
||||
wlrOutputAvailable = false
|
||||
}
|
||||
|
||||
function onWlrOutputStateUpdate(data) {
|
||||
if (!wlrOutputAvailable) {
|
||||
return
|
||||
}
|
||||
handleStateUpdate(data)
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!DMSService.dmsAvailable) {
|
||||
return
|
||||
}
|
||||
checkCapabilities()
|
||||
}
|
||||
|
||||
function checkCapabilities() {
|
||||
if (!DMSService.capabilities || !Array.isArray(DMSService.capabilities)) {
|
||||
wlrOutputAvailable = false
|
||||
return
|
||||
}
|
||||
|
||||
const hasWlrOutput = DMSService.capabilities.includes("wlroutput")
|
||||
if (hasWlrOutput && !wlrOutputAvailable) {
|
||||
wlrOutputAvailable = true
|
||||
console.info("WlrOutputService: wlr-output-management capability detected")
|
||||
requestState()
|
||||
return
|
||||
}
|
||||
|
||||
if (!hasWlrOutput) {
|
||||
wlrOutputAvailable = false
|
||||
}
|
||||
}
|
||||
|
||||
function requestState() {
|
||||
if (!DMSService.isConnected || !wlrOutputAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
DMSService.sendRequest("wlroutput.getState", null, response => {
|
||||
if (!response.result) {
|
||||
return
|
||||
}
|
||||
handleStateUpdate(response.result)
|
||||
})
|
||||
}
|
||||
|
||||
function handleStateUpdate(state) {
|
||||
outputs = state.outputs || []
|
||||
serial = state.serial || 0
|
||||
|
||||
if (outputs.length === 0) {
|
||||
console.warn("WlrOutputService: Received empty outputs list")
|
||||
} else {
|
||||
console.log("WlrOutputService: Updated with", outputs.length, "outputs, serial:", serial)
|
||||
outputs.forEach((output, index) => {
|
||||
console.log("WlrOutputService: Output", index, "-", output.name,
|
||||
"enabled:", output.enabled,
|
||||
"mode:", output.currentMode ?
|
||||
output.currentMode.width + "x" + output.currentMode.height + "@" +
|
||||
(output.currentMode.refresh / 1000) + "Hz" : "none")
|
||||
})
|
||||
}
|
||||
stateChanged()
|
||||
}
|
||||
|
||||
function getOutput(name) {
|
||||
for (const output of outputs) {
|
||||
if (output.name === name) {
|
||||
return output
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getEnabledOutputs() {
|
||||
return outputs.filter(output => output.enabled)
|
||||
}
|
||||
|
||||
function applyConfiguration(heads, callback) {
|
||||
if (!DMSService.isConnected || !wlrOutputAvailable) {
|
||||
if (callback) {
|
||||
callback(false, "Not connected")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
console.log("WlrOutputService: Applying configuration for", heads.length, "outputs")
|
||||
heads.forEach((head, index) => {
|
||||
console.log("WlrOutputService: Head", index, "- name:", head.name,
|
||||
"enabled:", head.enabled,
|
||||
"modeId:", head.modeId,
|
||||
"customMode:", JSON.stringify(head.customMode),
|
||||
"position:", JSON.stringify(head.position),
|
||||
"scale:", head.scale,
|
||||
"transform:", head.transform,
|
||||
"adaptiveSync:", head.adaptiveSync)
|
||||
})
|
||||
|
||||
DMSService.sendRequest("wlroutput.applyConfiguration", {
|
||||
"heads": heads
|
||||
}, response => {
|
||||
const success = !response.error
|
||||
const message = response.error || response.result?.message || ""
|
||||
|
||||
if (response.error) {
|
||||
console.warn("WlrOutputService: applyConfiguration error:", response.error)
|
||||
} else {
|
||||
console.log("WlrOutputService: Configuration applied successfully")
|
||||
}
|
||||
|
||||
configurationApplied(success, message)
|
||||
if (callback) {
|
||||
callback(success, message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function testConfiguration(heads, callback) {
|
||||
if (!DMSService.isConnected || !wlrOutputAvailable) {
|
||||
if (callback) {
|
||||
callback(false, "Not connected")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
console.log("WlrOutputService: Testing configuration for", heads.length, "outputs")
|
||||
|
||||
DMSService.sendRequest("wlroutput.testConfiguration", {
|
||||
"heads": heads
|
||||
}, response => {
|
||||
const success = !response.error
|
||||
const message = response.error || response.result?.message || ""
|
||||
|
||||
if (response.error) {
|
||||
console.warn("WlrOutputService: testConfiguration error:", response.error)
|
||||
} else {
|
||||
console.log("WlrOutputService: Configuration test passed")
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback(success, message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function setOutputEnabled(outputName, enabled, callback) {
|
||||
const output = getOutput(outputName)
|
||||
if (!output) {
|
||||
console.warn("WlrOutputService: Output not found:", outputName)
|
||||
if (callback) {
|
||||
callback(false, "Output not found")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const heads = [{
|
||||
"name": outputName,
|
||||
"enabled": enabled
|
||||
}]
|
||||
|
||||
if (enabled && output.currentMode) {
|
||||
heads[0].modeId = output.currentMode.id
|
||||
}
|
||||
|
||||
applyConfiguration(heads, callback)
|
||||
}
|
||||
|
||||
function setOutputMode(outputName, modeId, callback) {
|
||||
const heads = [{
|
||||
"name": outputName,
|
||||
"enabled": true,
|
||||
"modeId": modeId
|
||||
}]
|
||||
|
||||
applyConfiguration(heads, callback)
|
||||
}
|
||||
|
||||
function setOutputCustomMode(outputName, width, height, refresh, callback) {
|
||||
const heads = [{
|
||||
"name": outputName,
|
||||
"enabled": true,
|
||||
"customMode": {
|
||||
"width": width,
|
||||
"height": height,
|
||||
"refresh": refresh
|
||||
}
|
||||
}]
|
||||
|
||||
applyConfiguration(heads, callback)
|
||||
}
|
||||
|
||||
function setOutputPosition(outputName, x, y, callback) {
|
||||
const heads = [{
|
||||
"name": outputName,
|
||||
"enabled": true,
|
||||
"position": {
|
||||
"x": x,
|
||||
"y": y
|
||||
}
|
||||
}]
|
||||
|
||||
applyConfiguration(heads, callback)
|
||||
}
|
||||
|
||||
function setOutputScale(outputName, scale, callback) {
|
||||
const heads = [{
|
||||
"name": outputName,
|
||||
"enabled": true,
|
||||
"scale": scale
|
||||
}]
|
||||
|
||||
applyConfiguration(heads, callback)
|
||||
}
|
||||
|
||||
function setOutputTransform(outputName, transform, callback) {
|
||||
const heads = [{
|
||||
"name": outputName,
|
||||
"enabled": true,
|
||||
"transform": transform
|
||||
}]
|
||||
|
||||
applyConfiguration(heads, callback)
|
||||
}
|
||||
|
||||
function setOutputAdaptiveSync(outputName, state, callback) {
|
||||
const heads = [{
|
||||
"name": outputName,
|
||||
"enabled": true,
|
||||
"adaptiveSync": state
|
||||
}]
|
||||
|
||||
applyConfiguration(heads, callback)
|
||||
}
|
||||
|
||||
function configureOutput(config, callback) {
|
||||
const heads = [config]
|
||||
applyConfiguration(heads, callback)
|
||||
}
|
||||
|
||||
function configureMultipleOutputs(configs, callback) {
|
||||
applyConfiguration(configs, callback)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -26,7 +27,7 @@ Item {
|
||||
readonly property bool isUnicode: iconValue.startsWith("unicode:")
|
||||
readonly property string materialName: isMaterial ? iconValue.substring(9) : ""
|
||||
readonly property string unicodeChar: isUnicode ? iconValue.substring(8) : ""
|
||||
readonly property string iconPath: isMaterial || isUnicode ? "" : Quickshell.iconPath(iconValue, true)
|
||||
readonly property string iconPath: isMaterial || isUnicode ? "" : Quickshell.iconPath(iconValue, true) || DesktopService.resolveIconPath(iconValue)
|
||||
|
||||
visible: iconValue !== undefined && iconValue !== ""
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ Rectangle {
|
||||
width: computedIconSize
|
||||
height: computedIconSize
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
iconValue: model.icon || ""
|
||||
iconValue: model.icon && model.icon !== "" ? model.icon : model.startupClass
|
||||
iconSize: computedIconSize
|
||||
fallbackText: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||
materialIconSizeAdjustment: root.iconMaterialSizeAdjustment
|
||||
|
||||
@@ -45,7 +45,7 @@ Rectangle {
|
||||
width: root.iconSize
|
||||
height: root.iconSize
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconValue: model.icon || ""
|
||||
iconValue: model.icon && model.icon !== "" ? model.icon : model.startupClass
|
||||
iconSize: root.iconSize
|
||||
fallbackText: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||
iconMargins: root.iconMargins
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
@@ -64,7 +65,9 @@ Flow {
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: root.model
|
||||
model: ScriptModel {
|
||||
values: root.model
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
id: segment
|
||||
|
||||
@@ -2,6 +2,7 @@ import "../Common/fzf.js" as Fzf
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
@@ -21,6 +22,7 @@ Item {
|
||||
property bool alignPopupRight: false
|
||||
property int dropdownWidth: 200
|
||||
property bool compactMode: text === "" && description === ""
|
||||
property bool addHorizontalPadding: false
|
||||
|
||||
signal valueChanged(string value)
|
||||
|
||||
@@ -40,6 +42,7 @@ Item {
|
||||
anchors.left: parent.left
|
||||
anchors.right: dropdown.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: root.addHorizontalPadding ? Theme.spacingM : 0
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
spacing: Theme.spacingXS
|
||||
visible: !root.compactMode
|
||||
@@ -67,6 +70,7 @@ Item {
|
||||
width: root.compactMode ? parent.width : (root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : root.dropdownWidth))
|
||||
height: 40
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: root.addHorizontalPadding && !root.compactMode ? Theme.spacingM : 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: Theme.cornerRadius
|
||||
color: dropdownArea.containsMouse || dropdownMenu.visible ? Theme.surfaceContainerHigh : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
@@ -303,7 +307,9 @@ Item {
|
||||
width: parent.width
|
||||
height: parent.height - (root.enableFuzzySearch ? searchContainer.height + Theme.spacingXS : 0)
|
||||
clip: true
|
||||
model: dropdownMenu.filteredOptions
|
||||
model: ScriptModel {
|
||||
values: dropdownMenu.filteredOptions
|
||||
}
|
||||
spacing: 2
|
||||
|
||||
interactive: true
|
||||
|
||||
@@ -29,6 +29,7 @@ Flickable {
|
||||
property real lastWheelTime: 0
|
||||
property real momentum: 0
|
||||
property var velocitySamples: []
|
||||
property bool sessionUsedMouseWheel: false
|
||||
|
||||
function startMomentum() {
|
||||
flickable.isMomentumActive = true
|
||||
@@ -49,10 +50,12 @@ Flickable {
|
||||
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0
|
||||
|
||||
if (isMouseWheel) {
|
||||
sessionUsedMouseWheel = true
|
||||
momentumTimer.stop()
|
||||
flickable.isMomentumActive = false
|
||||
velocitySamples = []
|
||||
momentum = 0
|
||||
flickable.momentumVelocity = 0
|
||||
|
||||
const lines = Math.floor(Math.abs(deltaY) / 120)
|
||||
const scrollAmount = (deltaY > 0 ? -lines : lines) * flickable.mouseWheelSpeed
|
||||
@@ -65,6 +68,7 @@ Flickable {
|
||||
|
||||
flickable.contentY = newY
|
||||
} else {
|
||||
sessionUsedMouseWheel = false
|
||||
momentumTimer.stop()
|
||||
flickable.isMomentumActive = false
|
||||
|
||||
@@ -111,7 +115,7 @@ Flickable {
|
||||
|
||||
onActiveChanged: {
|
||||
if (!active) {
|
||||
if (Math.abs(flickable.momentumVelocity) >= flickable.minMomentumVelocity) {
|
||||
if (!sessionUsedMouseWheel && Math.abs(flickable.momentumVelocity) >= flickable.minMomentumVelocity) {
|
||||
startMomentum()
|
||||
} else {
|
||||
velocitySamples = []
|
||||
|
||||
@@ -33,6 +33,7 @@ GridView {
|
||||
property real lastWheelTime: 0
|
||||
property real momentum: 0
|
||||
property var velocitySamples: []
|
||||
property bool sessionUsedMouseWheel: false
|
||||
|
||||
function startMomentum() {
|
||||
isMomentumActive = true
|
||||
@@ -52,10 +53,12 @@ GridView {
|
||||
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0
|
||||
|
||||
if (isMouseWheel) {
|
||||
sessionUsedMouseWheel = true
|
||||
momentumTimer.stop()
|
||||
isMomentumActive = false
|
||||
velocitySamples = []
|
||||
momentum = 0
|
||||
momentumVelocity = 0
|
||||
|
||||
const lines = Math.floor(Math.abs(deltaY) / 120)
|
||||
const scrollAmount = (deltaY > 0 ? -lines : lines) * cellHeight * 0.35
|
||||
@@ -68,6 +71,7 @@ GridView {
|
||||
|
||||
contentY = newY
|
||||
} else {
|
||||
sessionUsedMouseWheel = false
|
||||
momentumTimer.stop()
|
||||
isMomentumActive = false
|
||||
|
||||
@@ -108,7 +112,7 @@ GridView {
|
||||
}
|
||||
onActiveChanged: {
|
||||
if (!active) {
|
||||
if (Math.abs(momentumVelocity) >= minMomentumVelocity) {
|
||||
if (!sessionUsedMouseWheel && Math.abs(momentumVelocity) >= minMomentumVelocity) {
|
||||
startMomentum()
|
||||
} else {
|
||||
velocitySamples = []
|
||||
|
||||
@@ -50,6 +50,7 @@ ListView {
|
||||
property real lastWheelTime: 0
|
||||
property real momentum: 0
|
||||
property var velocitySamples: []
|
||||
property bool sessionUsedMouseWheel: false
|
||||
|
||||
function startMomentum() {
|
||||
isMomentumActive = true
|
||||
@@ -71,10 +72,12 @@ ListView {
|
||||
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0
|
||||
|
||||
if (isMouseWheel) {
|
||||
sessionUsedMouseWheel = true
|
||||
momentumTimer.stop()
|
||||
isMomentumActive = false
|
||||
velocitySamples = []
|
||||
momentum = 0
|
||||
momentumVelocity = 0
|
||||
|
||||
const lines = Math.floor(Math.abs(deltaY) / 120)
|
||||
const scrollAmount = (deltaY > 0 ? -lines : lines) * mouseWheelSpeed
|
||||
@@ -89,6 +92,7 @@ ListView {
|
||||
listView.contentY = newY
|
||||
savedY = newY
|
||||
} else {
|
||||
sessionUsedMouseWheel = false
|
||||
momentumTimer.stop()
|
||||
isMomentumActive = false
|
||||
|
||||
@@ -138,7 +142,7 @@ ListView {
|
||||
onActiveChanged: {
|
||||
if (!active) {
|
||||
isUserScrolling = false
|
||||
if (Math.abs(momentumVelocity) >= minMomentumVelocity) {
|
||||
if (!sessionUsedMouseWheel && Math.abs(momentumVelocity) >= minMomentumVelocity) {
|
||||
startMomentum()
|
||||
} else {
|
||||
velocitySamples = []
|
||||
|
||||
@@ -5,15 +5,28 @@ Item {
|
||||
id: root
|
||||
|
||||
property string name: ""
|
||||
property alias size: icon.font.pixelSize
|
||||
property int size: Theme.fontSizeMedium
|
||||
property alias color: icon.color
|
||||
|
||||
implicitWidth: icon.implicitWidth
|
||||
implicitHeight: icon.implicitHeight
|
||||
width: size
|
||||
height: size
|
||||
visible: text.length > 0
|
||||
|
||||
// This is for file browser, particularly - might want another map later for app IDs
|
||||
readonly property var iconMap: ({
|
||||
// --- Distribution logos ---
|
||||
"debian": "\u{f08da}",
|
||||
"arch": "\u{f08c7}",
|
||||
"archcraft": "\u{f345}",
|
||||
"guix": "\u{f325}",
|
||||
"fedora": "\u{f08db}",
|
||||
"nixos": "\u{f1105}",
|
||||
"ubuntu": "\u{f0548}",
|
||||
"gentoo": "\u{f08e8}",
|
||||
"endeavouros": "\u{f322}",
|
||||
"manjaro": "\u{f160a}",
|
||||
"opensuse": "\u{f314}",
|
||||
|
||||
// --- special types ---
|
||||
"folder": "\u{F024B}",
|
||||
"file": "\u{F0214}",
|
||||
@@ -129,14 +142,12 @@ Item {
|
||||
StyledText {
|
||||
id: icon
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.centerIn: parent
|
||||
|
||||
font.family: firaCodeFont.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.pixelSize: root.size
|
||||
color: Theme.surfaceText
|
||||
text: root.text
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
antialiasing: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
@@ -117,14 +118,37 @@ PanelWindow {
|
||||
scale: shouldBeVisible ? 1 : 0.9
|
||||
|
||||
property bool childHovered: false
|
||||
property real shadowBlurPx: 10
|
||||
property real shadowSpreadPx: 0
|
||||
property real shadowBaseAlpha: 0.60
|
||||
readonly property real popupSurfaceAlpha: SettingsData.popupTransparency
|
||||
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha * osdContainer.opacity))
|
||||
|
||||
Rectangle {
|
||||
id: osdBackground
|
||||
Item {
|
||||
id: bgShadowLayer
|
||||
anchors.fill: parent
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
visible: osdContainer.popupSurfaceAlpha >= 0.95
|
||||
layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
layer.smooth: false
|
||||
layer.textureSize: Qt.size(Math.round(width * root.dpr), Math.round(height * root.dpr))
|
||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
id: shadowFx
|
||||
autoPaddingEnabled: true
|
||||
shadowEnabled: true
|
||||
blurEnabled: false
|
||||
maskEnabled: false
|
||||
property int blurMax: 64
|
||||
shadowBlur: Math.max(0, Math.min(1, osdContainer.shadowBlurPx / blurMax))
|
||||
shadowScale: 1 + (2 * osdContainer.shadowSpreadPx) / Math.max(1, Math.min(bgShadowLayer.width, bgShadowLayer.height))
|
||||
shadowColor: Qt.rgba(0, 0, 0, osdContainer.effectiveShadowAlpha)
|
||||
}
|
||||
|
||||
DankRectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@@ -162,6 +186,6 @@ PanelWindow {
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: osdBackground
|
||||
item: bgShadowLayer
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
@@ -63,7 +65,18 @@ PanelWindow {
|
||||
}
|
||||
|
||||
color: "transparent"
|
||||
WlrLayershell.layer: WlrLayershell.Top
|
||||
WlrLayershell.layer: {
|
||||
switch (Quickshell.env("DMS_DANKBAR_LAYER")) {
|
||||
case "bottom":
|
||||
return WlrLayershell.Bottom
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay
|
||||
case "background":
|
||||
return WlrLayershell.Background
|
||||
default:
|
||||
return WlrLayershell.Top
|
||||
}
|
||||
}
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: shouldBeVisible ? keyboardFocusMode : WlrKeyboardFocus.None
|
||||
|
||||
@@ -114,92 +127,131 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
Item {
|
||||
id: contentContainer
|
||||
x: alignedX
|
||||
y: alignedY
|
||||
width: alignedWidth
|
||||
height: alignedHeight
|
||||
active: root.visible
|
||||
asynchronous: false
|
||||
transformOrigin: Item.Center
|
||||
layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
layer.smooth: true
|
||||
layer.textureSize: Qt.size(width * Math.max(2, root.screen?.devicePixelRatio || 1), height * Math.max(2, root.screen?.devicePixelRatio || 1))
|
||||
layer.samples: 4
|
||||
opacity: shouldBeVisible ? 1 : 0
|
||||
visible: opacity > 0
|
||||
transform: [scaleTransform, motionTransform]
|
||||
|
||||
Scale {
|
||||
id: scaleTransform
|
||||
readonly property bool barTop: SettingsData.dankBarPosition === SettingsData.Position.Top
|
||||
readonly property bool barBottom: SettingsData.dankBarPosition === SettingsData.Position.Bottom
|
||||
readonly property bool barLeft: SettingsData.dankBarPosition === SettingsData.Position.Left
|
||||
readonly property bool barRight: SettingsData.dankBarPosition === SettingsData.Position.Right
|
||||
readonly property real offsetX: barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0)
|
||||
readonly property real offsetY: barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0)
|
||||
|
||||
origin.x: contentLoader.width / 2
|
||||
origin.y: contentLoader.height / 2
|
||||
xScale: root.shouldBeVisible ? 1 : root.animationScaleCollapsed
|
||||
yScale: root.shouldBeVisible ? 1 : root.animationScaleCollapsed
|
||||
property real animX: 0
|
||||
property real animY: 0
|
||||
property real scaleValue: root.animationScaleCollapsed
|
||||
|
||||
Behavior on xScale {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
|
||||
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
|
||||
|
||||
Behavior on yScale {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
contentContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetX, root.dpr)
|
||||
contentContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetY, root.dpr)
|
||||
contentContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed
|
||||
}
|
||||
}
|
||||
|
||||
Translate {
|
||||
id: motionTransform
|
||||
|
||||
readonly property bool barTop: SettingsData.dankBarPosition === SettingsData.Position.Top
|
||||
readonly property bool barBottom: SettingsData.dankBarPosition === SettingsData.Position.Bottom
|
||||
readonly property bool barLeft: SettingsData.dankBarPosition === SettingsData.Position.Left
|
||||
readonly property bool barRight: SettingsData.dankBarPosition === SettingsData.Position.Right
|
||||
readonly property real hiddenX: barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0)
|
||||
readonly property real hiddenY: barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0)
|
||||
|
||||
x: Theme.snap(root.shouldBeVisible ? 0 : hiddenX, root.dpr)
|
||||
y: Theme.snap(root.shouldBeVisible ? 0 : hiddenY, root.dpr)
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Behavior on animX {
|
||||
NumberAnimation {
|
||||
duration: animationDuration
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animY {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scaleValue {
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentWrapper
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
opacity: shouldBeVisible ? 1 : 0
|
||||
visible: opacity > 0
|
||||
scale: contentContainer.scaleValue
|
||||
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
|
||||
property real shadowBlurPx: 10
|
||||
property real shadowSpreadPx: 0
|
||||
property real shadowBaseAlpha: 0.60
|
||||
readonly property real popupSurfaceAlpha: SettingsData.popupTransparency
|
||||
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha * contentWrapper.opacity))
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: bgShadowLayer
|
||||
anchors.fill: parent
|
||||
visible: contentWrapper.popupSurfaceAlpha >= 0.95
|
||||
layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
layer.smooth: false
|
||||
layer.textureSize: Qt.size(Math.round(width * root.dpr), Math.round(height * root.dpr))
|
||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
id: shadowFx
|
||||
autoPaddingEnabled: true
|
||||
shadowEnabled: true
|
||||
blurEnabled: false
|
||||
maskEnabled: false
|
||||
property int blurMax: 64
|
||||
shadowBlur: Math.max(0, Math.min(1, contentWrapper.shadowBlurPx / blurMax))
|
||||
shadowScale: 1 + (2 * contentWrapper.shadowSpreadPx) / Math.max(1, Math.min(bgShadowLayer.width, bgShadowLayer.height))
|
||||
shadowColor: Qt.rgba(0, 0, 0, contentWrapper.effectiveShadowAlpha)
|
||||
}
|
||||
|
||||
DankRectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentLoaderWrapper
|
||||
anchors.fill: parent
|
||||
x: Theme.snap(x, root.dpr)
|
||||
y: Theme.snap(y, root.dpr)
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
active: root.visible
|
||||
asynchronous: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
x: alignedX
|
||||
y: alignedY
|
||||
width: alignedWidth
|
||||
height: alignedHeight
|
||||
parent: contentContainer
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
|
||||
54
Widgets/DankRectangle.qml
Normal file
54
Widgets/DankRectangle.qml
Normal file
@@ -0,0 +1,54 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property color color: Theme.surfaceContainer
|
||||
property color borderColor: Theme.outlineMedium
|
||||
property real borderWidth: 1
|
||||
property real radius: Theme.cornerRadius
|
||||
property color overlayColor: "transparent"
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: root.color
|
||||
strokeColor: root.borderColor
|
||||
strokeWidth: root.borderWidth
|
||||
|
||||
startX: root.radius
|
||||
startY: 0
|
||||
|
||||
PathLine { x: root.width - root.radius; y: 0 }
|
||||
PathQuad { x: root.width; y: root.radius; controlX: root.width; controlY: 0 }
|
||||
PathLine { x: root.width; y: root.height - root.radius }
|
||||
PathQuad { x: root.width - root.radius; y: root.height; controlX: root.width; controlY: root.height }
|
||||
PathLine { x: root.radius; y: root.height }
|
||||
PathQuad { x: 0; y: root.height - root.radius; controlX: 0; controlY: root.height }
|
||||
PathLine { x: 0; y: root.radius }
|
||||
PathQuad { x: root.radius; y: 0; controlX: 0; controlY: 0 }
|
||||
}
|
||||
|
||||
ShapePath {
|
||||
fillColor: root.overlayColor
|
||||
strokeColor: "transparent"
|
||||
strokeWidth: 0
|
||||
|
||||
startX: root.radius
|
||||
startY: 0
|
||||
|
||||
PathLine { x: root.width - root.radius; y: 0 }
|
||||
PathQuad { x: root.width; y: root.radius; controlX: root.width; controlY: 0 }
|
||||
PathLine { x: root.width; y: root.height - root.radius }
|
||||
PathQuad { x: root.width - root.radius; y: root.height; controlX: root.width; controlY: root.height }
|
||||
PathLine { x: root.radius; y: root.height }
|
||||
PathQuad { x: 0; y: root.height - root.radius; controlX: 0; controlY: root.height }
|
||||
PathLine { x: 0; y: root.radius }
|
||||
PathQuad { x: root.radius; y: 0; controlX: 0; controlY: 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
@@ -58,47 +59,45 @@ PanelWindow {
|
||||
WlrLayershell.exclusiveZone: 0
|
||||
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
||||
|
||||
readonly property real dpr: CompositorService.getScreenScale(root.screen)
|
||||
readonly property real alignedWidth: Theme.px(expandable && expandedWidth ? expandedWidthValue : slideoutWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(modelData ? modelData.height : 800, dpr)
|
||||
|
||||
mask: Region {
|
||||
item: Rectangle {
|
||||
x: root.width - contentRect.width
|
||||
x: root.width - alignedWidth
|
||||
y: 0
|
||||
width: contentRect.width
|
||||
width: alignedWidth
|
||||
height: root.height
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: contentRect
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
layer.textureSize: Qt.size(width * Math.max(2, root.modelData?.devicePixelRatio || 1), height * Math.max(2, root.modelData?.devicePixelRatio || 1))
|
||||
layer.samples: 4
|
||||
|
||||
Item {
|
||||
id: slideContainer
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
width: expandable && expandedWidth ? expandedWidthValue : slideoutWidth
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b,
|
||||
customTransparency >= 0 ? customTransparency : SettingsData.popupTransparency)
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: 1
|
||||
radius: Theme.cornerRadius
|
||||
visible: isVisible || slideAnimation.running
|
||||
width: alignedWidth
|
||||
height: alignedHeight
|
||||
|
||||
transform: Translate {
|
||||
id: slideTransform
|
||||
x: isVisible ? 0 : contentRect.width
|
||||
property real slideOffset: alignedWidth
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
id: slideAnimation
|
||||
duration: 450
|
||||
easing.type: Easing.OutCubic
|
||||
Connections {
|
||||
target: root
|
||||
function onIsVisibleChanged() {
|
||||
slideContainer.slideOffset = root.isVisible ? 0 : slideContainer.width
|
||||
}
|
||||
}
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running && !isVisible) {
|
||||
root.visible = false
|
||||
}
|
||||
Behavior on slideOffset {
|
||||
NumberAnimation {
|
||||
id: slideAnimation
|
||||
duration: 450
|
||||
easing.type: Easing.OutCubic
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running && !isVisible) {
|
||||
root.visible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,76 +110,94 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: headerColumn
|
||||
Item {
|
||||
id: contentRect
|
||||
layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
layer.smooth: false
|
||||
layer.textureSize: Qt.size(width * root.dpr, height * root.dpr)
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
visible: root.title !== ""
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width
|
||||
x: Theme.snap(slideContainer.slideOffset, root.dpr)
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 32
|
||||
DankRectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b,
|
||||
customTransparency >= 0 ? customTransparency : SettingsData.popupTransparency)
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - buttonRow.width
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
text: root.title
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
Column {
|
||||
id: headerColumn
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
visible: root.title !== ""
|
||||
|
||||
Row {
|
||||
id: buttonRow
|
||||
spacing: Theme.spacingXS
|
||||
width: parent.width
|
||||
height: 32
|
||||
|
||||
DankActionButton {
|
||||
id: expandButton
|
||||
iconName: root.expandedWidth ? "unfold_less" : "unfold_more"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
visible: root.expandable
|
||||
onClicked: root.expandedWidth = !root.expandedWidth
|
||||
Column {
|
||||
width: parent.width - buttonRow.width
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
transform: Rotation {
|
||||
angle: 90
|
||||
origin.x: expandButton.width / 2
|
||||
origin.y: expandButton.height / 2
|
||||
StyledText {
|
||||
text: root.title
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: closeButton
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: root.hide()
|
||||
Row {
|
||||
id: buttonRow
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankActionButton {
|
||||
id: expandButton
|
||||
iconName: root.expandedWidth ? "unfold_less" : "unfold_more"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
visible: root.expandable
|
||||
onClicked: root.expandedWidth = !root.expandedWidth
|
||||
|
||||
transform: Rotation {
|
||||
angle: 90
|
||||
origin.x: expandButton.width / 2
|
||||
origin.y: expandButton.height / 2
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: closeButton
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: root.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
anchors.top: root.title !== "" ? headerColumn.bottom : parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.topMargin: root.title !== "" ? 0 : Theme.spacingL
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
anchors.bottomMargin: Theme.spacingL
|
||||
Item {
|
||||
id: contentContainer
|
||||
anchors.top: root.title !== "" ? headerColumn.bottom : parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.topMargin: root.title !== "" ? 0 : Theme.spacingL
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
anchors.bottomMargin: Theme.spacingL
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
sourceComponent: root.content
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
sourceComponent: root.content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ FocusScope {
|
||||
color: Theme.outlineStrong
|
||||
}
|
||||
|
||||
function updateIndicator(enableAnimation = true) {
|
||||
function updateIndicator() {
|
||||
if (tabRepeater.count === 0 || currentIndex < 0 || currentIndex >= tabRepeater.count) {
|
||||
return
|
||||
}
|
||||
@@ -229,25 +229,26 @@ FocusScope {
|
||||
const indicatorWidth = 60
|
||||
|
||||
if (tabPos.x < 10 && currentIndex > 0) {
|
||||
Qt.callLater(() => updateIndicator(enableAnimation))
|
||||
Qt.callLater(updateIndicator)
|
||||
return
|
||||
}
|
||||
|
||||
indicator.animationEnabled = enableAnimation
|
||||
indicator.width = indicatorWidth
|
||||
indicator.x = tabCenterX - indicatorWidth / 2
|
||||
indicator.visible = true
|
||||
if (!indicator.initialSetupComplete) {
|
||||
indicator.animationEnabled = false
|
||||
indicator.width = indicatorWidth
|
||||
indicator.x = tabCenterX - indicatorWidth / 2
|
||||
indicator.visible = true
|
||||
indicator.initialSetupComplete = true
|
||||
indicator.animationEnabled = true
|
||||
} else {
|
||||
indicator.width = indicatorWidth
|
||||
indicator.x = tabCenterX - indicatorWidth / 2
|
||||
indicator.visible = true
|
||||
}
|
||||
}
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
if (indicator.initialSetupComplete) {
|
||||
Qt.callLater(() => updateIndicator(true))
|
||||
} else {
|
||||
Qt.callLater(() => {
|
||||
updateIndicator(false)
|
||||
indicator.initialSetupComplete = true
|
||||
})
|
||||
}
|
||||
Qt.callLater(updateIndicator)
|
||||
}
|
||||
onWidthChanged: Qt.callLater(() => updateIndicator(indicator.initialSetupComplete))
|
||||
onWidthChanged: Qt.callLater(updateIndicator)
|
||||
}
|
||||
@@ -30,9 +30,9 @@ StyledRect {
|
||||
property color leftIconColor: Theme.surfaceVariantText
|
||||
property color leftIconFocusedColor: Theme.primary
|
||||
property bool showClearButton: false
|
||||
property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
property color backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
property color focusedBorderColor: Theme.primary
|
||||
property color normalBorderColor: Theme.outlineStrong
|
||||
property color normalBorderColor: Theme.outlineMedium
|
||||
property color placeholderColor: Theme.outlineButton
|
||||
property int borderWidth: 1
|
||||
property int focusedBorderWidth: 2
|
||||
|
||||
@@ -6,6 +6,8 @@ import qs.Common
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
WlrLayershell.namespace: "dms:tooltip"
|
||||
|
||||
property string text: ""
|
||||
property real targetX: 0
|
||||
property real targetY: 0
|
||||
|
||||
@@ -31,7 +31,6 @@ Item {
|
||||
antialiasing: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
layer.enabled: true
|
||||
layer.samples: 0
|
||||
|
||||
ShapePath {
|
||||
strokeColor: root.trackColor
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user