diff --git a/Shell/Windows/QuickSettings/QuickSettingsApp.qml b/Shell/Windows/QuickSettings/QuickSettingsApp.qml index 00cc9a7..a8e5ff1 100644 --- a/Shell/Windows/QuickSettings/QuickSettingsApp.qml +++ b/Shell/Windows/QuickSettings/QuickSettingsApp.qml @@ -1,5 +1,8 @@ import QtQuick +import Qt.labs.folderlistmodel +import Quickshell import Quickshell.Bluetooth +import Quickshell.Io import Quickshell.Services.Pipewire import "../.." @@ -16,6 +19,8 @@ Item { property int menuLeft: 64 property int menuTop: 140 property int lineHeight: 38 + 40 + 1 + property int visibleRows: Math.max(1, Math.floor((height - menuTop - 64) / lineHeight)) + property int scrollOffset: 0 property int nameFontSize: 32 property int stateFontSize: 28 @@ -30,9 +35,14 @@ Item { property ShellStateManager manager: null property int activeSelection: 0 property bool inBluetoothMenu: false + property bool inWallpaperMenu: false property bool isSelected: false + property string wallpapersDir: (Quickshell.env("HOME") || "") + "/Pictures/Wallpapers" + property string wallpaperCachePath: (Quickshell.env("HOME") || "") + "/.cache/.wallpaper" + property string currentWallpaperPath: "" + function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); } @@ -40,6 +50,8 @@ Item { function menuLength() { if (root.inBluetoothMenu && bluetoothRepeater) return bluetoothRepeater.count; + if (root.inWallpaperMenu && wallpaperFolderModel) + return wallpaperFolderModel.count; if (!menuModel) return 0; if (typeof menuModel.count === "function") @@ -69,7 +81,7 @@ Item { } function currentAction() { - if (root.inBluetoothMenu) + if (root.inBluetoothMenu || root.inWallpaperMenu) return null; return menuLength() > 0 ? menuAt(activeSelection) : null; } @@ -78,9 +90,26 @@ Item { const length = menuLength(); if (length === 0) { activeSelection = 0; + scrollOffset = 0; return; } activeSelection = clamp(activeSelection, 0, length - 1); + const maxOffset = Math.max(0, length - visibleRows); + scrollOffset = clamp(scrollOffset, 0, maxOffset); + } + + function ensureVisible() { + if (!root.inWallpaperMenu) + return; + const length = menuLength(); + if (length === 0) { + scrollOffset = 0; + return; + } + if (activeSelection < scrollOffset) + scrollOffset = activeSelection; + else if (activeSelection >= scrollOffset + visibleRows) + scrollOffset = Math.max(0, activeSelection - visibleRows + 1); } function bluetoothDisplayName(device) { @@ -105,11 +134,67 @@ Item { return item.device || null; } + function trimText(text) { + return (text || "").replace(/^\s+|\s+$/g, ""); + } + + function normalizedPath(path) { + var value = trimText(path); + if (value.indexOf("file://") === 0) + value = value.slice(7); + return value; + } + + function wallpaperNameAt(index) { + if (!wallpaperFolderModel || index < 0 || index >= wallpaperFolderModel.count) + return ""; + var name = wallpaperFolderModel.get(index, "fileName") || ""; + return name.replace(/\.[^/.]+$/, ""); + } + + function wallpaperPathAt(index) { + if (!wallpaperFolderModel || index < 0 || index >= wallpaperFolderModel.count) + return ""; + return wallpaperFolderModel.get(index, "filePath") || ""; + } + + function currentWallpaperIndex() { + var current = normalizedPath(currentWallpaperPath); + if (!current || !wallpaperFolderModel) + return -1; + for (var i = 0; i < wallpaperFolderModel.count; i++) { + if (normalizedPath(wallpaperPathAt(i)) === current) + return i; + } + return -1; + } + + function refreshCurrentWallpaper() { + root.currentWallpaperPath = normalizedPath(wallpaperFile.text()); + } + + function applyWallpaper(path) { + var normalized = normalizedPath(path); + if (!normalized || normalized.length === 0) + return; + + root.currentWallpaperPath = normalized; + wallpaperFile.setText(normalized + "\n"); + + wallpaperApplyProcess.running = false; + wallpaperApplyProcess.environment = { + "WALLPAPER_PATH": normalized + }; + wallpaperApplyProcess.command = ["bash", "-lc", "rm -rf \"$HOME/.cache/wal\"; if [[ \"$(hostname)\" != \"gentoo\" ]]; then swww img \"$WALLPAPER_PATH\" --transition-type none; else HYPRPAPER_PID=\"$(pidof hyprpaper)\"; if [ ${#HYPRPAPER_PID} -lt 1 ]; then hyprctl dispatch exec hyprpaper; sleep 1; fi; hyprctl hyprpaper unload all; hyprctl hyprpaper preload \"$WALLPAPER_PATH\"; hyprctl hyprpaper wallpaper ,\"$WALLPAPER_PATH\"; fi"]; + wallpaperApplyProcess.running = true; + } + PwObjectTracker { objects: [Pipewire.defaultAudioSink] } property int bluetoothActionIndex: 1 + property int wallpaperActionIndex: 2 property var actions: [ { name: "Master Volume", @@ -132,6 +217,7 @@ Item { name: "Bluetooth", ent: function () { root.inBluetoothMenu = true; + root.inWallpaperMenu = false; root.isSelected = false; root.activeSelection = 0; root.clampSelection(); @@ -139,10 +225,55 @@ Item { getState: function () { return ""; } + }, + { + name: "Wallpaper", + ent: function () { + root.inWallpaperMenu = true; + root.inBluetoothMenu = false; + root.isSelected = false; + var currentIndex = root.currentWallpaperIndex(); + root.activeSelection = currentIndex >= 0 ? currentIndex : 0; + root.scrollOffset = 0; + root.clampSelection(); + root.ensureVisible(); + }, + getState: function () { + return ""; + } } ] - property var menuModel: root.inBluetoothMenu ? Bluetooth.devices : actions + property var menuModel: root.inBluetoothMenu ? Bluetooth.devices : (root.inWallpaperMenu ? wallpaperFolderModel : actions) + + FolderListModel { + id: wallpaperFolderModel + folder: "file://" + root.wallpapersDir + nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.webp", "*.bmp", "*.gif"] + showDirs: false + showDotAndDotDot: false + sortField: FolderListModel.Name + sortReversed: false + onCountChanged: { + if (root.inWallpaperMenu) { + root.clampSelection(); + root.ensureVisible(); + } + } + } + + FileView { + id: wallpaperFile + path: root.wallpaperCachePath + watchChanges: true + onLoaded: root.refreshCurrentWallpaper() + onFileChanged: reload() + onLoadFailed: root.currentWallpaperPath = "" + } + + Process { + id: wallpaperApplyProcess + } Connections { target: Bluetooth.devices @@ -165,8 +296,31 @@ Item { } } + Connections { + target: wallpaperFolderModel + ignoreUnknownSignals: true + function onModelReset() { + if (root.inWallpaperMenu) { + root.clampSelection(); + root.ensureVisible(); + } + } + function onRowsInserted() { + if (root.inWallpaperMenu) { + root.clampSelection(); + root.ensureVisible(); + } + } + function onRowsRemoved() { + if (root.inWallpaperMenu) { + root.clampSelection(); + root.ensureVisible(); + } + } + } + Text { - text: root.inBluetoothMenu ? "BLUETOOTH" : "CONFIG" + text: root.inBluetoothMenu ? "BLUETOOTH" : (root.inWallpaperMenu ? "WALLPAPER" : "CONFIG") font.family: "8bitoperator JVE" font.pixelSize: 71 renderType: Text.NativeRendering @@ -186,7 +340,8 @@ Item { width: root.width height: lineHeight x: 0 - y: menuTop + index * lineHeight + y: menuTop + (root.inWallpaperMenu ? (index - root.scrollOffset) * lineHeight : index * lineHeight) + visible: !root.inWallpaperMenu || (index >= root.scrollOffset && index < root.scrollOffset + root.visibleRows) property var device: root.inBluetoothMenu ? modelData : null @@ -202,7 +357,8 @@ Item { Text { x: 239 y: 0 - text: root.inBluetoothMenu ? root.bluetoothDisplayName(modelData) : modelData.name + text: root.inBluetoothMenu ? root.bluetoothDisplayName(modelData) : (root.inWallpaperMenu ? root.wallpaperNameAt(index) : modelData.name) + width: root.width - 239 - (root.inWallpaperMenu ? 96 : 300) font.family: "8bitoperator JVE" font.pixelSize: 71 font.letterSpacing: 1 @@ -210,13 +366,16 @@ Item { font.hintingPreference: Font.PreferNoHinting smooth: false antialiasing: false - color: (root.activeSelection == index && root.isSelected == true) ? "#fefe00" : "#ffffff" + wrapMode: Text.NoWrap + elide: Text.ElideRight + color: (root.activeSelection == index && (root.isSelected == true || root.inBluetoothMenu || root.inWallpaperMenu)) ? "#fefe00" : "#ffffff" } // Option state Text { x: menuLeft + stateColumnX y: 4 + visible: !root.inWallpaperMenu text: root.inBluetoothMenu ? root.bluetoothDisplayState(modelData) : (modelData.getState ? modelData.getState() : "") font.family: "8bitoperator JVE" font.pixelSize: 71 @@ -225,7 +384,7 @@ Item { font.hintingPreference: Font.PreferNoHinting smooth: false antialiasing: false - color: (root.activeSelection == index && root.isSelected == true) ? "#fefe00" : "#ffffff" + color: (root.activeSelection == index && (root.isSelected == true || root.inBluetoothMenu || root.inWallpaperMenu)) ? "#fefe00" : "#ffffff" } } } @@ -237,12 +396,16 @@ Item { function handleKey(key) { switch (key) { case Qt.Key_Up: - if (root.inBluetoothMenu || root.isSelected === false) + if (root.inBluetoothMenu || root.inWallpaperMenu || root.isSelected === false) { activeSelection = wrapIndex(activeSelection - 1); + root.ensureVisible(); + } return true; case Qt.Key_Down: - if (root.inBluetoothMenu || root.isSelected === false) + if (root.inBluetoothMenu || root.inWallpaperMenu || root.isSelected === false) { activeSelection = wrapIndex(activeSelection + 1); + root.ensureVisible(); + } return true; case Qt.Key_Left: { @@ -273,6 +436,10 @@ Item { } // device.connected = !device.connected; } + } else if (root.inWallpaperMenu) { + const wallpaperPath = wallpaperPathAt(activeSelection); + if (wallpaperPath && wallpaperPath.length > 0) + root.applyWallpaper(wallpaperPath); } else { const a = currentAction(); if (a && a.ent) { @@ -286,7 +453,13 @@ Item { case Qt.Key_X: case Qt.Key_Shift: case Qt.Key_Escape: - if (root.inBluetoothMenu) { + if (root.inWallpaperMenu) { + root.inWallpaperMenu = false; + root.isSelected = false; + root.scrollOffset = 0; + root.activeSelection = root.wallpaperActionIndex; + root.clampSelection(); + } else if (root.inBluetoothMenu) { root.inBluetoothMenu = false; root.isSelected = false; root.activeSelection = root.bluetoothActionIndex; @@ -307,6 +480,9 @@ Item { root.activeSelection = 0; root.isSelected = false; root.inBluetoothMenu = false; + root.inWallpaperMenu = false; + root.scrollOffset = 0; + root.refreshCurrentWallpaper(); ShellInputManager.registerHandler("quickSettings", handleKey); }