From 28a6284968ddb28c30372eb86f8dabd6f7e04b5d Mon Sep 17 00:00:00 2001 From: sinnayuh Date: Fri, 15 Aug 2025 17:33:10 +0100 Subject: [PATCH 01/35] feat(wallpaper selector): add wallpaper selector menu --- .config/hypr/hyprland/keybinds.conf | 1 + .config/quickshell/ii/GlobalStates.qml | 1 + .../wallpaperOverview/WallpaperOverview.qml | 244 ++++++++++++++++++ .config/quickshell/ii/services/Wallpapers.qml | 76 ++++++ .config/quickshell/ii/shell.qml | 3 + 5 files changed, 325 insertions(+) create mode 100644 .config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml create mode 100644 .config/quickshell/ii/services/Wallpapers.qml diff --git a/.config/hypr/hyprland/keybinds.conf b/.config/hypr/hyprland/keybinds.conf index c7e953d94..9c9529a69 100644 --- a/.config/hypr/hyprland/keybinds.conf +++ b/.config/hypr/hyprland/keybinds.conf @@ -25,6 +25,7 @@ bindit = ,Super_R, global, quickshell:workspaceNumber # [hidden] bindd = Super, V, Clipboard history >> clipboard, global, quickshell:overviewClipboardToggle # Clipboard history >> clipboard bindd = Super, Period, Emoji >> clipboard, global, quickshell:overviewEmojiToggle # Emoji >> clipboard bindd = Super, Tab, Toggle overview, global, quickshell:overviewToggle # [hidden] Toggle overview/launcher (alt) +bindd = Super, Comma, Toggle wallpaper overview, global, quickshell:wallpaperOverviewToggle # Wallpaper overview bindd = Super, A, Toggle left sidebar, global, quickshell:sidebarLeftToggle # Toggle left sidebar bind = Super+Alt, A, global, quickshell:sidebarLeftToggleDetach # [hidden] bind = Super, B, global, quickshell:sidebarLeftToggle # [hidden] diff --git a/.config/quickshell/ii/GlobalStates.qml b/.config/quickshell/ii/GlobalStates.qml index 3f7468f4c..57ce3b4cc 100644 --- a/.config/quickshell/ii/GlobalStates.qml +++ b/.config/quickshell/ii/GlobalStates.qml @@ -17,6 +17,7 @@ Singleton { property bool osdVolumeOpen: false property bool oskOpen: false property bool overviewOpen: false + property bool wallpaperOverviewOpen: false property bool screenLocked: false property bool screenLockContainsCharacters: false property bool screenUnlockFailed: false diff --git a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml b/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml new file mode 100644 index 000000000..233ae9534 --- /dev/null +++ b/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml @@ -0,0 +1,244 @@ +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Hyprland + +// Fullscreen panel similar to Overview, but shows a grid of wallpaper previews. +Scope { + id: scope + + Variants { + id: variants + model: Quickshell.screens + + PanelWindow { + id: root + required property var modelData + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) + property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) + screen: modelData + visible: GlobalStates.wallpaperOverviewOpen && monitorIsFocused + + WlrLayershell.namespace: "quickshell:wallpaper-overview" + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + color: "transparent" + + anchors { top: true; bottom: true; left: true; right: true } + + ColumnLayout { + id: layout + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + spacing: 8 + + Item { width: 1; height: 1 } + + Rectangle { + id: bg + focus: true + color: Appearance.colors.colLayer0 + border.width: 1 + border.color: Appearance.colors.colLayer0Border + radius: Appearance.rounding.screenRounding + + // Compact size for 4 thumbnails per row, 3 rows high + implicitWidth: Math.min(root.width * 0.7, 900) + implicitHeight: Math.min(root.height * 0.6, 500) + + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Escape) { + GlobalStates.wallpaperOverviewOpen = false + event.accepted = true + } else if (event.key === Qt.Key_Left) { + grid.moveSelection(-1) + event.accepted = true + } else if (event.key === Qt.Key_Right) { + grid.moveSelection(1) + event.accepted = true + } else if (event.key === Qt.Key_Up) { + grid.moveSelection(-grid.columns) + event.accepted = true + } else if (event.key === Qt.Key_Down) { + grid.moveSelection(grid.columns) + event.accepted = true + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + grid.activateCurrent() + event.accepted = true + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 8 + + GridView { + id: grid + readonly property int columns: 4 // Fixed to 4 columns + property int currentIndex: 0 + readonly property int rows: Math.max(1, Math.ceil(count / columns)) + + Layout.preferredWidth: columns * cellWidth + Layout.alignment: Qt.AlignHCenter + Layout.fillHeight: true + cellWidth: 220 + cellHeight: 140 + clip: true + interactive: true + keyNavigationWraps: true + boundsBehavior: Flickable.StopAtBounds + + // Performance optimization: cache more delegates for smoother scrolling + cacheBuffer: cellHeight * 2 // Cache 2 extra rows above/below visible area + ScrollBar.horizontal: ScrollBar { + policy: ScrollBar.AsNeeded + visible: false + } + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + visible: false + } + // Back to simple wallpapers array + model: Wallpapers.wallpapers + onModelChanged: currentIndex = 0 + + function moveSelection(delta) { + // Clear all hover states when using keyboard navigation + for (let i = 0; i < count; i++) { + const item = itemAtIndex(i) + if (item) { + item.isHovered = false + } + } + currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)) + positionViewAtIndex(currentIndex, GridView.Contain) + } + function activateCurrent() { + const path = Wallpapers.wallpapers[currentIndex] + if (!path) return + GlobalStates.wallpaperOverviewOpen = false + Wallpapers.apply(path) + } + + delegate: Item { + width: grid.cellWidth + height: grid.cellHeight + property bool isHovered: false + + Rectangle { + anchors.fill: parent + radius: Appearance.rounding.windowRounding + color: Appearance.colors.colLayer1 + border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 + border.color: Appearance.colors.colSecondary + } + + Rectangle { + anchors.fill: parent + anchors.margins: 8 + color: Appearance.colors.colLayer2 + radius: Appearance.rounding.elementRounding + + // Loading placeholder + Rectangle { + anchors.centerIn: parent + width: Math.min(parent.width * 0.4, 32) + height: Math.min(parent.height * 0.4, 32) + radius: Appearance.rounding.elementRounding + color: Appearance.colors.colLayer3 + visible: thumbnailImage.status !== Image.Ready + + // Simple loading animation + opacity: 0.3 + SequentialAnimation on opacity { + running: parent.visible + loops: Animation.Infinite + NumberAnimation { to: 1.0; duration: 800; easing.type: Easing.InOutSine } + NumberAnimation { to: 0.3; duration: 800; easing.type: Easing.InOutSine } + } + } + + Image { + id: thumbnailImage + anchors.fill: parent + source: `file://${modelData}` + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: false + smooth: true + + // Much smaller sourceSize for faster loading - this is key! + // Using smaller dimensions significantly reduces decode time + sourceSize.width: Math.min(128, grid.cellWidth - 16) + sourceSize.height: Math.min(96, grid.cellHeight - 16) + + // Disable mipmap for faster loading (quality vs speed tradeoff) + mipmap: false + + // Smooth fade-in when ready + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + // Clear all other hover states and set current index + for (let i = 0; i < grid.count; i++) { + const item = grid.itemAtIndex(i) + if (item && item !== parent) { + item.isHovered = false + } + } + parent.isHovered = true + grid.currentIndex = index + } + onExited: { + parent.isHovered = false + } + onClicked: { + GlobalStates.wallpaperOverviewOpen = false + Wallpapers.apply(modelData) + } + } + } + } + } + } + } + + Connections { + target: GlobalStates + function onWallpaperOverviewOpenChanged() { + if (GlobalStates.wallpaperOverviewOpen && monitorIsFocused) { + bg.forceActiveFocus(); + } + } + } + } + } + + GlobalShortcut { + name: "wallpaperOverviewToggle" + description: "Toggle wallpaper overview" + onPressed: { GlobalStates.wallpaperOverviewOpen = !GlobalStates.wallpaperOverviewOpen } + } +} + + diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml new file mode 100644 index 000000000..d2db55141 --- /dev/null +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -0,0 +1,76 @@ +import qs.modules.common +import qs.modules.common.functions +import QtQuick +import Qt.labs.folderlistmodel +import Quickshell +import Quickshell.Io +pragma Singleton +pragma ComponentBehavior: Bound + +/** + * Provides a list of wallpapers and an "apply" action that calls the existing + * switchwall.sh script. Uses QML's built-in image scaling for thumbnails. + */ +Singleton { + id: root + + // Directory to search for wallpapers (new location) + // Resolves to: ~/Pictures/wallpapers + property list searchDirs: [ FileUtils.trimFileProtocol(`${Directories.pictures}/wallpapers`) ] + + // Supported image extensions. Videos are intentionally excluded for now + // to keep the overview lightweight. + readonly property list extensions: [ + "jpg", "jpeg", "png", "webp", "avif", "bmp" + ] + + // Resulting list of absolute file paths (without file:// prefix) + property list wallpapers: [] + + // Public API (FolderListModel driven) + function reload() { + files.folder = `file://${root.searchDirs[0]}` + } + onSearchDirsChanged: reload() + + function apply(path) { + if (!path || path.length === 0) return + applyProc.command = [ + "bash", "-c", + `${StringUtils.shellSingleQuoteEscape(Directories.wallpaperSwitchScriptPath)} ` + + `--image ${StringUtils.shellSingleQuoteEscape(path)}` + ] + applyProc.running = true + } + + // Folder model + FolderListModel { + id: files + nameFilters: extensions.map(ext => `*.${ext}`) + showDirs: false + showDotAndDotDot: false + showOnlyReadable: true + sortField: FolderListModel.Time + sortReversed: true + onCountChanged: { + console.log(`[Wallpapers] FolderListModel count=${files.count} folder=${files.folder}`) + root.wallpapers = [] + for (let i = 0; i < files.count; i++) { + const path = files.get(i, "filePath") || FileUtils.trimFileProtocol(files.get(i, "fileURL")) + if (path && path.length) root.wallpapers.push(path) + } + } + } + + // Expose the model for direct binding when needed + property alias filesModel: files + + // Internal: applying a wallpaper + Process { + id: applyProc + } + + Component.onCompleted: reload() +} + + diff --git a/.config/quickshell/ii/shell.qml b/.config/quickshell/ii/shell.qml index 8186d1a56..45532738c 100644 --- a/.config/quickshell/ii/shell.qml +++ b/.config/quickshell/ii/shell.qml @@ -18,6 +18,7 @@ import "./modules/notificationPopup/" import "./modules/onScreenDisplay/" import "./modules/onScreenKeyboard/" import "./modules/overview/" +import "./modules/wallpaperOverview/" import "./modules/screenCorners/" import "./modules/session/" import "./modules/sidebarLeft/" @@ -43,6 +44,7 @@ ShellRoot { property bool enableOnScreenDisplayVolume: true property bool enableOnScreenKeyboard: true property bool enableOverview: true + property bool enableWallpaperOverview: true property bool enableReloadPopup: true property bool enableScreenCorners: true property bool enableSession: true @@ -69,6 +71,7 @@ ShellRoot { LazyLoader { active: enableOnScreenDisplayVolume; component: OnScreenDisplayVolume {} } LazyLoader { active: enableOnScreenKeyboard; component: OnScreenKeyboard {} } LazyLoader { active: enableOverview; component: Overview {} } + LazyLoader { active: enableWallpaperOverview; component: WallpaperOverview {} } LazyLoader { active: enableReloadPopup; component: ReloadPopup {} } LazyLoader { active: enableScreenCorners; component: ScreenCorners {} } LazyLoader { active: enableSession; component: Session {} } From 190dbff98d70536e25f0819ab3e2056183847206 Mon Sep 17 00:00:00 2001 From: sin <92541374+sinnayuh@users.noreply.github.com> Date: Sun, 17 Aug 2025 10:51:51 +0000 Subject: [PATCH 02/35] feat(wallpaper selector): add search functionality --- .../wallpaperOverview/WallpaperOverview.qml | 328 ++++++++++++++---- 1 file changed, 257 insertions(+), 71 deletions(-) diff --git a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml b/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml index 233ae9534..ef1fa2d03 100644 --- a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml +++ b/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml @@ -11,7 +11,6 @@ import Quickshell.Io import Quickshell.Wayland import Quickshell.Hyprland -// Fullscreen panel similar to Overview, but shows a grid of wallpaper previews. Scope { id: scope @@ -19,20 +18,26 @@ Scope { id: variants model: Quickshell.screens - PanelWindow { - id: root - required property var modelData - readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) - property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) - screen: modelData - visible: GlobalStates.wallpaperOverviewOpen && monitorIsFocused + PanelWindow { + id: root + required property var modelData + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) + property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) + screen: modelData + visible: GlobalStates.wallpaperOverviewOpen && monitorIsFocused + property var filteredWallpapers: Wallpapers.wallpapers WlrLayershell.namespace: "quickshell:wallpaper-overview" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand color: "transparent" - anchors { top: true; bottom: true; left: true; right: true } + anchors { + top: true + bottom: true + left: true + right: true + } ColumnLayout { id: layout @@ -40,7 +45,76 @@ Scope { anchors.top: parent.top spacing: 8 - Item { width: 1; height: 1 } + Item { + width: 1 + height: 1 + } + + TextField { + id: filterField + Layout.preferredWidth: bg.implicitWidth + Layout.alignment: Qt.AlignHcenter + implicitHeight: 40 + padding: 10 + placeholderText: "Search wallpapers..." + placeholderTextColor: Appearance.colors.colSubtext + color: Appearance.colors.colPrimary + background: Rectangle { + color: Appearance.colors.colLayer0 + border.color: Appearance.colors.colLayer0Border + border.width: 1 + radius: Appearance.rounding.small + } + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal + + onTextChanged: { + let newModel = []; + if (text.length > 0) { + for (let i = 0; i < Wallpapers.wallpapers.length; ++i) { + let wallpaperPath = Wallpapers.wallpapers[i]; + if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) { + newModel.push(wallpaperPath); + } + } + root.filteredWallpapers = newModel; + } else { + root.filteredWallpapers = Wallpapers.wallpapers; + } + } + + Keys.onPressed: event => { + if (text.length === 0) { + if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { + bg.forceActiveFocus(); + if (event.key === Qt.Key_Down) + grid.moveSelection(grid.columns); + else if (event.key === Qt.Key_Left) + grid.moveSelection(-1); + else if (event.key === Qt.Key_Right) + grid.moveSelection(1); + event.accepted = true; + } + } else { + if (event.key === Qt.Key_Down) { + grid.moveSelection(grid.columns); + event.accepted = true; + bg.forceActiveFocus(); + } + } + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + grid.activateCurrent(); + event.accepted = true; + } else if (event.key === Qt.Key_Escape) { + if (filterField.text.length > 0) { + filterField.text = ""; + } else { + GlobalStates.wallpaperOverviewOpen = false; + } + event.accepted = true; + } + } + } Rectangle { id: bg @@ -50,29 +124,76 @@ Scope { border.color: Appearance.colors.colLayer0Border radius: Appearance.rounding.screenRounding - // Compact size for 4 thumbnails per row, 3 rows high - implicitWidth: Math.min(root.width * 0.7, 900) - implicitHeight: Math.min(root.height * 0.6, 500) + property int calculatedRows: Math.ceil(grid.count / grid.columns) - Keys.onPressed: (event) => { + implicitWidth: { + if (root.filteredWallpapers.length === 0) { + return 300; + } else if (root.filteredWallpapers.length < grid.columns) { + return root.filteredWallpapers.length * grid.cellWidth + 16; + } else { + return Math.min(root.width * 0.7, 900); + } + } + + implicitHeight: { + if (root.filteredWallpapers.length === 0) { + return 100; + } else { + return Math.min(root.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); + } + } + + Behavior on implicitWidth { + SpringAnimation { + spring: 3 + damping: 0.2 + } + } + + Behavior on implicitHeight { + SpringAnimation { + spring: 3 + damping: 0.2 + } + } + + Keys.onPressed: event => { if (event.key === Qt.Key_Escape) { - GlobalStates.wallpaperOverviewOpen = false - event.accepted = true + GlobalStates.wallpaperOverviewOpen = false; + event.accepted = true; } else if (event.key === Qt.Key_Left) { - grid.moveSelection(-1) - event.accepted = true + grid.moveSelection(-1); + event.accepted = true; } else if (event.key === Qt.Key_Right) { - grid.moveSelection(1) - event.accepted = true + grid.moveSelection(1); + event.accepted = true; } else if (event.key === Qt.Key_Up) { - grid.moveSelection(-grid.columns) - event.accepted = true + if (grid.currentIndex < grid.columns) { + filterField.forceActiveFocus(); + } else { + grid.moveSelection(-grid.columns); + } + event.accepted = true; } else if (event.key === Qt.Key_Down) { - grid.moveSelection(grid.columns) - event.accepted = true + grid.moveSelection(grid.columns); + event.accepted = true; } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - grid.activateCurrent() - event.accepted = true + grid.activateCurrent(); + event.accepted = true; + } else if (event.key === Qt.Key_Backspace) { + if (filterField.text.length > 0) { + filterField.text = filterField.text.substring(0, filterField.text.length - 1); + } + filterField.forceActiveFocus(); + event.accepted = true; + } else { + filterField.forceActiveFocus(); + if (event.text.length > 0) { + filterField.text += event.text; + filterField.cursorPosition = filterField.text.length; + } + event.accepted = true; } } @@ -83,12 +204,14 @@ Scope { GridView { id: grid - readonly property int columns: 4 // Fixed to 4 columns + visible: root.filteredWallpapers.length > 0 + + readonly property int columns: 4 property int currentIndex: 0 readonly property int rows: Math.max(1, Math.ceil(count / columns)) Layout.preferredWidth: columns * cellWidth - Layout.alignment: Qt.AlignHCenter + Layout.alignment: Qt.AlignHcenter Layout.fillHeight: true cellWidth: 220 cellHeight: 140 @@ -96,37 +219,37 @@ Scope { interactive: true keyNavigationWraps: true boundsBehavior: Flickable.StopAtBounds - - // Performance optimization: cache more delegates for smoother scrolling - cacheBuffer: cellHeight * 2 // Cache 2 extra rows above/below visible area - ScrollBar.horizontal: ScrollBar { + + cacheBuffer: cellHeight * 2 + ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AsNeeded visible: false } - ScrollBar.vertical: ScrollBar { + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded visible: false } - // Back to simple wallpapers array - model: Wallpapers.wallpapers + + model: root.filteredWallpapers onModelChanged: currentIndex = 0 function moveSelection(delta) { - // Clear all hover states when using keyboard navigation for (let i = 0; i < count; i++) { - const item = itemAtIndex(i) + const item = itemAtIndex(i); if (item) { - item.isHovered = false + item.isHovered = false; } } - currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)) - positionViewAtIndex(currentIndex, GridView.Contain) + currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); + positionViewAtIndex(currentIndex, GridView.Contain); } function activateCurrent() { - const path = Wallpapers.wallpapers[currentIndex] - if (!path) return - GlobalStates.wallpaperOverviewOpen = false - Wallpapers.apply(path) + const path = model[currentIndex]; + if (!path) + return; + GlobalStates.wallpaperOverviewOpen = false; + filterField.text = ""; + Wallpapers.apply(path); } delegate: Item { @@ -134,6 +257,20 @@ Scope { height: grid.cellHeight property bool isHovered: false + Behavior on width { + NumberAnimation { + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + } + + Behavior on height { + NumberAnimation { + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + } + Rectangle { anchors.fill: parent radius: Appearance.rounding.windowRounding @@ -147,8 +284,7 @@ Scope { anchors.margins: 8 color: Appearance.colors.colLayer2 radius: Appearance.rounding.elementRounding - - // Loading placeholder + Rectangle { anchors.centerIn: parent width: Math.min(parent.width * 0.4, 32) @@ -156,14 +292,21 @@ Scope { radius: Appearance.rounding.elementRounding color: Appearance.colors.colLayer3 visible: thumbnailImage.status !== Image.Ready - - // Simple loading animation + opacity: 0.3 SequentialAnimation on opacity { running: parent.visible loops: Animation.Infinite - NumberAnimation { to: 1.0; duration: 800; easing.type: Easing.InOutSine } - NumberAnimation { to: 0.3; duration: 800; easing.type: Easing.InOutSine } + NumberAnimation { + to: 1.0 + duration: 800 + easing.type: Easing.InOutSine + } + NumberAnimation { + to: 0.3 + duration: 800 + easing.type: Easing.InOutSine + } } } @@ -175,21 +318,17 @@ Scope { asynchronous: true cache: false smooth: true - - // Much smaller sourceSize for faster loading - this is key! - // Using smaller dimensions significantly reduces decode time + sourceSize.width: Math.min(128, grid.cellWidth - 16) sourceSize.height: Math.min(96, grid.cellHeight - 16) - - // Disable mipmap for faster loading (quality vs speed tradeoff) + mipmap: false - - // Smooth fade-in when ready + opacity: status === Image.Ready ? 1 : 0 Behavior on opacity { - NumberAnimation { + NumberAnimation { duration: 200 - easing.type: Easing.OutCubic + easing.type: Easing.OutCubic } } } @@ -199,25 +338,72 @@ Scope { anchors.fill: parent hoverEnabled: true onEntered: { - // Clear all other hover states and set current index for (let i = 0; i < grid.count; i++) { - const item = grid.itemAtIndex(i) + const item = grid.itemAtIndex(i); if (item && item !== parent) { - item.isHovered = false + item.isHovered = false; } } - parent.isHovered = true - grid.currentIndex = index + parent.isHovered = true; + grid.currentIndex = index; } onExited: { - parent.isHovered = false + parent.isHovered = false; } onClicked: { - GlobalStates.wallpaperOverviewOpen = false - Wallpapers.apply(modelData) + GlobalStates.wallpaperOverviewOpen = false; + filterField.text = ""; + Wallpapers.apply(modelData); } } } + + add: Transition { + from: "*" + to: "*" + ParallelAnimation { + PropertyAnimation { + property: "x" + from: grid.contentX + (grid.width / 2) - width / 2 + } + PropertyAnimation { + property: "y" + from: grid.contentY + (grid.height / 2) - height / 2 + } + NumberAnimation { + property: "scale" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + NumberAnimation { + property: "opacity" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + } + } + } + // show when no wallpaper found + ColumnLayout { + id: noWallpapersFoundLayout + visible: root.filteredWallpapers.length === 0 + anchors.centerIn: parent + + implicitHeight: noWallpapersFoundLabel.implicitHeight + implicitWidth: noWallpapersFoundLabel.implicitWidth + + Label { + id: noWallpapersFoundLabel + text: "No wallpapers found" + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colSubtext + Layout.alignment: Qt.AlignHcenter | Qt.AlignVCenter + } } } } @@ -227,7 +413,7 @@ Scope { target: GlobalStates function onWallpaperOverviewOpenChanged() { if (GlobalStates.wallpaperOverviewOpen && monitorIsFocused) { - bg.forceActiveFocus(); + filterField.forceActiveFocus(); } } } @@ -237,8 +423,8 @@ Scope { GlobalShortcut { name: "wallpaperOverviewToggle" description: "Toggle wallpaper overview" - onPressed: { GlobalStates.wallpaperOverviewOpen = !GlobalStates.wallpaperOverviewOpen } + onPressed: { + GlobalStates.wallpaperOverviewOpen = !GlobalStates.wallpaperOverviewOpen; + } } } - - From 5b1124d65899e787e850818e39994785c03a9996 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 17 Aug 2025 21:50:12 +0700 Subject: [PATCH 03/35] correct wallpaper path capitalization --- .config/quickshell/ii/services/Wallpapers.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index d2db55141..aeead2795 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -15,8 +15,8 @@ Singleton { id: root // Directory to search for wallpapers (new location) - // Resolves to: ~/Pictures/wallpapers - property list searchDirs: [ FileUtils.trimFileProtocol(`${Directories.pictures}/wallpapers`) ] + // Resolves to: ~/Pictures/Wallpapers + property list searchDirs: [ FileUtils.trimFileProtocol(`${Directories.pictures}/Wallpapers`) ] // Supported image extensions. Videos are intentionally excluded for now // to keep the overview lightweight. From de6ba1c82e1db5d2279257ba123d36b1f6ef3e1e Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 17 Aug 2025 22:54:40 +0700 Subject: [PATCH 04/35] wallpapers: add svg format --- .config/quickshell/ii/services/Wallpapers.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index aeead2795..e7d244426 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -21,7 +21,7 @@ Singleton { // Supported image extensions. Videos are intentionally excluded for now // to keep the overview lightweight. readonly property list extensions: [ - "jpg", "jpeg", "png", "webp", "avif", "bmp" + "jpg", "jpeg", "png", "webp", "avif", "bmp", "svg" ] // Resulting list of absolute file paths (without file:// prefix) From b01b52c8f86f55c642c822171d6fa455c15a4ed5 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 18:24:41 +0700 Subject: [PATCH 05/35] wallpapers: show newer ones first --- .config/quickshell/ii/services/Wallpapers.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index e7d244426..4683bed99 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -51,7 +51,7 @@ Singleton { showDotAndDotDot: false showOnlyReadable: true sortField: FolderListModel.Time - sortReversed: true + sortReversed: false onCountChanged: { console.log(`[Wallpapers] FolderListModel count=${files.count} folder=${files.folder}`) root.wallpapers = [] From fe23017a97faec7d5cf44b4f65d64bd67c71fb0e Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 18:25:23 +0700 Subject: [PATCH 06/35] WallpaperOverview: remove unnecessary Variants and Item --- .../wallpaperOverview/WallpaperOverview.qml | 752 +++++++++--------- 1 file changed, 370 insertions(+), 382 deletions(-) diff --git a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml b/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml index ef1fa2d03..7ca666b18 100644 --- a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml +++ b/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml @@ -14,408 +14,396 @@ import Quickshell.Hyprland Scope { id: scope - Variants { - id: variants - model: Quickshell.screens + PanelWindow { + id: root + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) + property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) + visible: GlobalStates.wallpaperOverviewOpen + property var filteredWallpapers: Wallpapers.wallpapers - PanelWindow { - id: root - required property var modelData - readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) - property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) - screen: modelData - visible: GlobalStates.wallpaperOverviewOpen && monitorIsFocused - property var filteredWallpapers: Wallpapers.wallpapers + WlrLayershell.namespace: "quickshell:wallpaper-overview" + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + color: "transparent" - WlrLayershell.namespace: "quickshell:wallpaper-overview" - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - color: "transparent" + anchors { + top: true + bottom: true + left: true + right: true + } - anchors { - top: true - bottom: true - left: true - right: true - } + ColumnLayout { + id: layout + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + spacing: 8 - ColumnLayout { - id: layout - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - spacing: 8 - - Item { - width: 1 - height: 1 - } - - TextField { - id: filterField - Layout.preferredWidth: bg.implicitWidth - Layout.alignment: Qt.AlignHcenter - implicitHeight: 40 - padding: 10 - placeholderText: "Search wallpapers..." - placeholderTextColor: Appearance.colors.colSubtext - color: Appearance.colors.colPrimary - background: Rectangle { - color: Appearance.colors.colLayer0 - border.color: Appearance.colors.colLayer0Border - border.width: 1 - radius: Appearance.rounding.small - } - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - - onTextChanged: { - let newModel = []; - if (text.length > 0) { - for (let i = 0; i < Wallpapers.wallpapers.length; ++i) { - let wallpaperPath = Wallpapers.wallpapers[i]; - if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) { - newModel.push(wallpaperPath); - } - } - root.filteredWallpapers = newModel; - } else { - root.filteredWallpapers = Wallpapers.wallpapers; - } - } - - Keys.onPressed: event => { - if (text.length === 0) { - if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { - bg.forceActiveFocus(); - if (event.key === Qt.Key_Down) - grid.moveSelection(grid.columns); - else if (event.key === Qt.Key_Left) - grid.moveSelection(-1); - else if (event.key === Qt.Key_Right) - grid.moveSelection(1); - event.accepted = true; - } - } else { - if (event.key === Qt.Key_Down) { - grid.moveSelection(grid.columns); - event.accepted = true; - bg.forceActiveFocus(); - } - } - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - grid.activateCurrent(); - event.accepted = true; - } else if (event.key === Qt.Key_Escape) { - if (filterField.text.length > 0) { - filterField.text = ""; - } else { - GlobalStates.wallpaperOverviewOpen = false; - } - event.accepted = true; - } - } - } - - Rectangle { - id: bg - focus: true + TextField { + id: filterField + Layout.preferredWidth: bg.implicitWidth + Layout.alignment: Qt.AlignHcenter + implicitHeight: 40 + padding: 10 + placeholderText: "Search wallpapers..." + placeholderTextColor: Appearance.colors.colSubtext + color: Appearance.colors.colPrimary + background: Rectangle { color: Appearance.colors.colLayer0 - border.width: 1 border.color: Appearance.colors.colLayer0Border - radius: Appearance.rounding.screenRounding + border.width: 1 + radius: Appearance.rounding.small + } + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal - property int calculatedRows: Math.ceil(grid.count / grid.columns) - - implicitWidth: { - if (root.filteredWallpapers.length === 0) { - return 300; - } else if (root.filteredWallpapers.length < grid.columns) { - return root.filteredWallpapers.length * grid.cellWidth + 16; - } else { - return Math.min(root.width * 0.7, 900); - } - } - - implicitHeight: { - if (root.filteredWallpapers.length === 0) { - return 100; - } else { - return Math.min(root.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); - } - } - - Behavior on implicitWidth { - SpringAnimation { - spring: 3 - damping: 0.2 - } - } - - Behavior on implicitHeight { - SpringAnimation { - spring: 3 - damping: 0.2 - } - } - - Keys.onPressed: event => { - if (event.key === Qt.Key_Escape) { - GlobalStates.wallpaperOverviewOpen = false; - event.accepted = true; - } else if (event.key === Qt.Key_Left) { - grid.moveSelection(-1); - event.accepted = true; - } else if (event.key === Qt.Key_Right) { - grid.moveSelection(1); - event.accepted = true; - } else if (event.key === Qt.Key_Up) { - if (grid.currentIndex < grid.columns) { - filterField.forceActiveFocus(); - } else { - grid.moveSelection(-grid.columns); + onTextChanged: { + let newModel = []; + if (text.length > 0) { + for (let i = 0; i < Wallpapers.wallpapers.length; ++i) { + let wallpaperPath = Wallpapers.wallpapers[i]; + if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) { + newModel.push(wallpaperPath); } + } + root.filteredWallpapers = newModel; + } else { + root.filteredWallpapers = Wallpapers.wallpapers; + } + } + + Keys.onPressed: event => { + if (text.length === 0) { + if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { + bg.forceActiveFocus(); + if (event.key === Qt.Key_Down) + grid.moveSelection(grid.columns); + else if (event.key === Qt.Key_Left) + grid.moveSelection(-1); + else if (event.key === Qt.Key_Right) + grid.moveSelection(1); event.accepted = true; - } else if (event.key === Qt.Key_Down) { + } + } else { + if (event.key === Qt.Key_Down) { grid.moveSelection(grid.columns); event.accepted = true; - } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - grid.activateCurrent(); - event.accepted = true; - } else if (event.key === Qt.Key_Backspace) { - if (filterField.text.length > 0) { - filterField.text = filterField.text.substring(0, filterField.text.length - 1); - } - filterField.forceActiveFocus(); - event.accepted = true; - } else { - filterField.forceActiveFocus(); - if (event.text.length > 0) { - filterField.text += event.text; - filterField.cursorPosition = filterField.text.length; - } - event.accepted = true; + bg.forceActiveFocus(); } } - - ColumnLayout { - anchors.fill: parent - anchors.margins: 8 - spacing: 8 - - GridView { - id: grid - visible: root.filteredWallpapers.length > 0 - - readonly property int columns: 4 - property int currentIndex: 0 - readonly property int rows: Math.max(1, Math.ceil(count / columns)) - - Layout.preferredWidth: columns * cellWidth - Layout.alignment: Qt.AlignHcenter - Layout.fillHeight: true - cellWidth: 220 - cellHeight: 140 - clip: true - interactive: true - keyNavigationWraps: true - boundsBehavior: Flickable.StopAtBounds - - cacheBuffer: cellHeight * 2 - ScrollBar.horizontal: ScrollBar { - policy: ScrollBar.AsNeeded - visible: false - } - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded - visible: false - } - - model: root.filteredWallpapers - onModelChanged: currentIndex = 0 - - function moveSelection(delta) { - for (let i = 0; i < count; i++) { - const item = itemAtIndex(i); - if (item) { - item.isHovered = false; - } - } - currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); - positionViewAtIndex(currentIndex, GridView.Contain); - } - function activateCurrent() { - const path = model[currentIndex]; - if (!path) - return; - GlobalStates.wallpaperOverviewOpen = false; - filterField.text = ""; - Wallpapers.apply(path); - } - - delegate: Item { - width: grid.cellWidth - height: grid.cellHeight - property bool isHovered: false - - Behavior on width { - NumberAnimation { - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } - } - - Behavior on height { - NumberAnimation { - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } - } - - Rectangle { - anchors.fill: parent - radius: Appearance.rounding.windowRounding - color: Appearance.colors.colLayer1 - border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 - border.color: Appearance.colors.colSecondary - } - - Rectangle { - anchors.fill: parent - anchors.margins: 8 - color: Appearance.colors.colLayer2 - radius: Appearance.rounding.elementRounding - - Rectangle { - anchors.centerIn: parent - width: Math.min(parent.width * 0.4, 32) - height: Math.min(parent.height * 0.4, 32) - radius: Appearance.rounding.elementRounding - color: Appearance.colors.colLayer3 - visible: thumbnailImage.status !== Image.Ready - - opacity: 0.3 - SequentialAnimation on opacity { - running: parent.visible - loops: Animation.Infinite - NumberAnimation { - to: 1.0 - duration: 800 - easing.type: Easing.InOutSine - } - NumberAnimation { - to: 0.3 - duration: 800 - easing.type: Easing.InOutSine - } - } - } - - Image { - id: thumbnailImage - anchors.fill: parent - source: `file://${modelData}` - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: false - smooth: true - - sourceSize.width: Math.min(128, grid.cellWidth - 16) - sourceSize.height: Math.min(96, grid.cellHeight - 16) - - mipmap: false - - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { - NumberAnimation { - duration: 200 - easing.type: Easing.OutCubic - } - } - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - for (let i = 0; i < grid.count; i++) { - const item = grid.itemAtIndex(i); - if (item && item !== parent) { - item.isHovered = false; - } - } - parent.isHovered = true; - grid.currentIndex = index; - } - onExited: { - parent.isHovered = false; - } - onClicked: { - GlobalStates.wallpaperOverviewOpen = false; - filterField.text = ""; - Wallpapers.apply(modelData); - } - } - } - - add: Transition { - from: "*" - to: "*" - ParallelAnimation { - PropertyAnimation { - property: "x" - from: grid.contentX + (grid.width / 2) - width / 2 - } - PropertyAnimation { - property: "y" - from: grid.contentY + (grid.height / 2) - height / 2 - } - NumberAnimation { - property: "scale" - from: 0.0 - to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } - NumberAnimation { - property: "opacity" - from: 0.0 - to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } - } - } - } - // show when no wallpaper found - ColumnLayout { - id: noWallpapersFoundLayout - visible: root.filteredWallpapers.length === 0 - anchors.centerIn: parent - - implicitHeight: noWallpapersFoundLabel.implicitHeight - implicitWidth: noWallpapersFoundLabel.implicitWidth - - Label { - id: noWallpapersFoundLabel - text: "No wallpapers found" - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - color: Appearance.colors.colSubtext - Layout.alignment: Qt.AlignHcenter | Qt.AlignVCenter - } + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + grid.activateCurrent(); + event.accepted = true; + } else if (event.key === Qt.Key_Escape) { + if (filterField.text.length > 0) { + filterField.text = ""; + } else { + GlobalStates.wallpaperOverviewOpen = false; } + event.accepted = true; } } } - Connections { - target: GlobalStates - function onWallpaperOverviewOpenChanged() { - if (GlobalStates.wallpaperOverviewOpen && monitorIsFocused) { - filterField.forceActiveFocus(); + Rectangle { + id: bg + focus: true + color: Appearance.colors.colLayer0 + border.width: 1 + border.color: Appearance.colors.colLayer0Border + radius: Appearance.rounding.screenRounding + + property int calculatedRows: Math.ceil(grid.count / grid.columns) + + implicitWidth: { + if (root.filteredWallpapers.length === 0) { + return 300; + } else if (root.filteredWallpapers.length < grid.columns) { + return root.filteredWallpapers.length * grid.cellWidth + 16; + } else { + return Math.min(root.width * 0.7, 900); } } + + implicitHeight: { + if (root.filteredWallpapers.length === 0) { + return 100; + } else { + return Math.min(root.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); + } + } + + Behavior on implicitWidth { + SpringAnimation { + spring: 3 + damping: 0.2 + } + } + + Behavior on implicitHeight { + SpringAnimation { + spring: 3 + damping: 0.2 + } + } + + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + GlobalStates.wallpaperOverviewOpen = false; + event.accepted = true; + } else if (event.key === Qt.Key_Left) { + grid.moveSelection(-1); + event.accepted = true; + } else if (event.key === Qt.Key_Right) { + grid.moveSelection(1); + event.accepted = true; + } else if (event.key === Qt.Key_Up) { + if (grid.currentIndex < grid.columns) { + filterField.forceActiveFocus(); + } else { + grid.moveSelection(-grid.columns); + } + event.accepted = true; + } else if (event.key === Qt.Key_Down) { + grid.moveSelection(grid.columns); + event.accepted = true; + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + grid.activateCurrent(); + event.accepted = true; + } else if (event.key === Qt.Key_Backspace) { + if (filterField.text.length > 0) { + filterField.text = filterField.text.substring(0, filterField.text.length - 1); + } + filterField.forceActiveFocus(); + event.accepted = true; + } else { + filterField.forceActiveFocus(); + if (event.text.length > 0) { + filterField.text += event.text; + filterField.cursorPosition = filterField.text.length; + } + event.accepted = true; + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 8 + + GridView { + id: grid + visible: root.filteredWallpapers.length > 0 + + readonly property int columns: 4 + property int currentIndex: 0 + readonly property int rows: Math.max(1, Math.ceil(count / columns)) + + Layout.preferredWidth: columns * cellWidth + Layout.alignment: Qt.AlignHcenter + Layout.fillHeight: true + cellWidth: 220 + cellHeight: 140 + clip: true + interactive: true + keyNavigationWraps: true + boundsBehavior: Flickable.StopAtBounds + + cacheBuffer: cellHeight * 2 + ScrollBar.horizontal: ScrollBar { + policy: ScrollBar.AsNeeded + visible: false + } + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + visible: false + } + + model: root.filteredWallpapers + onModelChanged: currentIndex = 0 + + function moveSelection(delta) { + for (let i = 0; i < count; i++) { + const item = itemAtIndex(i); + if (item) { + item.isHovered = false; + } + } + currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); + positionViewAtIndex(currentIndex, GridView.Contain); + } + function activateCurrent() { + const path = model[currentIndex]; + if (!path) + return; + GlobalStates.wallpaperOverviewOpen = false; + filterField.text = ""; + Wallpapers.apply(path); + } + + delegate: Item { + width: grid.cellWidth + height: grid.cellHeight + property bool isHovered: false + + Behavior on width { + NumberAnimation { + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + } + + Behavior on height { + NumberAnimation { + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + } + + Rectangle { + anchors.fill: parent + radius: Appearance.rounding.windowRounding + color: Appearance.colors.colLayer1 + border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 + border.color: Appearance.colors.colSecondary + } + + Rectangle { + anchors.fill: parent + anchors.margins: 8 + color: Appearance.colors.colLayer2 + radius: Appearance.rounding.elementRounding + + Rectangle { + anchors.centerIn: parent + width: Math.min(parent.width * 0.4, 32) + height: Math.min(parent.height * 0.4, 32) + radius: Appearance.rounding.elementRounding + color: Appearance.colors.colLayer3 + visible: thumbnailImage.status !== Image.Ready + + opacity: 0.3 + SequentialAnimation on opacity { + running: parent.visible + loops: Animation.Infinite + NumberAnimation { + to: 1.0 + duration: 800 + easing.type: Easing.InOutSine + } + NumberAnimation { + to: 0.3 + duration: 800 + easing.type: Easing.InOutSine + } + } + } + + Image { + id: thumbnailImage + anchors.fill: parent + source: `file://${modelData}` + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: false + smooth: true + + sourceSize.width: Math.min(128, grid.cellWidth - 16) + sourceSize.height: Math.min(96, grid.cellHeight - 16) + + mipmap: false + + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + for (let i = 0; i < grid.count; i++) { + const item = grid.itemAtIndex(i); + if (item && item !== parent) { + item.isHovered = false; + } + } + parent.isHovered = true; + grid.currentIndex = index; + } + onExited: { + parent.isHovered = false; + } + onClicked: { + GlobalStates.wallpaperOverviewOpen = false; + filterField.text = ""; + Wallpapers.apply(modelData); + } + } + } + + add: Transition { + from: "*" + to: "*" + ParallelAnimation { + PropertyAnimation { + property: "x" + from: grid.contentX + (grid.width / 2) - width / 2 + } + PropertyAnimation { + property: "y" + from: grid.contentY + (grid.height / 2) - height / 2 + } + NumberAnimation { + property: "scale" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + NumberAnimation { + property: "opacity" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + } + } + } + // show when no wallpaper found + ColumnLayout { + id: noWallpapersFoundLayout + visible: root.filteredWallpapers.length === 0 + anchors.centerIn: parent + + implicitHeight: noWallpapersFoundLabel.implicitHeight + implicitWidth: noWallpapersFoundLabel.implicitWidth + + Label { + id: noWallpapersFoundLabel + text: "No wallpapers found" + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colSubtext + Layout.alignment: Qt.AlignHcenter | Qt.AlignVCenter + } + } + } + } + } + + Connections { + target: GlobalStates + function onWallpaperOverviewOpenChanged() { + if (GlobalStates.wallpaperOverviewOpen && monitorIsFocused) { + filterField.forceActiveFocus(); + } } } } From 8feee4e61a0b06832ae894e499a4ec72c90a4993 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 18:27:54 +0700 Subject: [PATCH 07/35] WallpaperOverview: make anims consistent --- .../wallpaperOverview/WallpaperOverview.qml | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml b/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml index 7ca666b18..70d065dba 100644 --- a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml +++ b/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml @@ -134,17 +134,11 @@ Scope { } Behavior on implicitWidth { - SpringAnimation { - spring: 3 - damping: 0.2 - } + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) } Behavior on implicitHeight { - SpringAnimation { - spring: 3 - damping: 0.2 - } + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) } Keys.onPressed: event => { @@ -247,17 +241,11 @@ Scope { property bool isHovered: false Behavior on width { - NumberAnimation { - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) } Behavior on height { - NumberAnimation { - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) } Rectangle { @@ -315,10 +303,7 @@ Scope { opacity: status === Image.Ready ? 1 : 0 Behavior on opacity { - NumberAnimation { - duration: 200 - easing.type: Easing.OutCubic - } + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) } } } From 445b10d6f04f732e80c2e6ac51115fed173e6da1 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 20:41:44 +0700 Subject: [PATCH 08/35] put wallpaper picker in loader and make it use real thumbnails --- .../ii/modules/common/Directories.qml | 1 + .../wallpaperOverview/WallpaperOverview.qml | 658 +++++++++--------- 2 files changed, 334 insertions(+), 325 deletions(-) diff --git a/.config/quickshell/ii/modules/common/Directories.qml b/.config/quickshell/ii/modules/common/Directories.qml index a1748ece3..2b235c691 100644 --- a/.config/quickshell/ii/modules/common/Directories.qml +++ b/.config/quickshell/ii/modules/common/Directories.qml @@ -11,6 +11,7 @@ Singleton { readonly property string config: StandardPaths.standardLocations(StandardPaths.ConfigLocation)[0] readonly property string state: StandardPaths.standardLocations(StandardPaths.StateLocation)[0] readonly property string cache: StandardPaths.standardLocations(StandardPaths.CacheLocation)[0] + readonly property string genericCache: StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0] readonly property string pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0] readonly property string downloads: StandardPaths.standardLocations(StandardPaths.DownloadLocation)[0] diff --git a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml b/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml index 70d065dba..667f2015e 100644 --- a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml +++ b/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml @@ -14,380 +14,388 @@ import Quickshell.Hyprland Scope { id: scope - PanelWindow { - id: root - readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) - property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) - visible: GlobalStates.wallpaperOverviewOpen - property var filteredWallpapers: Wallpapers.wallpapers + Loader { + active: GlobalStates.wallpaperOverviewOpen + sourceComponent: PanelWindow { + id: root + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) + property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) + property var filteredWallpapers: Wallpapers.wallpapers - WlrLayershell.namespace: "quickshell:wallpaper-overview" - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand - color: "transparent" + WlrLayershell.namespace: "quickshell:wallpaper-overview" + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + color: "transparent" - anchors { - top: true - bottom: true - left: true - right: true - } + anchors { + top: true + bottom: true + left: true + right: true + } - ColumnLayout { - id: layout - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - spacing: 8 + ColumnLayout { + id: layout + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + spacing: 8 - TextField { - id: filterField - Layout.preferredWidth: bg.implicitWidth - Layout.alignment: Qt.AlignHcenter - implicitHeight: 40 - padding: 10 - placeholderText: "Search wallpapers..." - placeholderTextColor: Appearance.colors.colSubtext - color: Appearance.colors.colPrimary - background: Rectangle { - color: Appearance.colors.colLayer0 - border.color: Appearance.colors.colLayer0Border - border.width: 1 - radius: Appearance.rounding.small - } - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal + TextField { + id: filterField + Layout.preferredWidth: bg.implicitWidth + Layout.alignment: Qt.AlignHcenter + implicitHeight: 40 + padding: 10 + placeholderText: "Search wallpapers..." + placeholderTextColor: Appearance.colors.colSubtext + color: Appearance.colors.colPrimary + background: Rectangle { + color: Appearance.colors.colLayer0 + border.color: Appearance.colors.colLayer0Border + border.width: 1 + radius: Appearance.rounding.small + } + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal - onTextChanged: { - let newModel = []; - if (text.length > 0) { - for (let i = 0; i < Wallpapers.wallpapers.length; ++i) { - let wallpaperPath = Wallpapers.wallpapers[i]; - if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) { - newModel.push(wallpaperPath); + onTextChanged: { + let newModel = []; + if (text.length > 0) { + for (let i = 0; i < Wallpapers.wallpapers.length; ++i) { + let wallpaperPath = Wallpapers.wallpapers[i]; + if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) { + newModel.push(wallpaperPath); + } + } + root.filteredWallpapers = newModel; + } else { + root.filteredWallpapers = Wallpapers.wallpapers; + } + } + + Keys.onPressed: event => { + if (text.length === 0) { + if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { + bg.forceActiveFocus(); + if (event.key === Qt.Key_Down) + grid.moveSelection(grid.columns); + else if (event.key === Qt.Key_Left) + grid.moveSelection(-1); + else if (event.key === Qt.Key_Right) + grid.moveSelection(1); + event.accepted = true; + } + } else { + if (event.key === Qt.Key_Down) { + grid.moveSelection(grid.columns); + event.accepted = true; + bg.forceActiveFocus(); } } - root.filteredWallpapers = newModel; - } else { - root.filteredWallpapers = Wallpapers.wallpapers; + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + grid.activateCurrent(); + event.accepted = true; + } else if (event.key === Qt.Key_Escape) { + if (filterField.text.length > 0) { + filterField.text = ""; + } else { + GlobalStates.wallpaperOverviewOpen = false; + } + event.accepted = true; + } } } - Keys.onPressed: event => { - if (text.length === 0) { - if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { - bg.forceActiveFocus(); - if (event.key === Qt.Key_Down) - grid.moveSelection(grid.columns); - else if (event.key === Qt.Key_Left) - grid.moveSelection(-1); - else if (event.key === Qt.Key_Right) - grid.moveSelection(1); - event.accepted = true; + Rectangle { + id: bg + focus: true + color: Appearance.colors.colLayer0 + border.width: 1 + border.color: Appearance.colors.colLayer0Border + radius: Appearance.rounding.screenRounding + + property int calculatedRows: Math.ceil(grid.count / grid.columns) + + implicitWidth: { + if (root.filteredWallpapers.length === 0) { + return 300; + } else if (root.filteredWallpapers.length < grid.columns) { + return root.filteredWallpapers.length * grid.cellWidth + 16; + } else { + return Math.min(root.width * 0.7, 900); } - } else { - if (event.key === Qt.Key_Down) { + } + + implicitHeight: { + if (root.filteredWallpapers.length === 0) { + return 100; + } else { + return Math.min(root.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); + } + } + + Behavior on implicitWidth { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + + Behavior on implicitHeight { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + GlobalStates.wallpaperOverviewOpen = false; + event.accepted = true; + } else if (event.key === Qt.Key_Left) { + grid.moveSelection(-1); + event.accepted = true; + } else if (event.key === Qt.Key_Right) { + grid.moveSelection(1); + event.accepted = true; + } else if (event.key === Qt.Key_Up) { + if (grid.currentIndex < grid.columns) { + filterField.forceActiveFocus(); + } else { + grid.moveSelection(-grid.columns); + } + event.accepted = true; + } else if (event.key === Qt.Key_Down) { grid.moveSelection(grid.columns); event.accepted = true; - bg.forceActiveFocus(); - } - } - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - grid.activateCurrent(); - event.accepted = true; - } else if (event.key === Qt.Key_Escape) { - if (filterField.text.length > 0) { - filterField.text = ""; - } else { - GlobalStates.wallpaperOverviewOpen = false; - } - event.accepted = true; - } - } - } - - Rectangle { - id: bg - focus: true - color: Appearance.colors.colLayer0 - border.width: 1 - border.color: Appearance.colors.colLayer0Border - radius: Appearance.rounding.screenRounding - - property int calculatedRows: Math.ceil(grid.count / grid.columns) - - implicitWidth: { - if (root.filteredWallpapers.length === 0) { - return 300; - } else if (root.filteredWallpapers.length < grid.columns) { - return root.filteredWallpapers.length * grid.cellWidth + 16; - } else { - return Math.min(root.width * 0.7, 900); - } - } - - implicitHeight: { - if (root.filteredWallpapers.length === 0) { - return 100; - } else { - return Math.min(root.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); - } - } - - Behavior on implicitWidth { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - - Behavior on implicitHeight { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - - Keys.onPressed: event => { - if (event.key === Qt.Key_Escape) { - GlobalStates.wallpaperOverviewOpen = false; - event.accepted = true; - } else if (event.key === Qt.Key_Left) { - grid.moveSelection(-1); - event.accepted = true; - } else if (event.key === Qt.Key_Right) { - grid.moveSelection(1); - event.accepted = true; - } else if (event.key === Qt.Key_Up) { - if (grid.currentIndex < grid.columns) { + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + grid.activateCurrent(); + event.accepted = true; + } else if (event.key === Qt.Key_Backspace) { + if (filterField.text.length > 0) { + filterField.text = filterField.text.substring(0, filterField.text.length - 1); + } filterField.forceActiveFocus(); + event.accepted = true; } else { - grid.moveSelection(-grid.columns); + filterField.forceActiveFocus(); + if (event.text.length > 0) { + filterField.text += event.text; + filterField.cursorPosition = filterField.text.length; + } + event.accepted = true; } - event.accepted = true; - } else if (event.key === Qt.Key_Down) { - grid.moveSelection(grid.columns); - event.accepted = true; - } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - grid.activateCurrent(); - event.accepted = true; - } else if (event.key === Qt.Key_Backspace) { - if (filterField.text.length > 0) { - filterField.text = filterField.text.substring(0, filterField.text.length - 1); - } - filterField.forceActiveFocus(); - event.accepted = true; - } else { - filterField.forceActiveFocus(); - if (event.text.length > 0) { - filterField.text += event.text; - filterField.cursorPosition = filterField.text.length; - } - event.accepted = true; } - } - ColumnLayout { - anchors.fill: parent - anchors.margins: 8 - spacing: 8 + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 8 - GridView { - id: grid - visible: root.filteredWallpapers.length > 0 + GridView { + id: grid + visible: root.filteredWallpapers.length > 0 - readonly property int columns: 4 - property int currentIndex: 0 - readonly property int rows: Math.max(1, Math.ceil(count / columns)) + readonly property int columns: 4 + property int currentIndex: 0 + readonly property int rows: Math.max(1, Math.ceil(count / columns)) - Layout.preferredWidth: columns * cellWidth - Layout.alignment: Qt.AlignHcenter - Layout.fillHeight: true - cellWidth: 220 - cellHeight: 140 - clip: true - interactive: true - keyNavigationWraps: true - boundsBehavior: Flickable.StopAtBounds + Layout.preferredWidth: columns * cellWidth + Layout.alignment: Qt.AlignHCenter + Layout.fillHeight: true + cellWidth: 128 + cellHeight: 72 + clip: true + interactive: true + keyNavigationWraps: true + boundsBehavior: Flickable.StopAtBounds - cacheBuffer: cellHeight * 2 - ScrollBar.horizontal: ScrollBar { - policy: ScrollBar.AsNeeded - visible: false - } - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded - visible: false - } + cacheBuffer: cellHeight * 2 + ScrollBar.horizontal: ScrollBar { + policy: ScrollBar.AsNeeded + visible: false + } + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + visible: false + } - model: root.filteredWallpapers - onModelChanged: currentIndex = 0 + model: root.filteredWallpapers + onModelChanged: currentIndex = 0 - function moveSelection(delta) { - for (let i = 0; i < count; i++) { - const item = itemAtIndex(i); - if (item) { - item.isHovered = false; + function moveSelection(delta) { + for (let i = 0; i < count; i++) { + const item = itemAtIndex(i); + if (item) { + item.isHovered = false; + } } + currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); + positionViewAtIndex(currentIndex, GridView.Contain); } - currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); - positionViewAtIndex(currentIndex, GridView.Contain); - } - function activateCurrent() { - const path = model[currentIndex]; - if (!path) - return; - GlobalStates.wallpaperOverviewOpen = false; - filterField.text = ""; - Wallpapers.apply(path); - } - - delegate: Item { - width: grid.cellWidth - height: grid.cellHeight - property bool isHovered: false - - Behavior on width { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + function activateCurrent() { + const path = model[currentIndex]; + if (!path) + return; + GlobalStates.wallpaperOverviewOpen = false; + filterField.text = ""; + Wallpapers.apply(path); } - Behavior on height { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } + delegate: Item { + width: grid.cellWidth + height: grid.cellHeight + property bool isHovered: false - Rectangle { - anchors.fill: parent - radius: Appearance.rounding.windowRounding - color: Appearance.colors.colLayer1 - border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 - border.color: Appearance.colors.colSecondary - } + Behavior on width { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } - Rectangle { - anchors.fill: parent - anchors.margins: 8 - color: Appearance.colors.colLayer2 - radius: Appearance.rounding.elementRounding + Behavior on height { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } Rectangle { - anchors.centerIn: parent - width: Math.min(parent.width * 0.4, 32) - height: Math.min(parent.height * 0.4, 32) - radius: Appearance.rounding.elementRounding - color: Appearance.colors.colLayer3 - visible: thumbnailImage.status !== Image.Ready - - opacity: 0.3 - SequentialAnimation on opacity { - running: parent.visible - loops: Animation.Infinite - NumberAnimation { - to: 1.0 - duration: 800 - easing.type: Easing.InOutSine - } - NumberAnimation { - to: 0.3 - duration: 800 - easing.type: Easing.InOutSine - } - } - } - - Image { - id: thumbnailImage anchors.fill: parent - source: `file://${modelData}` - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: false - smooth: true - - sourceSize.width: Math.min(128, grid.cellWidth - 16) - sourceSize.height: Math.min(96, grid.cellHeight - 16) - - mipmap: false - - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } + radius: Appearance.rounding.windowRounding + color: Appearance.colors.colLayer1 + border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 + border.color: Appearance.colors.colSecondary } - } - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - for (let i = 0; i < grid.count; i++) { - const item = grid.itemAtIndex(i); - if (item && item !== parent) { - item.isHovered = false; + Rectangle { + anchors.fill: parent + anchors.margins: 8 + color: Appearance.colors.colLayer2 + radius: Appearance.rounding.elementRounding + + Rectangle { + anchors.centerIn: parent + width: Math.min(parent.width * 0.4, 32) + height: Math.min(parent.height * 0.4, 32) + radius: Appearance.rounding.elementRounding + color: Appearance.colors.colLayer3 + visible: thumbnailImage.status !== Image.Ready + + opacity: 0.3 + SequentialAnimation on opacity { + running: parent.visible + loops: Animation.Infinite + NumberAnimation { + to: 1.0 + duration: 800 + easing.type: Easing.InOutSine + } + NumberAnimation { + to: 0.3 + duration: 800 + easing.type: Easing.InOutSine + } + } + } + + Image { + id: thumbnailImage + anchors.fill: parent + source: { + const resolvedUrl = Qt.resolvedUrl(modelData); + const md5Hash = Qt.md5(resolvedUrl); + const cacheSize = "normal" + const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; + return thumbnailPath + } + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: true + smooth: true + + sourceSize.width: Math.min(128, grid.cellWidth - 16) + sourceSize.height: Math.min(96, grid.cellHeight - 16) + + mipmap: false + + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) } } - parent.isHovered = true; - grid.currentIndex = index; } - onExited: { - parent.isHovered = false; + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + for (let i = 0; i < grid.count; i++) { + const item = grid.itemAtIndex(i); + if (item && item !== parent) { + item.isHovered = false; + } + } + parent.isHovered = true; + grid.currentIndex = index; + } + onExited: { + parent.isHovered = false; + } + onClicked: { + GlobalStates.wallpaperOverviewOpen = false; + filterField.text = ""; + Wallpapers.apply(modelData); + } } - onClicked: { - GlobalStates.wallpaperOverviewOpen = false; - filterField.text = ""; - Wallpapers.apply(modelData); + } + + add: Transition { + from: "*" + to: "*" + ParallelAnimation { + PropertyAnimation { + property: "x" + from: grid.contentX + (grid.width / 2) - width / 2 + } + PropertyAnimation { + property: "y" + from: grid.contentY + (grid.height / 2) - height / 2 + } + NumberAnimation { + property: "scale" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + NumberAnimation { + property: "opacity" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } } } } + // show when no wallpaper found + ColumnLayout { + id: noWallpapersFoundLayout + visible: root.filteredWallpapers.length === 0 + anchors.centerIn: parent - add: Transition { - from: "*" - to: "*" - ParallelAnimation { - PropertyAnimation { - property: "x" - from: grid.contentX + (grid.width / 2) - width / 2 - } - PropertyAnimation { - property: "y" - from: grid.contentY + (grid.height / 2) - height / 2 - } - NumberAnimation { - property: "scale" - from: 0.0 - to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } - NumberAnimation { - property: "opacity" - from: 0.0 - to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } + implicitHeight: noWallpapersFoundLabel.implicitHeight + implicitWidth: noWallpapersFoundLabel.implicitWidth + + Label { + id: noWallpapersFoundLabel + text: "No wallpapers found" + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colSubtext + Layout.alignment: Qt.AlignHcenter | Qt.AlignVCenter } } } - // show when no wallpaper found - ColumnLayout { - id: noWallpapersFoundLayout - visible: root.filteredWallpapers.length === 0 - anchors.centerIn: parent - - implicitHeight: noWallpapersFoundLabel.implicitHeight - implicitWidth: noWallpapersFoundLabel.implicitWidth - - Label { - id: noWallpapersFoundLabel - text: "No wallpapers found" - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - color: Appearance.colors.colSubtext - Layout.alignment: Qt.AlignHcenter | Qt.AlignVCenter - } - } } } - } - Connections { - target: GlobalStates - function onWallpaperOverviewOpenChanged() { - if (GlobalStates.wallpaperOverviewOpen && monitorIsFocused) { - filterField.forceActiveFocus(); + Connections { + target: GlobalStates + function onWallpaperOverviewOpenChanged() { + if (GlobalStates.wallpaperOverviewOpen && monitorIsFocused) { + filterField.forceActiveFocus(); + } } } } From 2d8eb163e784b44f9ac39c60da7b41f91eb0bf69 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 20:55:54 +0700 Subject: [PATCH 09/35] rename wallpaper overview to wallpaper selector --- .config/hypr/hyprland/keybinds.conf | 4 ++-- .config/quickshell/ii/GlobalStates.qml | 2 +- .../WallpaperSelector.qml} | 23 ++++++++++--------- .config/quickshell/ii/shell.qml | 6 ++--- 4 files changed, 18 insertions(+), 17 deletions(-) rename .config/quickshell/ii/modules/{wallpaperOverview/WallpaperOverview.qml => wallpaperSelector/WallpaperSelector.qml} (96%) diff --git a/.config/hypr/hyprland/keybinds.conf b/.config/hypr/hyprland/keybinds.conf index ca723b8df..e29dcaba5 100644 --- a/.config/hypr/hyprland/keybinds.conf +++ b/.config/hypr/hyprland/keybinds.conf @@ -25,7 +25,6 @@ bindit = ,Super_R, global, quickshell:workspaceNumber # [hidden] bindd = Super, V, Clipboard history >> clipboard, global, quickshell:overviewClipboardToggle # Clipboard history >> clipboard bindd = Super, Period, Emoji >> clipboard, global, quickshell:overviewEmojiToggle # Emoji >> clipboard bindd = Super, Tab, Toggle overview, global, quickshell:overviewToggle # [hidden] Toggle overview/launcher (alt) -bindd = Super, Comma, Toggle wallpaper overview, global, quickshell:wallpaperOverviewToggle # Wallpaper overview bindd = Super, A, Toggle left sidebar, global, quickshell:sidebarLeftToggle # Toggle left sidebar bind = Super+Alt, A, global, quickshell:sidebarLeftToggleDetach # [hidden] bind = Super, B, global, quickshell:sidebarLeftToggle # [hidden] @@ -49,7 +48,8 @@ bindld = Super+Shift,M, Toggle mute, exec, wpctl set-mute @DEFAULT_SINK@ toggle bindl = Alt ,XF86AudioMute, exec, wpctl set-mute @DEFAULT_SOURCE@ toggle # [hidden] bindl = ,XF86AudioMicMute, exec, wpctl set-mute @DEFAULT_SOURCE@ toggle # [hidden] bindld = Super+Alt,M, Toggle mic, exec, wpctl set-mute @DEFAULT_SOURCE@ toggle # [hidden] -bindd = Ctrl+Super, T, Change wallpaper, exec, ~/.config/quickshell/$qsConfig/scripts/colors/switchwall.sh # Change wallpaper +bindd = Ctrl+Super, T, Toggle wallpaper selector, global, quickshell:wallpaperSelectorToggle # Wallpaper selector +bindd = Ctrl+Super, T, Change wallpaper, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/quickshell/$qsConfig/scripts/colors/switchwall.sh # [hidden] Change wallpaper (fallback) bind = Ctrl+Super, R, exec, killall ags agsv1 gjs ydotool qs quickshell; qs -c $qsConfig & # Restart widgets ##! Utilities diff --git a/.config/quickshell/ii/GlobalStates.qml b/.config/quickshell/ii/GlobalStates.qml index 57ce3b4cc..f2836c99b 100644 --- a/.config/quickshell/ii/GlobalStates.qml +++ b/.config/quickshell/ii/GlobalStates.qml @@ -17,7 +17,7 @@ Singleton { property bool osdVolumeOpen: false property bool oskOpen: false property bool overviewOpen: false - property bool wallpaperOverviewOpen: false + property bool wallpaperSelectorOpen: false property bool screenLocked: false property bool screenLockContainsCharacters: false property bool screenUnlockFailed: false diff --git a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml similarity index 96% rename from .config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml rename to .config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index 667f2015e..645a125c6 100644 --- a/.config/quickshell/ii/modules/wallpaperOverview/WallpaperOverview.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -15,14 +15,15 @@ Scope { id: scope Loader { - active: GlobalStates.wallpaperOverviewOpen + active: GlobalStates.wallpaperSelectorOpen + sourceComponent: PanelWindow { id: root readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) property var filteredWallpapers: Wallpapers.wallpapers - WlrLayershell.namespace: "quickshell:wallpaper-overview" + WlrLayershell.namespace: "quickshell:wallpaperSelector" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand color: "transparent" @@ -99,7 +100,7 @@ Scope { if (filterField.text.length > 0) { filterField.text = ""; } else { - GlobalStates.wallpaperOverviewOpen = false; + GlobalStates.wallpaperSelectorOpen = false; } event.accepted = true; } @@ -144,7 +145,7 @@ Scope { Keys.onPressed: event => { if (event.key === Qt.Key_Escape) { - GlobalStates.wallpaperOverviewOpen = false; + GlobalStates.wallpaperSelectorOpen = false; event.accepted = true; } else if (event.key === Qt.Key_Left) { grid.moveSelection(-1); @@ -231,7 +232,7 @@ Scope { const path = model[currentIndex]; if (!path) return; - GlobalStates.wallpaperOverviewOpen = false; + GlobalStates.wallpaperSelectorOpen = false; filterField.text = ""; Wallpapers.apply(path); } @@ -332,7 +333,7 @@ Scope { parent.isHovered = false; } onClicked: { - GlobalStates.wallpaperOverviewOpen = false; + GlobalStates.wallpaperSelectorOpen = false; filterField.text = ""; Wallpapers.apply(modelData); } @@ -392,8 +393,8 @@ Scope { Connections { target: GlobalStates - function onWallpaperOverviewOpenChanged() { - if (GlobalStates.wallpaperOverviewOpen && monitorIsFocused) { + function onwallpaperSelectorOpenChanged() { + if (GlobalStates.wallpaperSelectorOpen && monitorIsFocused) { filterField.forceActiveFocus(); } } @@ -402,10 +403,10 @@ Scope { } GlobalShortcut { - name: "wallpaperOverviewToggle" - description: "Toggle wallpaper overview" + name: "wallpaperSelectorToggle" + description: "Toggle wallpaper selector" onPressed: { - GlobalStates.wallpaperOverviewOpen = !GlobalStates.wallpaperOverviewOpen; + GlobalStates.wallpaperSelectorOpen = !GlobalStates.wallpaperSelectorOpen; } } } diff --git a/.config/quickshell/ii/shell.qml b/.config/quickshell/ii/shell.qml index b135f5924..56dc75ff0 100644 --- a/.config/quickshell/ii/shell.qml +++ b/.config/quickshell/ii/shell.qml @@ -18,12 +18,12 @@ import "./modules/notificationPopup/" import "./modules/onScreenDisplay/" import "./modules/onScreenKeyboard/" import "./modules/overview/" -import "./modules/wallpaperOverview/" import "./modules/screenCorners/" import "./modules/session/" import "./modules/sidebarLeft/" import "./modules/sidebarRight/" import "./modules/verticalBar/" +import "./modules/wallpaperSelector/" import QtQuick import QtQuick.Window @@ -44,13 +44,13 @@ ShellRoot { property bool enableOnScreenDisplayVolume: true property bool enableOnScreenKeyboard: true property bool enableOverview: true - property bool enableWallpaperOverview: true property bool enableReloadPopup: true property bool enableScreenCorners: true property bool enableSession: true property bool enableSidebarLeft: true property bool enableSidebarRight: true property bool enableVerticalBar: true + property bool enableWallpaperSelector: true // Force initialization of some singletons Component.onCompleted: { @@ -72,12 +72,12 @@ ShellRoot { LazyLoader { active: enableOnScreenDisplayVolume; component: OnScreenDisplayVolume {} } LazyLoader { active: enableOnScreenKeyboard; component: OnScreenKeyboard {} } LazyLoader { active: enableOverview; component: Overview {} } - LazyLoader { active: enableWallpaperOverview; component: WallpaperOverview {} } LazyLoader { active: enableReloadPopup; component: ReloadPopup {} } LazyLoader { active: enableScreenCorners; component: ScreenCorners {} } LazyLoader { active: enableSession; component: Session {} } LazyLoader { active: enableSidebarLeft; component: SidebarLeft {} } LazyLoader { active: enableSidebarRight; component: SidebarRight {} } LazyLoader { active: enableVerticalBar && Config.ready && Config.options.bar.vertical; component: VerticalBar {} } + LazyLoader { active: enableWallpaperSelector; component: WallpaperSelector {} } } From 6aa37e252978f7041ae9e95f571a544b48e410df Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 20:59:26 +0700 Subject: [PATCH 10/35] wallpaper selector: dont cache images --- .../ii/modules/wallpaperSelector/WallpaperSelector.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index 645a125c6..4704c8887 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -301,7 +301,7 @@ Scope { } fillMode: Image.PreserveAspectCrop asynchronous: true - cache: true + cache: false smooth: true sourceSize.width: Math.min(128, grid.cellWidth - 16) From 28fe7817b470ff575bf79b8324d821b1107e5066 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 21:00:41 +0700 Subject: [PATCH 11/35] wallpaper selector: add ipchandler alongside globalshortcut --- .../ii/modules/wallpaperSelector/WallpaperSelector.qml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index 4704c8887..6642d20e6 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -402,6 +402,14 @@ Scope { } } + IpcHandler { + target: "wallpaperSelector" + + function toggle(): void { + GlobalStates.wallpaperSelectorOpen = !GlobalStates.wallpaperSelectorOpen + } + } + GlobalShortcut { name: "wallpaperSelectorToggle" description: "Toggle wallpaper selector" From 42695d9253df2568c181c2fdd6b7d1963591375b Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 21:02:03 +0700 Subject: [PATCH 12/35] Wallpapers: use Process::exec instead of setting command and setting `running` prop --- .config/quickshell/ii/services/Wallpapers.qml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index 4683bed99..d5c4e0701 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -35,12 +35,11 @@ Singleton { function apply(path) { if (!path || path.length === 0) return - applyProc.command = [ + applyProc.exec([ "bash", "-c", `${StringUtils.shellSingleQuoteEscape(Directories.wallpaperSwitchScriptPath)} ` + `--image ${StringUtils.shellSingleQuoteEscape(path)}` - ] - applyProc.running = true + ]) } // Folder model From 89e726b5a2d7d22501b79df98ebb9d411ad9b76a Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 21:09:10 +0700 Subject: [PATCH 13/35] remove animations that aren't supposed to happen --- .../ii/modules/wallpaperSelector/WallpaperSelector.qml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index 6642d20e6..91a9dbc04 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -242,14 +242,6 @@ Scope { height: grid.cellHeight property bool isHovered: false - Behavior on width { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - - Behavior on height { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - Rectangle { anchors.fill: parent radius: Appearance.rounding.windowRounding From 8124f688da5c347ebe120a88d3d25cbfbd5caed8 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 21:35:01 +0700 Subject: [PATCH 14/35] remove manual `file://` construction --- .config/quickshell/ii/services/Wallpapers.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index d5c4e0701..98b78b0b6 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -29,7 +29,7 @@ Singleton { // Public API (FolderListModel driven) function reload() { - files.folder = `file://${root.searchDirs[0]}` + files.folder = Qt.resolvedUrl(root.searchDirs[0]) } onSearchDirsChanged: reload() From 980533052bc7c89b09e38e3340c240dc992bf1e7 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 18 Aug 2025 21:47:55 +0700 Subject: [PATCH 15/35] wallpaper selector: remove some useless layout crap --- .../wallpaperSelector/WallpaperSelector.qml | 356 +++++++++--------- 1 file changed, 173 insertions(+), 183 deletions(-) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index 91a9dbc04..effd880ce 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -12,17 +12,20 @@ import Quickshell.Wayland import Quickshell.Hyprland Scope { - id: scope + id: root + property int thumbnailWidth: 128 + property int thumbnailHeight: 72 Loader { active: GlobalStates.wallpaperSelectorOpen sourceComponent: PanelWindow { - id: root - readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) + id: panelWindow + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen) property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) property var filteredWallpapers: Wallpapers.wallpapers + exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "quickshell:wallpaperSelector" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand @@ -35,6 +38,10 @@ Scope { right: true } + margins { + top: Appearance.sizes.barHeight + Appearance.sizes.hyprlandGapsOut + } + ColumnLayout { id: layout anchors.horizontalCenter: parent.horizontalCenter @@ -43,9 +50,9 @@ Scope { TextField { id: filterField - Layout.preferredWidth: bg.implicitWidth - Layout.alignment: Qt.AlignHcenter + Layout.alignment: Qt.AlignHCenter implicitHeight: 40 + implicitWidth: Appearance.sizes.searchWidth padding: 10 placeholderText: "Search wallpapers..." placeholderTextColor: Appearance.colors.colSubtext @@ -68,9 +75,9 @@ Scope { newModel.push(wallpaperPath); } } - root.filteredWallpapers = newModel; + panelWindow.filteredWallpapers = newModel; } else { - root.filteredWallpapers = Wallpapers.wallpapers; + panelWindow.filteredWallpapers = Wallpapers.wallpapers; } } @@ -114,24 +121,25 @@ Scope { border.width: 1 border.color: Appearance.colors.colLayer0Border radius: Appearance.rounding.screenRounding + // Layout.alignment: Qt.AlignHCenter property int calculatedRows: Math.ceil(grid.count / grid.columns) implicitWidth: { - if (root.filteredWallpapers.length === 0) { + if (panelWindow.filteredWallpapers.length === 0) { return 300; - } else if (root.filteredWallpapers.length < grid.columns) { - return root.filteredWallpapers.length * grid.cellWidth + 16; + } else if (panelWindow.filteredWallpapers.length < grid.columns) { + return panelWindow.filteredWallpapers.length * grid.cellWidth + 16; } else { - return Math.min(root.width * 0.7, 900); + return Math.min(panelWindow.width * 0.7, 900); } } implicitHeight: { - if (root.filteredWallpapers.length === 0) { + if (panelWindow.filteredWallpapers.length === 0) { return 100; } else { - return Math.min(root.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); + return Math.min(panelWindow.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); } } @@ -182,204 +190,186 @@ Scope { } } - ColumnLayout { + GridView { + id: grid + visible: panelWindow.filteredWallpapers.length > 0 + + readonly property int columns: 7 + property int currentIndex: 0 + readonly property int rows: Math.max(1, Math.ceil(count / columns)) + anchors.fill: parent - anchors.margins: 8 - spacing: 8 + cellWidth: root.thumbnailWidth + cellHeight: root.thumbnailHeight + clip: true + interactive: true + keyNavigationWraps: true + boundsBehavior: Flickable.StopAtBounds - GridView { - id: grid - visible: root.filteredWallpapers.length > 0 + cacheBuffer: cellHeight * 2 + ScrollBar.horizontal: ScrollBar { + policy: ScrollBar.AsNeeded + } + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + } - readonly property int columns: 4 - property int currentIndex: 0 - readonly property int rows: Math.max(1, Math.ceil(count / columns)) + model: panelWindow.filteredWallpapers + onModelChanged: currentIndex = 0 - Layout.preferredWidth: columns * cellWidth - Layout.alignment: Qt.AlignHCenter - Layout.fillHeight: true - cellWidth: 128 - cellHeight: 72 - clip: true - interactive: true - keyNavigationWraps: true - boundsBehavior: Flickable.StopAtBounds - - cacheBuffer: cellHeight * 2 - ScrollBar.horizontal: ScrollBar { - policy: ScrollBar.AsNeeded - visible: false - } - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded - visible: false - } - - model: root.filteredWallpapers - onModelChanged: currentIndex = 0 - - function moveSelection(delta) { - for (let i = 0; i < count; i++) { - const item = itemAtIndex(i); - if (item) { - item.isHovered = false; - } + function moveSelection(delta) { + for (let i = 0; i < count; i++) { + const item = itemAtIndex(i); + if (item) { + item.isHovered = false; } - currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); - positionViewAtIndex(currentIndex, GridView.Contain); } - function activateCurrent() { - const path = model[currentIndex]; - if (!path) - return; - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(path); + currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); + positionViewAtIndex(currentIndex, GridView.Contain); + } + function activateCurrent() { + const path = model[currentIndex]; + if (!path) + return; + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(path); + } + + delegate: Item { + width: grid.cellWidth + height: grid.cellHeight + property bool isHovered: false + + Rectangle { + anchors.fill: parent + radius: Appearance.rounding.windowRounding + color: Appearance.colors.colLayer1 + border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 + border.color: Appearance.colors.colSecondary } - delegate: Item { - width: grid.cellWidth - height: grid.cellHeight - property bool isHovered: false + Rectangle { + anchors.fill: parent + anchors.margins: 8 + color: Appearance.colors.colLayer2 + radius: Appearance.rounding.elementRounding Rectangle { - anchors.fill: parent - radius: Appearance.rounding.windowRounding - color: Appearance.colors.colLayer1 - border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 - border.color: Appearance.colors.colSecondary - } - - Rectangle { - anchors.fill: parent - anchors.margins: 8 - color: Appearance.colors.colLayer2 + anchors.centerIn: parent + width: Math.min(parent.width * 0.4, 32) + height: Math.min(parent.height * 0.4, 32) radius: Appearance.rounding.elementRounding + color: Appearance.colors.colLayer3 + visible: thumbnailImage.status !== Image.Ready - Rectangle { - anchors.centerIn: parent - width: Math.min(parent.width * 0.4, 32) - height: Math.min(parent.height * 0.4, 32) - radius: Appearance.rounding.elementRounding - color: Appearance.colors.colLayer3 - visible: thumbnailImage.status !== Image.Ready - - opacity: 0.3 - SequentialAnimation on opacity { - running: parent.visible - loops: Animation.Infinite - NumberAnimation { - to: 1.0 - duration: 800 - easing.type: Easing.InOutSine - } - NumberAnimation { - to: 0.3 - duration: 800 - easing.type: Easing.InOutSine - } + opacity: 0.3 + SequentialAnimation on opacity { + running: parent.visible + loops: Animation.Infinite + NumberAnimation { + to: 1.0 + duration: 800 + easing.type: Easing.InOutSine } - } - - Image { - id: thumbnailImage - anchors.fill: parent - source: { - const resolvedUrl = Qt.resolvedUrl(modelData); - const md5Hash = Qt.md5(resolvedUrl); - const cacheSize = "normal" - const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; - return thumbnailPath - } - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: false - smooth: true - - sourceSize.width: Math.min(128, grid.cellWidth - 16) - sourceSize.height: Math.min(96, grid.cellHeight - 16) - - mipmap: false - - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + NumberAnimation { + to: 0.3 + duration: 800 + easing.type: Easing.InOutSine } } } - MouseArea { + Image { + id: thumbnailImage anchors.fill: parent - hoverEnabled: true - onEntered: { - for (let i = 0; i < grid.count; i++) { - const item = grid.itemAtIndex(i); - if (item && item !== parent) { - item.isHovered = false; - } + source: { + const resolvedUrl = Qt.resolvedUrl(modelData); + const md5Hash = Qt.md5(resolvedUrl); + const cacheSize = "normal" + const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; + return thumbnailPath + } + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: false + smooth: true + + sourceSize.width: Math.min(128, grid.cellWidth - 16) + sourceSize.height: Math.min(96, grid.cellHeight - 16) + + mipmap: false + + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + for (let i = 0; i < grid.count; i++) { + const item = grid.itemAtIndex(i); + if (item && item !== parent) { + item.isHovered = false; } - parent.isHovered = true; - grid.currentIndex = index; - } - onExited: { - parent.isHovered = false; - } - onClicked: { - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(modelData); } + parent.isHovered = true; + grid.currentIndex = index; } - } - - add: Transition { - from: "*" - to: "*" - ParallelAnimation { - PropertyAnimation { - property: "x" - from: grid.contentX + (grid.width / 2) - width / 2 - } - PropertyAnimation { - property: "y" - from: grid.contentY + (grid.height / 2) - height / 2 - } - NumberAnimation { - property: "scale" - from: 0.0 - to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } - NumberAnimation { - property: "opacity" - from: 0.0 - to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } + onExited: { + parent.isHovered = false; + } + onClicked: { + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(modelData); } } } - // show when no wallpaper found - ColumnLayout { - id: noWallpapersFoundLayout - visible: root.filteredWallpapers.length === 0 - anchors.centerIn: parent - implicitHeight: noWallpapersFoundLabel.implicitHeight - implicitWidth: noWallpapersFoundLabel.implicitWidth - - Label { - id: noWallpapersFoundLabel - text: "No wallpapers found" - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - color: Appearance.colors.colSubtext - Layout.alignment: Qt.AlignHcenter | Qt.AlignVCenter + add: Transition { + from: "*" + to: "*" + ParallelAnimation { + PropertyAnimation { + property: "x" + from: grid.contentX + (grid.width / 2) - width / 2 + } + PropertyAnimation { + property: "y" + from: grid.contentY + (grid.height / 2) - height / 2 + } + NumberAnimation { + property: "scale" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + NumberAnimation { + property: "opacity" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } } } } + + Label { + id: noWallpapersFoundLabel + visible: panelWindow.filteredWallpapers.length === 0 + anchors.centerIn: parent + text: "No wallpapers found" + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colSubtext + } } } From 9cc576b98de946188bde842e2ac7c02825bbf387 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Fri, 22 Aug 2025 21:58:13 +0700 Subject: [PATCH 16/35] wallpaper selector: move content to a new file --- .../wallpaperSelector/WallpaperSelector.qml | 343 +---------------- .../WallpaperSelectorContent.qml | 354 ++++++++++++++++++ 2 files changed, 358 insertions(+), 339 deletions(-) create mode 100644 .config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index effd880ce..c3ff54cd9 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -13,8 +13,6 @@ import Quickshell.Hyprland Scope { id: root - property int thumbnailWidth: 128 - property int thumbnailHeight: 72 Loader { active: GlobalStates.wallpaperSelectorOpen @@ -42,343 +40,10 @@ Scope { top: Appearance.sizes.barHeight + Appearance.sizes.hyprlandGapsOut } - ColumnLayout { - id: layout - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - spacing: 8 - - TextField { - id: filterField - Layout.alignment: Qt.AlignHCenter - implicitHeight: 40 - implicitWidth: Appearance.sizes.searchWidth - padding: 10 - placeholderText: "Search wallpapers..." - placeholderTextColor: Appearance.colors.colSubtext - color: Appearance.colors.colPrimary - background: Rectangle { - color: Appearance.colors.colLayer0 - border.color: Appearance.colors.colLayer0Border - border.width: 1 - radius: Appearance.rounding.small - } - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - - onTextChanged: { - let newModel = []; - if (text.length > 0) { - for (let i = 0; i < Wallpapers.wallpapers.length; ++i) { - let wallpaperPath = Wallpapers.wallpapers[i]; - if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) { - newModel.push(wallpaperPath); - } - } - panelWindow.filteredWallpapers = newModel; - } else { - panelWindow.filteredWallpapers = Wallpapers.wallpapers; - } - } - - Keys.onPressed: event => { - if (text.length === 0) { - if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { - bg.forceActiveFocus(); - if (event.key === Qt.Key_Down) - grid.moveSelection(grid.columns); - else if (event.key === Qt.Key_Left) - grid.moveSelection(-1); - else if (event.key === Qt.Key_Right) - grid.moveSelection(1); - event.accepted = true; - } - } else { - if (event.key === Qt.Key_Down) { - grid.moveSelection(grid.columns); - event.accepted = true; - bg.forceActiveFocus(); - } - } - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - grid.activateCurrent(); - event.accepted = true; - } else if (event.key === Qt.Key_Escape) { - if (filterField.text.length > 0) { - filterField.text = ""; - } else { - GlobalStates.wallpaperSelectorOpen = false; - } - event.accepted = true; - } - } - } - - Rectangle { - id: bg - focus: true - color: Appearance.colors.colLayer0 - border.width: 1 - border.color: Appearance.colors.colLayer0Border - radius: Appearance.rounding.screenRounding - // Layout.alignment: Qt.AlignHCenter - - property int calculatedRows: Math.ceil(grid.count / grid.columns) - - implicitWidth: { - if (panelWindow.filteredWallpapers.length === 0) { - return 300; - } else if (panelWindow.filteredWallpapers.length < grid.columns) { - return panelWindow.filteredWallpapers.length * grid.cellWidth + 16; - } else { - return Math.min(panelWindow.width * 0.7, 900); - } - } - - implicitHeight: { - if (panelWindow.filteredWallpapers.length === 0) { - return 100; - } else { - return Math.min(panelWindow.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); - } - } - - Behavior on implicitWidth { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - - Behavior on implicitHeight { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - - Keys.onPressed: event => { - if (event.key === Qt.Key_Escape) { - GlobalStates.wallpaperSelectorOpen = false; - event.accepted = true; - } else if (event.key === Qt.Key_Left) { - grid.moveSelection(-1); - event.accepted = true; - } else if (event.key === Qt.Key_Right) { - grid.moveSelection(1); - event.accepted = true; - } else if (event.key === Qt.Key_Up) { - if (grid.currentIndex < grid.columns) { - filterField.forceActiveFocus(); - } else { - grid.moveSelection(-grid.columns); - } - event.accepted = true; - } else if (event.key === Qt.Key_Down) { - grid.moveSelection(grid.columns); - event.accepted = true; - } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - grid.activateCurrent(); - event.accepted = true; - } else if (event.key === Qt.Key_Backspace) { - if (filterField.text.length > 0) { - filterField.text = filterField.text.substring(0, filterField.text.length - 1); - } - filterField.forceActiveFocus(); - event.accepted = true; - } else { - filterField.forceActiveFocus(); - if (event.text.length > 0) { - filterField.text += event.text; - filterField.cursorPosition = filterField.text.length; - } - event.accepted = true; - } - } - - GridView { - id: grid - visible: panelWindow.filteredWallpapers.length > 0 - - readonly property int columns: 7 - property int currentIndex: 0 - readonly property int rows: Math.max(1, Math.ceil(count / columns)) - - anchors.fill: parent - cellWidth: root.thumbnailWidth - cellHeight: root.thumbnailHeight - clip: true - interactive: true - keyNavigationWraps: true - boundsBehavior: Flickable.StopAtBounds - - cacheBuffer: cellHeight * 2 - ScrollBar.horizontal: ScrollBar { - policy: ScrollBar.AsNeeded - } - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded - } - - model: panelWindow.filteredWallpapers - onModelChanged: currentIndex = 0 - - function moveSelection(delta) { - for (let i = 0; i < count; i++) { - const item = itemAtIndex(i); - if (item) { - item.isHovered = false; - } - } - currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); - positionViewAtIndex(currentIndex, GridView.Contain); - } - function activateCurrent() { - const path = model[currentIndex]; - if (!path) - return; - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(path); - } - - delegate: Item { - width: grid.cellWidth - height: grid.cellHeight - property bool isHovered: false - - Rectangle { - anchors.fill: parent - radius: Appearance.rounding.windowRounding - color: Appearance.colors.colLayer1 - border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 - border.color: Appearance.colors.colSecondary - } - - Rectangle { - anchors.fill: parent - anchors.margins: 8 - color: Appearance.colors.colLayer2 - radius: Appearance.rounding.elementRounding - - Rectangle { - anchors.centerIn: parent - width: Math.min(parent.width * 0.4, 32) - height: Math.min(parent.height * 0.4, 32) - radius: Appearance.rounding.elementRounding - color: Appearance.colors.colLayer3 - visible: thumbnailImage.status !== Image.Ready - - opacity: 0.3 - SequentialAnimation on opacity { - running: parent.visible - loops: Animation.Infinite - NumberAnimation { - to: 1.0 - duration: 800 - easing.type: Easing.InOutSine - } - NumberAnimation { - to: 0.3 - duration: 800 - easing.type: Easing.InOutSine - } - } - } - - Image { - id: thumbnailImage - anchors.fill: parent - source: { - const resolvedUrl = Qt.resolvedUrl(modelData); - const md5Hash = Qt.md5(resolvedUrl); - const cacheSize = "normal" - const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; - return thumbnailPath - } - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: false - smooth: true - - sourceSize.width: Math.min(128, grid.cellWidth - 16) - sourceSize.height: Math.min(96, grid.cellHeight - 16) - - mipmap: false - - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - for (let i = 0; i < grid.count; i++) { - const item = grid.itemAtIndex(i); - if (item && item !== parent) { - item.isHovered = false; - } - } - parent.isHovered = true; - grid.currentIndex = index; - } - onExited: { - parent.isHovered = false; - } - onClicked: { - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(modelData); - } - } - } - - add: Transition { - from: "*" - to: "*" - ParallelAnimation { - PropertyAnimation { - property: "x" - from: grid.contentX + (grid.width / 2) - width / 2 - } - PropertyAnimation { - property: "y" - from: grid.contentY + (grid.height / 2) - height / 2 - } - NumberAnimation { - property: "scale" - from: 0.0 - to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } - NumberAnimation { - property: "opacity" - from: 0.0 - to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } - } - } - } - - Label { - id: noWallpapersFoundLabel - visible: panelWindow.filteredWallpapers.length === 0 - anchors.centerIn: parent - text: "No wallpapers found" - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - color: Appearance.colors.colSubtext - } - } - } - - Connections { - target: GlobalStates - function onwallpaperSelectorOpenChanged() { - if (GlobalStates.wallpaperSelectorOpen && monitorIsFocused) { - filterField.forceActiveFocus(); - } + WallpaperSelectorContent { + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter } } } diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml new file mode 100644 index 000000000..84a7e9611 --- /dev/null +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -0,0 +1,354 @@ +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Hyprland + +Item { + id: root + property int columns: 4 + property int thumbnailWidth: 192 + property int thumbnailHeight: 108 + implicitHeight: columnLayout.implicitHeight + implicitWidth: columnLayout.implicitWidth + + ColumnLayout { + id: columnLayout + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + spacing: 8 + + TextField { + id: filterField + Layout.alignment: Qt.AlignHCenter + implicitHeight: 40 + implicitWidth: Appearance.sizes.searchWidth + padding: 10 + placeholderText: "Search wallpapers..." + placeholderTextColor: Appearance.colors.colSubtext + color: Appearance.colors.colPrimary + background: Rectangle { + color: Appearance.colors.colLayer0 + border.color: Appearance.colors.colLayer0Border + border.width: 1 + radius: Appearance.rounding.small + } + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal + + onTextChanged: { + let newModel = []; + if (text.length > 0) { + for (let i = 0; i < Wallpapers.wallpapers.length; ++i) { + let wallpaperPath = Wallpapers.wallpapers[i]; + if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) { + newModel.push(wallpaperPath); + } + } + panelWindow.filteredWallpapers = newModel; + } else { + panelWindow.filteredWallpapers = Wallpapers.wallpapers; + } + } + + Keys.onPressed: event => { + if (text.length === 0) { + if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { + bg.forceActiveFocus(); + if (event.key === Qt.Key_Down) + grid.moveSelection(grid.columns); + else if (event.key === Qt.Key_Left) + grid.moveSelection(-1); + else if (event.key === Qt.Key_Right) + grid.moveSelection(1); + event.accepted = true; + } + } else { + if (event.key === Qt.Key_Down) { + grid.moveSelection(grid.columns); + event.accepted = true; + bg.forceActiveFocus(); + } + } + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + grid.activateCurrent(); + event.accepted = true; + } else if (event.key === Qt.Key_Escape) { + if (filterField.text.length > 0) { + filterField.text = ""; + } else { + GlobalStates.wallpaperSelectorOpen = false; + } + event.accepted = true; + } + } + } + + Rectangle { + id: bg + focus: true + color: Appearance.colors.colLayer0 + border.width: 1 + border.color: Appearance.colors.colLayer0Border + radius: Appearance.rounding.screenRounding + // Layout.alignment: Qt.AlignHCenter + + property int calculatedRows: Math.ceil(grid.count / grid.columns) + + implicitWidth: { + if (panelWindow.filteredWallpapers.length === 0) { + return 300; + } else if (panelWindow.filteredWallpapers.length < grid.columns) { + return panelWindow.filteredWallpapers.length * grid.cellWidth + 16; + } else { + return Math.min(panelWindow.width * 0.7, 900); + } + } + + implicitHeight: { + if (panelWindow.filteredWallpapers.length === 0) { + return 100; + } else { + return Math.min(panelWindow.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); + } + } + + Behavior on implicitWidth { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + + Behavior on implicitHeight { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + GlobalStates.wallpaperSelectorOpen = false; + event.accepted = true; + } else if (event.key === Qt.Key_Left) { + grid.moveSelection(-1); + event.accepted = true; + } else if (event.key === Qt.Key_Right) { + grid.moveSelection(1); + event.accepted = true; + } else if (event.key === Qt.Key_Up) { + if (grid.currentIndex < grid.columns) { + filterField.forceActiveFocus(); + } else { + grid.moveSelection(-grid.columns); + } + event.accepted = true; + } else if (event.key === Qt.Key_Down) { + grid.moveSelection(grid.columns); + event.accepted = true; + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + grid.activateCurrent(); + event.accepted = true; + } else if (event.key === Qt.Key_Backspace) { + if (filterField.text.length > 0) { + filterField.text = filterField.text.substring(0, filterField.text.length - 1); + } + filterField.forceActiveFocus(); + event.accepted = true; + } else { + filterField.forceActiveFocus(); + if (event.text.length > 0) { + filterField.text += event.text; + filterField.cursorPosition = filterField.text.length; + } + event.accepted = true; + } + } + + GridView { + id: grid + visible: panelWindow.filteredWallpapers.length > 0 + + property int currentIndex: 0 + readonly property int columns: root.columns + readonly property int rows: Math.max(1, Math.ceil(count / columns)) + + anchors.fill: parent + cellWidth: root.thumbnailWidth + cellHeight: root.thumbnailHeight + clip: true + interactive: true + keyNavigationWraps: true + boundsBehavior: Flickable.StopAtBounds + + cacheBuffer: cellHeight * 2 + ScrollBar.horizontal: ScrollBar { + policy: ScrollBar.AsNeeded + } + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + } + + model: panelWindow.filteredWallpapers + onModelChanged: currentIndex = 0 + + function moveSelection(delta) { + for (let i = 0; i < count; i++) { + const item = itemAtIndex(i); + if (item) { + item.isHovered = false; + } + } + currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); + positionViewAtIndex(currentIndex, GridView.Contain); + } + function activateCurrent() { + const path = model[currentIndex]; + if (!path) + return; + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(path); + } + + delegate: Item { + width: grid.cellWidth + height: grid.cellHeight + property bool isHovered: false + + Rectangle { + anchors.fill: parent + radius: Appearance.rounding.windowRounding + color: Appearance.colors.colLayer1 + border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 + border.color: Appearance.colors.colSecondary + } + + Rectangle { + anchors.fill: parent + anchors.margins: 8 + color: Appearance.colors.colLayer2 + radius: Appearance.rounding.small + + Image { + id: thumbnailImage + anchors.fill: parent + source: { + const resolvedUrl = Qt.resolvedUrl(modelData); + const md5Hash = Qt.md5(resolvedUrl); + const cacheSize = "normal" + const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; + return thumbnailPath + } + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: false + smooth: true + mipmap: false + + sourceSize.width: Math.min(128, grid.cellWidth - 16) + sourceSize.height: Math.min(96, grid.cellHeight - 16) + + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: thumbnailImage.width + height: thumbnailImage.height + radius: Appearance.rounding.small + } + } + + Rectangle { + anchors.fill: parent + color: "transparent" + border.width: 1 + border.color: Appearance.colors.colOutlineVariant + radius: Appearance.rounding.small + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + for (let i = 0; i < grid.count; i++) { + const item = grid.itemAtIndex(i); + if (item && item !== parent) { + item.isHovered = false; + } + } + parent.isHovered = true; + grid.currentIndex = index; + } + onExited: { + parent.isHovered = false; + } + onClicked: { + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(modelData); + } + } + } + + add: Transition { + from: "*" + to: "*" + ParallelAnimation { + PropertyAnimation { + property: "x" + from: grid.contentX + (grid.width / 2) - width / 2 + } + PropertyAnimation { + property: "y" + from: grid.contentY + (grid.height / 2) - height / 2 + } + NumberAnimation { + property: "scale" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + NumberAnimation { + property: "opacity" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + } + } + } + + Label { + id: noWallpapersFoundLabel + visible: panelWindow.filteredWallpapers.length === 0 + anchors.centerIn: parent + text: "No wallpapers found" + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colSubtext + } + } + } + + Connections { + target: GlobalStates + function onWallpaperSelectorOpenChanged() { + if (GlobalStates.wallpaperSelectorOpen && monitorIsFocused) { + filterField.forceActiveFocus(); + } + } + } + +} \ No newline at end of file From dac9ed27850308f3740b334c001b6ea27a39d9cc Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Fri, 22 Aug 2025 22:03:12 +0700 Subject: [PATCH 17/35] wallpaper selector: add shadows --- .../WallpaperSelectorContent.qml | 559 +++++++++--------- 1 file changed, 288 insertions(+), 271 deletions(-) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 84a7e9611..bd2be581a 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -19,326 +19,344 @@ Item { property int thumbnailHeight: 108 implicitHeight: columnLayout.implicitHeight implicitWidth: columnLayout.implicitWidth - + ColumnLayout { id: columnLayout anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top spacing: 8 - TextField { - id: filterField + Item { + implicitHeight: filterField.implicitHeight + implicitWidth: filterField.implicitWidth Layout.alignment: Qt.AlignHCenter - implicitHeight: 40 - implicitWidth: Appearance.sizes.searchWidth - padding: 10 - placeholderText: "Search wallpapers..." - placeholderTextColor: Appearance.colors.colSubtext - color: Appearance.colors.colPrimary - background: Rectangle { - color: Appearance.colors.colLayer0 - border.color: Appearance.colors.colLayer0Border - border.width: 1 - radius: Appearance.rounding.small - } - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - onTextChanged: { - let newModel = []; - if (text.length > 0) { - for (let i = 0; i < Wallpapers.wallpapers.length; ++i) { - let wallpaperPath = Wallpapers.wallpapers[i]; - if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) { - newModel.push(wallpaperPath); + StyledRectangularShadow { + target: filterField + } + + TextField { + id: filterField + implicitHeight: 40 + implicitWidth: Appearance.sizes.searchWidth + padding: 10 + placeholderText: "Search wallpapers..." + placeholderTextColor: Appearance.colors.colSubtext + color: Appearance.colors.colPrimary + background: Rectangle { + color: Appearance.colors.colLayer0 + border.color: Appearance.colors.colLayer0Border + border.width: 1 + radius: Appearance.rounding.small + } + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal + + onTextChanged: { + let newModel = []; + if (text.length > 0) { + for (let i = 0; i < Wallpapers.wallpapers.length; ++i) { + let wallpaperPath = Wallpapers.wallpapers[i]; + if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) { + newModel.push(wallpaperPath); + } + } + panelWindow.filteredWallpapers = newModel; + } else { + panelWindow.filteredWallpapers = Wallpapers.wallpapers; + } + } + + Keys.onPressed: event => { + if (text.length === 0) { + if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { + bg.forceActiveFocus(); + if (event.key === Qt.Key_Down) + grid.moveSelection(grid.columns); + else if (event.key === Qt.Key_Left) + grid.moveSelection(-1); + else if (event.key === Qt.Key_Right) + grid.moveSelection(1); + event.accepted = true; + } + } else { + if (event.key === Qt.Key_Down) { + grid.moveSelection(grid.columns); + event.accepted = true; + bg.forceActiveFocus(); } } - panelWindow.filteredWallpapers = newModel; - } else { - panelWindow.filteredWallpapers = Wallpapers.wallpapers; - } - } - - Keys.onPressed: event => { - if (text.length === 0) { - if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { - bg.forceActiveFocus(); - if (event.key === Qt.Key_Down) - grid.moveSelection(grid.columns); - else if (event.key === Qt.Key_Left) - grid.moveSelection(-1); - else if (event.key === Qt.Key_Right) - grid.moveSelection(1); + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + grid.activateCurrent(); + event.accepted = true; + } else if (event.key === Qt.Key_Escape) { + if (filterField.text.length > 0) { + filterField.text = ""; + } else { + GlobalStates.wallpaperSelectorOpen = false; + } event.accepted = true; } - } else { - if (event.key === Qt.Key_Down) { - grid.moveSelection(grid.columns); - event.accepted = true; - bg.forceActiveFocus(); - } - } - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - grid.activateCurrent(); - event.accepted = true; - } else if (event.key === Qt.Key_Escape) { - if (filterField.text.length > 0) { - filterField.text = ""; - } else { - GlobalStates.wallpaperSelectorOpen = false; - } - event.accepted = true; } } } - Rectangle { - id: bg - focus: true - color: Appearance.colors.colLayer0 - border.width: 1 - border.color: Appearance.colors.colLayer0Border - radius: Appearance.rounding.screenRounding - // Layout.alignment: Qt.AlignHCenter - - property int calculatedRows: Math.ceil(grid.count / grid.columns) - - implicitWidth: { - if (panelWindow.filteredWallpapers.length === 0) { - return 300; - } else if (panelWindow.filteredWallpapers.length < grid.columns) { - return panelWindow.filteredWallpapers.length * grid.cellWidth + 16; - } else { - return Math.min(panelWindow.width * 0.7, 900); - } + Item { + implicitWidth: wallpaperGridBackground.implicitWidth + implicitHeight: wallpaperGridBackground.implicitHeight + + StyledRectangularShadow { + target: wallpaperGridBackground } + Rectangle { + id: wallpaperGridBackground - implicitHeight: { - if (panelWindow.filteredWallpapers.length === 0) { - return 100; - } else { - return Math.min(panelWindow.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); - } - } + Layout.alignment: Qt.AlignHCenter + color: Appearance.colors.colLayer0 + radius: Appearance.rounding.screenRounding + border.width: 1 + border.color: Appearance.colors.colLayer0Border + focus: true - Behavior on implicitWidth { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } + property int calculatedRows: Math.ceil(grid.count / grid.columns) - Behavior on implicitHeight { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - - Keys.onPressed: event => { - if (event.key === Qt.Key_Escape) { - GlobalStates.wallpaperSelectorOpen = false; - event.accepted = true; - } else if (event.key === Qt.Key_Left) { - grid.moveSelection(-1); - event.accepted = true; - } else if (event.key === Qt.Key_Right) { - grid.moveSelection(1); - event.accepted = true; - } else if (event.key === Qt.Key_Up) { - if (grid.currentIndex < grid.columns) { - filterField.forceActiveFocus(); + implicitWidth: { + if (panelWindow.filteredWallpapers.length === 0) { + return 300; + } else if (panelWindow.filteredWallpapers.length < grid.columns) { + return panelWindow.filteredWallpapers.length * grid.cellWidth + 16; } else { - grid.moveSelection(-grid.columns); + return Math.min(panelWindow.width * 0.7, 900); } - event.accepted = true; - } else if (event.key === Qt.Key_Down) { - grid.moveSelection(grid.columns); - event.accepted = true; - } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - grid.activateCurrent(); - event.accepted = true; - } else if (event.key === Qt.Key_Backspace) { - if (filterField.text.length > 0) { - filterField.text = filterField.text.substring(0, filterField.text.length - 1); - } - filterField.forceActiveFocus(); - event.accepted = true; - } else { - filterField.forceActiveFocus(); - if (event.text.length > 0) { - filterField.text += event.text; - filterField.cursorPosition = filterField.text.length; - } - event.accepted = true; - } - } - - GridView { - id: grid - visible: panelWindow.filteredWallpapers.length > 0 - - property int currentIndex: 0 - readonly property int columns: root.columns - readonly property int rows: Math.max(1, Math.ceil(count / columns)) - - anchors.fill: parent - cellWidth: root.thumbnailWidth - cellHeight: root.thumbnailHeight - clip: true - interactive: true - keyNavigationWraps: true - boundsBehavior: Flickable.StopAtBounds - - cacheBuffer: cellHeight * 2 - ScrollBar.horizontal: ScrollBar { - policy: ScrollBar.AsNeeded - } - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded } - model: panelWindow.filteredWallpapers - onModelChanged: currentIndex = 0 + implicitHeight: { + if (panelWindow.filteredWallpapers.length === 0) { + return 100; + } else { + return Math.min(panelWindow.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); + } + } - function moveSelection(delta) { - for (let i = 0; i < count; i++) { - const item = itemAtIndex(i); - if (item) { - item.isHovered = false; + Behavior on implicitWidth { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + + Behavior on implicitHeight { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + GlobalStates.wallpaperSelectorOpen = false; + event.accepted = true; + } else if (event.key === Qt.Key_Left) { + grid.moveSelection(-1); + event.accepted = true; + } else if (event.key === Qt.Key_Right) { + grid.moveSelection(1); + event.accepted = true; + } else if (event.key === Qt.Key_Up) { + if (grid.currentIndex < grid.columns) { + filterField.forceActiveFocus(); + } else { + grid.moveSelection(-grid.columns); } + event.accepted = true; + } else if (event.key === Qt.Key_Down) { + grid.moveSelection(grid.columns); + event.accepted = true; + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + grid.activateCurrent(); + event.accepted = true; + } else if (event.key === Qt.Key_Backspace) { + if (filterField.text.length > 0) { + filterField.text = filterField.text.substring(0, filterField.text.length - 1); + } + filterField.forceActiveFocus(); + event.accepted = true; + } else { + filterField.forceActiveFocus(); + if (event.text.length > 0) { + filterField.text += event.text; + filterField.cursorPosition = filterField.text.length; + } + event.accepted = true; } - currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); - positionViewAtIndex(currentIndex, GridView.Contain); - } - function activateCurrent() { - const path = model[currentIndex]; - if (!path) - return; - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(path); } - delegate: Item { - width: grid.cellWidth - height: grid.cellHeight - property bool isHovered: false + GridView { + id: grid + visible: panelWindow.filteredWallpapers.length > 0 - Rectangle { - anchors.fill: parent - radius: Appearance.rounding.windowRounding - color: Appearance.colors.colLayer1 - border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 - border.color: Appearance.colors.colSecondary + property int currentIndex: 0 + readonly property int columns: root.columns + readonly property int rows: Math.max(1, Math.ceil(count / columns)) + + anchors.fill: parent + cellWidth: root.thumbnailWidth + cellHeight: root.thumbnailHeight + clip: true + interactive: true + keyNavigationWraps: true + boundsBehavior: Flickable.StopAtBounds + + cacheBuffer: cellHeight * 2 + ScrollBar.horizontal: ScrollBar { + policy: ScrollBar.AsNeeded + } + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded } - Rectangle { - anchors.fill: parent - anchors.margins: 8 - color: Appearance.colors.colLayer2 - radius: Appearance.rounding.small + model: panelWindow.filteredWallpapers + onModelChanged: currentIndex = 0 - Image { - id: thumbnailImage + function moveSelection(delta) { + for (let i = 0; i < count; i++) { + const item = itemAtIndex(i); + if (item) { + item.isHovered = false; + } + } + currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); + positionViewAtIndex(currentIndex, GridView.Contain); + } + function activateCurrent() { + const path = model[currentIndex]; + if (!path) + return; + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(path); + } + + delegate: Item { + width: grid.cellWidth + height: grid.cellHeight + property bool isHovered: false + + Rectangle { anchors.fill: parent - source: { - const resolvedUrl = Qt.resolvedUrl(modelData); - const md5Hash = Qt.md5(resolvedUrl); - const cacheSize = "normal" - const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; - return thumbnailPath - } - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: false - smooth: true - mipmap: false + radius: Appearance.rounding.windowRounding + color: Appearance.colors.colLayer1 + border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 + border.color: Appearance.colors.colSecondary + } - sourceSize.width: Math.min(128, grid.cellWidth - 16) - sourceSize.height: Math.min(96, grid.cellHeight - 16) + Rectangle { + anchors.fill: parent + anchors.margins: 8 + color: Appearance.colors.colLayer2 + radius: Appearance.rounding.small - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } + Image { + id: thumbnailImage + anchors.fill: parent + source: { + const resolvedUrl = Qt.resolvedUrl(modelData); + const md5Hash = Qt.md5(resolvedUrl); + const cacheSize = "normal"; + const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; + return thumbnailPath; + } + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: false + smooth: true + mipmap: false - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: thumbnailImage.width - height: thumbnailImage.height + sourceSize.width: Math.min(128, grid.cellWidth - 16) + sourceSize.height: Math.min(96, grid.cellHeight - 16) + + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: thumbnailImage.width + height: thumbnailImage.height + radius: Appearance.rounding.small + } + } + + Rectangle { + anchors.fill: parent + color: "transparent" + border.width: 1 + border.color: Appearance.colors.colOutlineVariant radius: Appearance.rounding.small } } - - Rectangle { - anchors.fill: parent - color: "transparent" - border.width: 1 - border.color: Appearance.colors.colOutlineVariant - radius: Appearance.rounding.small - } } - } - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - for (let i = 0; i < grid.count; i++) { - const item = grid.itemAtIndex(i); - if (item && item !== parent) { - item.isHovered = false; + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + for (let i = 0; i < grid.count; i++) { + const item = grid.itemAtIndex(i); + if (item && item !== parent) { + item.isHovered = false; + } } + parent.isHovered = true; + grid.currentIndex = index; + } + onExited: { + parent.isHovered = false; + } + onClicked: { + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(modelData); } - parent.isHovered = true; - grid.currentIndex = index; } - onExited: { - parent.isHovered = false; - } - onClicked: { - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(modelData); + } + + add: Transition { + from: "*" + to: "*" + ParallelAnimation { + PropertyAnimation { + property: "x" + from: grid.contentX + (grid.width / 2) - width / 2 + } + PropertyAnimation { + property: "y" + from: grid.contentY + (grid.height / 2) - height / 2 + } + NumberAnimation { + property: "scale" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } + NumberAnimation { + property: "opacity" + from: 0.0 + to: 1.0 + duration: animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: animationCurves.expressiveDefaultSpatial + } } } } - add: Transition { - from: "*" - to: "*" - ParallelAnimation { - PropertyAnimation { - property: "x" - from: grid.contentX + (grid.width / 2) - width / 2 - } - PropertyAnimation { - property: "y" - from: grid.contentY + (grid.height / 2) - height / 2 - } - NumberAnimation { - property: "scale" - from: 0.0 - to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } - NumberAnimation { - property: "opacity" - from: 0.0 - to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial - } - } + Label { + id: noWallpapersFoundLabel + visible: panelWindow.filteredWallpapers.length === 0 + anchors.centerIn: parent + text: "No wallpapers found" + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colSubtext } } - - Label { - id: noWallpapersFoundLabel - visible: panelWindow.filteredWallpapers.length === 0 - anchors.centerIn: parent - text: "No wallpapers found" - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - color: Appearance.colors.colSubtext - } } } @@ -350,5 +368,4 @@ Item { } } } - -} \ No newline at end of file +} From 28256c0a720330a6e2fc9a5e98c4e138f5f49f1d Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Fri, 22 Aug 2025 22:03:41 +0700 Subject: [PATCH 18/35] wallpaper selector: add shadows --- .../modules/wallpaperSelector/WallpaperSelectorContent.qml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index bd2be581a..981d7a43a 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -102,16 +102,15 @@ Item { } Item { + Layout.alignment: Qt.AlignHCenter implicitWidth: wallpaperGridBackground.implicitWidth implicitHeight: wallpaperGridBackground.implicitHeight - + StyledRectangularShadow { target: wallpaperGridBackground } Rectangle { id: wallpaperGridBackground - - Layout.alignment: Qt.AlignHCenter color: Appearance.colors.colLayer0 radius: Appearance.rounding.screenRounding border.width: 1 From a25a3c186b9603bed8e390183c6725c8868512ab Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Fri, 22 Aug 2025 22:47:21 +0700 Subject: [PATCH 19/35] wallpaper selector: add click outside to close --- .../wallpaperSelector/WallpaperSelector.qml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index c3ff54cd9..2fee98fdb 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -15,6 +15,7 @@ Scope { id: root Loader { + id: wallpaperSelectorLoader active: GlobalStates.wallpaperSelectorOpen sourceComponent: PanelWindow { @@ -35,12 +36,25 @@ Scope { left: true right: true } - margins { top: Appearance.sizes.barHeight + Appearance.sizes.hyprlandGapsOut } + mask: Region { + item: content + } + + HyprlandFocusGrab { // Click outside to close + id: grab + windows: [ panelWindow ] + active: wallpaperSelectorLoader.active + onCleared: () => { + if (!active) GlobalStates.wallpaperSelectorOpen = false; + } + } + WallpaperSelectorContent { + id: content anchors { top: parent.top horizontalCenter: parent.horizontalCenter From 767e35851b018ed087de9cec202ae262a39ee752 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 23 Aug 2025 09:36:43 +0700 Subject: [PATCH 20/35] wallpaper selector: move key handling to root --- .../wallpaperSelector/WallpaperSelector.qml | 2 - .../WallpaperSelectorContent.qml | 134 +++++++++--------- 2 files changed, 66 insertions(+), 70 deletions(-) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index 2fee98fdb..0a9abab5f 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -5,7 +5,6 @@ import qs.modules.common.widgets import qs.modules.common.functions import QtQuick import QtQuick.Controls -import QtQuick.Layouts import Quickshell import Quickshell.Io import Quickshell.Wayland @@ -22,7 +21,6 @@ Scope { id: panelWindow readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen) property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id) - property var filteredWallpapers: Wallpapers.wallpapers exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "quickshell:wallpaperSelector" diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 981d7a43a..670fcabce 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -15,10 +15,51 @@ import Quickshell.Hyprland Item { id: root property int columns: 4 - property int thumbnailWidth: 192 - property int thumbnailHeight: 108 + property int thumbnailWidth: 256 + property int thumbnailHeight: 144 implicitHeight: columnLayout.implicitHeight implicitWidth: columnLayout.implicitWidth + property var filteredWallpapers: Wallpapers.wallpapers + property string filterQuery: "" + + Keys.onPressed: event => { + if (event.key === Qt.Key_Escape) { + GlobalStates.wallpaperSelectorOpen = false; + event.accepted = true; + } else if (event.key === Qt.Key_Left) { + grid.moveSelection(-1); + event.accepted = true; + } else if (event.key === Qt.Key_Right) { + grid.moveSelection(1); + event.accepted = true; + } else if (event.key === Qt.Key_Up) { + if (grid.currentIndex < grid.columns) { + filterField.forceActiveFocus(); + } else { + grid.moveSelection(-grid.columns); + } + event.accepted = true; + } else if (event.key === Qt.Key_Down) { + grid.moveSelection(grid.columns); + event.accepted = true; + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + grid.activateCurrent(); + event.accepted = true; + } else if (event.key === Qt.Key_Backspace) { + if (filterField.text.length > 0) { + filterField.text = filterField.text.substring(0, filterField.text.length - 1); + } + filterField.forceActiveFocus(); + event.accepted = true; + } else { + filterField.forceActiveFocus(); + if (event.text.length > 0) { + filterField.text += event.text; + filterField.cursorPosition = filterField.text.length; + } + event.accepted = true; + } + } ColumnLayout { id: columnLayout @@ -26,7 +67,7 @@ Item { anchors.top: parent.top spacing: 8 - Item { + Item { // Search box implicitHeight: filterField.implicitHeight implicitWidth: filterField.implicitWidth Layout.alignment: Qt.AlignHCenter @@ -53,24 +94,13 @@ Item { font.pixelSize: Appearance.font.pixelSize.normal onTextChanged: { - let newModel = []; - if (text.length > 0) { - for (let i = 0; i < Wallpapers.wallpapers.length; ++i) { - let wallpaperPath = Wallpapers.wallpapers[i]; - if (wallpaperPath.toLowerCase().includes(text.toLowerCase())) { - newModel.push(wallpaperPath); - } - } - panelWindow.filteredWallpapers = newModel; - } else { - panelWindow.filteredWallpapers = Wallpapers.wallpapers; - } + root.filterQuery = text } Keys.onPressed: event => { if (text.length === 0) { if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { - bg.forceActiveFocus(); + wallpaperGrid.forceActiveFocus(); if (event.key === Qt.Key_Down) grid.moveSelection(grid.columns); else if (event.key === Qt.Key_Left) @@ -83,7 +113,7 @@ Item { if (event.key === Qt.Key_Down) { grid.moveSelection(grid.columns); event.accepted = true; - bg.forceActiveFocus(); + wallpaperGrid.forceActiveFocus(); } } if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { @@ -101,7 +131,8 @@ Item { } } - Item { + Item { // The grid + id: wallpaperGrid Layout.alignment: Qt.AlignHCenter implicitWidth: wallpaperGridBackground.implicitWidth implicitHeight: wallpaperGridBackground.implicitHeight @@ -120,17 +151,17 @@ Item { property int calculatedRows: Math.ceil(grid.count / grid.columns) implicitWidth: { - if (panelWindow.filteredWallpapers.length === 0) { + if (root.filteredWallpapers.length === 0) { return 300; - } else if (panelWindow.filteredWallpapers.length < grid.columns) { - return panelWindow.filteredWallpapers.length * grid.cellWidth + 16; + } else if (root.filteredWallpapers.length < grid.columns) { + return root.filteredWallpapers.length * grid.cellWidth + 16; } else { return Math.min(panelWindow.width * 0.7, 900); } } implicitHeight: { - if (panelWindow.filteredWallpapers.length === 0) { + if (root.filteredWallpapers.length === 0) { return 100; } else { return Math.min(panelWindow.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); @@ -145,48 +176,9 @@ Item { animation: Appearance.animation.elementMove.numberAnimation.createObject(this) } - Keys.onPressed: event => { - if (event.key === Qt.Key_Escape) { - GlobalStates.wallpaperSelectorOpen = false; - event.accepted = true; - } else if (event.key === Qt.Key_Left) { - grid.moveSelection(-1); - event.accepted = true; - } else if (event.key === Qt.Key_Right) { - grid.moveSelection(1); - event.accepted = true; - } else if (event.key === Qt.Key_Up) { - if (grid.currentIndex < grid.columns) { - filterField.forceActiveFocus(); - } else { - grid.moveSelection(-grid.columns); - } - event.accepted = true; - } else if (event.key === Qt.Key_Down) { - grid.moveSelection(grid.columns); - event.accepted = true; - } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - grid.activateCurrent(); - event.accepted = true; - } else if (event.key === Qt.Key_Backspace) { - if (filterField.text.length > 0) { - filterField.text = filterField.text.substring(0, filterField.text.length - 1); - } - filterField.forceActiveFocus(); - event.accepted = true; - } else { - filterField.forceActiveFocus(); - if (event.text.length > 0) { - filterField.text += event.text; - filterField.cursorPosition = filterField.text.length; - } - event.accepted = true; - } - } - GridView { id: grid - visible: panelWindow.filteredWallpapers.length > 0 + visible: root.filteredWallpapers.length > 0 property int currentIndex: 0 readonly property int columns: root.columns @@ -208,7 +200,13 @@ Item { policy: ScrollBar.AsNeeded } - model: panelWindow.filteredWallpapers + model: ScriptModel { + values: { + return root.filteredWallpapers.filter(w => ( + w.toLowerCase().includes(root.filterQuery.toLowerCase()) + )); + } + } onModelChanged: currentIndex = 0 function moveSelection(delta) { @@ -332,15 +330,15 @@ Item { property: "scale" from: 0.0 to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial + duration: Appearance.animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: Appearance.animationCurves.expressiveDefaultSpatial } NumberAnimation { property: "opacity" from: 0.0 to: 1.0 - duration: animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: animationCurves.expressiveDefaultSpatial + duration: Appearance.animationCurves.expressiveDefaultSpatialDuration + easing.bezierCurve: Appearance.animationCurves.expressiveDefaultSpatial } } } @@ -348,7 +346,7 @@ Item { Label { id: noWallpapersFoundLabel - visible: panelWindow.filteredWallpapers.length === 0 + visible: root.filteredWallpapers.length === 0 anchors.centerIn: parent text: "No wallpapers found" font.family: Appearance.font.family.main From 0e2eea7555813fe77e6b8c558aab06c87021de88 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 23 Aug 2025 10:13:52 +0700 Subject: [PATCH 21/35] wallpaper selector: unfuck grid placement --- .../ii/modules/common/Appearance.qml | 2 + .../wallpaperSelector/WallpaperSelector.qml | 13 +- .../WallpaperSelectorContent.qml | 198 +++++++----------- 3 files changed, 81 insertions(+), 132 deletions(-) diff --git a/.config/quickshell/ii/modules/common/Appearance.qml b/.config/quickshell/ii/modules/common/Appearance.qml index 870bbfe36..e1f0853a5 100644 --- a/.config/quickshell/ii/modules/common/Appearance.qml +++ b/.config/quickshell/ii/modules/common/Appearance.qml @@ -343,6 +343,8 @@ Singleton { property real baseVerticalBarWidth: 46 property real verticalBarWidth: Config.options.bar.cornerStyle === 1 ? (baseVerticalBarWidth + root.sizes.hyprlandGapsOut * 2) : baseVerticalBarWidth + property real wallpaperSelectorWidth: 1000 + property real wallpaperSelectorHeight: 580 } syntaxHighlightingTheme: root.m3colors.darkmode ? "Monokai" : "ayu Light" diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index 0a9abab5f..76923705f 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -28,12 +28,7 @@ Scope { WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand color: "transparent" - anchors { - top: true - bottom: true - left: true - right: true - } + anchors.top: true margins { top: Appearance.sizes.barHeight + Appearance.sizes.hyprlandGapsOut } @@ -42,6 +37,9 @@ Scope { item: content } + implicitHeight: Appearance.sizes.wallpaperSelectorHeight + implicitWidth: Appearance.sizes.wallpaperSelectorWidth + HyprlandFocusGrab { // Click outside to close id: grab windows: [ panelWindow ] @@ -54,8 +52,7 @@ Scope { WallpaperSelectorContent { id: content anchors { - top: parent.top - horizontalCenter: parent.horizontalCenter + fill: parent } } } diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 670fcabce..52a75658b 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -15,8 +15,7 @@ import Quickshell.Hyprland Item { id: root property int columns: 4 - property int thumbnailWidth: 256 - property int thumbnailHeight: 144 + property real previewAspectRatio: 16 / 9 implicitHeight: columnLayout.implicitHeight implicitWidth: columnLayout.implicitWidth property var filteredWallpapers: Wallpapers.wallpapers @@ -63,13 +62,13 @@ Item { ColumnLayout { id: columnLayout - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - spacing: 8 + anchors.fill: parent + spacing: -Appearance.sizes.elevationMargin - Item { // Search box - implicitHeight: filterField.implicitHeight - implicitWidth: filterField.implicitWidth + Item { + // Search box + implicitHeight: filterField.implicitHeight + Appearance.sizes.elevationMargin * 2 + implicitWidth: filterField.implicitWidth + Appearance.sizes.elevationMargin * 2 Layout.alignment: Qt.AlignHCenter StyledRectangularShadow { @@ -78,7 +77,11 @@ Item { TextField { id: filterField - implicitHeight: 40 + anchors { + fill: parent + margins: Appearance.sizes.elevationMargin + } + implicitHeight: 44 implicitWidth: Appearance.sizes.searchWidth padding: 10 placeholderText: "Search wallpapers..." @@ -94,7 +97,7 @@ Item { font.pixelSize: Appearance.font.pixelSize.normal onTextChanged: { - root.filterQuery = text + root.filterQuery = text; } Keys.onPressed: event => { @@ -133,48 +136,38 @@ Item { Item { // The grid id: wallpaperGrid - Layout.alignment: Qt.AlignHCenter - implicitWidth: wallpaperGridBackground.implicitWidth - implicitHeight: wallpaperGridBackground.implicitHeight + Layout.fillWidth: true + Layout.fillHeight: true + implicitWidth: wallpaperGridBackground.implicitWidth + Appearance.sizes.elevationMargin * 2 + implicitHeight: wallpaperGridBackground.implicitHeight + Appearance.sizes.elevationMargin * 2 StyledRectangularShadow { target: wallpaperGridBackground } Rectangle { id: wallpaperGridBackground - color: Appearance.colors.colLayer0 - radius: Appearance.rounding.screenRounding + anchors { + fill: parent + margins: Appearance.sizes.elevationMargin + } + focus: true border.width: 1 border.color: Appearance.colors.colLayer0Border - focus: true + color: Appearance.colors.colLayer0 + radius: Appearance.rounding.screenRounding + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: wallpaperGridBackground.width + height: wallpaperGridBackground.height + radius: wallpaperGridBackground.radius + } + } property int calculatedRows: Math.ceil(grid.count / grid.columns) - implicitWidth: { - if (root.filteredWallpapers.length === 0) { - return 300; - } else if (root.filteredWallpapers.length < grid.columns) { - return root.filteredWallpapers.length * grid.cellWidth + 16; - } else { - return Math.min(panelWindow.width * 0.7, 900); - } - } - - implicitHeight: { - if (root.filteredWallpapers.length === 0) { - return 100; - } else { - return Math.min(panelWindow.height * 0.6, Math.min(calculatedRows, 3) * grid.cellHeight + 16); - } - } - - Behavior on implicitWidth { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - - Behavior on implicitHeight { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } + implicitWidth: grid.implicitWidth + implicitHeight: grid.implicitHeight GridView { id: grid @@ -185,8 +178,8 @@ Item { readonly property int rows: Math.max(1, Math.ceil(count / columns)) anchors.fill: parent - cellWidth: root.thumbnailWidth - cellHeight: root.thumbnailHeight + cellWidth: width / root.columns + cellHeight: cellWidth / root.previewAspectRatio clip: true interactive: true keyNavigationWraps: true @@ -202,9 +195,7 @@ Item { model: ScriptModel { values: { - return root.filteredWallpapers.filter(w => ( - w.toLowerCase().includes(root.filterQuery.toLowerCase()) - )); + return root.filteredWallpapers.filter(w => (w.toLowerCase().includes(root.filterQuery.toLowerCase()))); } } onModelChanged: currentIndex = 0 @@ -233,61 +224,49 @@ Item { height: grid.cellHeight property bool isHovered: false - Rectangle { - anchors.fill: parent - radius: Appearance.rounding.windowRounding - color: Appearance.colors.colLayer1 - border.width: (index === grid.currentIndex || parent.isHovered) ? 3 : 0 - border.color: Appearance.colors.colSecondary - } + Image { + id: thumbnailImage + anchors { + fill: parent + margins: 8 + } + source: { + const resolvedUrl = Qt.resolvedUrl(modelData); + const md5Hash = Qt.md5(resolvedUrl); + const cacheSize = "normal"; + const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; + return thumbnailPath; + } + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: false + smooth: true + mipmap: false - Rectangle { - anchors.fill: parent - anchors.margins: 8 - color: Appearance.colors.colLayer2 - radius: Appearance.rounding.small + sourceSize.width: Math.min(128, grid.cellWidth - 16) + sourceSize.height: Math.min(96, grid.cellHeight - 16) - Image { - id: thumbnailImage - anchors.fill: parent - source: { - const resolvedUrl = Qt.resolvedUrl(modelData); - const md5Hash = Qt.md5(resolvedUrl); - const cacheSize = "normal"; - const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; - return thumbnailPath; - } - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: false - smooth: true - mipmap: false + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } - sourceSize.width: Math.min(128, grid.cellWidth - 16) - sourceSize.height: Math.min(96, grid.cellHeight - 16) - - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: thumbnailImage.width - height: thumbnailImage.height - radius: Appearance.rounding.small - } - } - - Rectangle { - anchors.fill: parent - color: "transparent" - border.width: 1 - border.color: Appearance.colors.colOutlineVariant + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: thumbnailImage.width + height: thumbnailImage.height radius: Appearance.rounding.small } } + + Rectangle { + anchors.fill: parent + color: "transparent" + radius: Appearance.rounding.small + border.width: (index === grid.currentIndex || parent.isHovered) ? 2 : 1 + border.color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colSecondary : Appearance.colors.colOutlineVariant + } } MouseArea { @@ -313,35 +292,6 @@ Item { } } } - - add: Transition { - from: "*" - to: "*" - ParallelAnimation { - PropertyAnimation { - property: "x" - from: grid.contentX + (grid.width / 2) - width / 2 - } - PropertyAnimation { - property: "y" - from: grid.contentY + (grid.height / 2) - height / 2 - } - NumberAnimation { - property: "scale" - from: 0.0 - to: 1.0 - duration: Appearance.animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: Appearance.animationCurves.expressiveDefaultSpatial - } - NumberAnimation { - property: "opacity" - from: 0.0 - to: 1.0 - duration: Appearance.animationCurves.expressiveDefaultSpatialDuration - easing.bezierCurve: Appearance.animationCurves.expressiveDefaultSpatial - } - } - } } Label { From a116ae6ab56bbf4003a525247e3b502f0b0a21c1 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 23 Aug 2025 12:25:34 +0700 Subject: [PATCH 22/35] wallpaper selector: nicer layout --- .../wallpaperSelector/WallpaperSelector.qml | 2 +- .../WallpaperSelectorContent.qml | 480 +++++++++++------- .config/quickshell/ii/services/Wallpapers.qml | 4 + 3 files changed, 298 insertions(+), 188 deletions(-) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index 76923705f..24cad7972 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -30,7 +30,7 @@ Scope { anchors.top: true margins { - top: Appearance.sizes.barHeight + Appearance.sizes.hyprlandGapsOut + top: Config?.options.bar.vertical ? Appearance.sizes.hyprlandGapsOut : Appearance.sizes.barHeight + Appearance.sizes.hyprlandGapsOut } mask: Region { diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 52a75658b..c47cf2ef8 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -15,10 +15,10 @@ import Quickshell.Hyprland Item { id: root property int columns: 4 - property real previewAspectRatio: 16 / 9 + property real previewCellAspectRatio: 4 / 3 implicitHeight: columnLayout.implicitHeight implicitWidth: columnLayout.implicitWidth - property var filteredWallpapers: Wallpapers.wallpapers + property var wallpapers: Wallpapers.wallpapers property string filterQuery: "" Keys.onPressed: event => { @@ -65,75 +65,6 @@ Item { anchors.fill: parent spacing: -Appearance.sizes.elevationMargin - Item { - // Search box - implicitHeight: filterField.implicitHeight + Appearance.sizes.elevationMargin * 2 - implicitWidth: filterField.implicitWidth + Appearance.sizes.elevationMargin * 2 - Layout.alignment: Qt.AlignHCenter - - StyledRectangularShadow { - target: filterField - } - - TextField { - id: filterField - anchors { - fill: parent - margins: Appearance.sizes.elevationMargin - } - implicitHeight: 44 - implicitWidth: Appearance.sizes.searchWidth - padding: 10 - placeholderText: "Search wallpapers..." - placeholderTextColor: Appearance.colors.colSubtext - color: Appearance.colors.colPrimary - background: Rectangle { - color: Appearance.colors.colLayer0 - border.color: Appearance.colors.colLayer0Border - border.width: 1 - radius: Appearance.rounding.small - } - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - - onTextChanged: { - root.filterQuery = text; - } - - Keys.onPressed: event => { - if (text.length === 0) { - if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { - wallpaperGrid.forceActiveFocus(); - if (event.key === Qt.Key_Down) - grid.moveSelection(grid.columns); - else if (event.key === Qt.Key_Left) - grid.moveSelection(-1); - else if (event.key === Qt.Key_Right) - grid.moveSelection(1); - event.accepted = true; - } - } else { - if (event.key === Qt.Key_Down) { - grid.moveSelection(grid.columns); - event.accepted = true; - wallpaperGrid.forceActiveFocus(); - } - } - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - grid.activateCurrent(); - event.accepted = true; - } else if (event.key === Qt.Key_Escape) { - if (filterField.text.length > 0) { - filterField.text = ""; - } else { - GlobalStates.wallpaperSelectorOpen = false; - } - event.accepted = true; - } - } - } - } - Item { // The grid id: wallpaperGrid Layout.fillWidth: true @@ -154,7 +85,7 @@ Item { border.width: 1 border.color: Appearance.colors.colLayer0Border color: Appearance.colors.colLayer0 - radius: Appearance.rounding.screenRounding + radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1 layer.enabled: true layer.effect: OpacityMask { maskSource: Rectangle { @@ -166,142 +97,317 @@ Item { property int calculatedRows: Math.ceil(grid.count / grid.columns) - implicitWidth: grid.implicitWidth - implicitHeight: grid.implicitHeight - - GridView { - id: grid - visible: root.filteredWallpapers.length > 0 - - property int currentIndex: 0 - readonly property int columns: root.columns - readonly property int rows: Math.max(1, Math.ceil(count / columns)) + // implicitWidth: gridColumnLayout.implicitWidth + // implicitHeight: gridColumnLayout.implicitHeight + Item { + // The grid anchors.fill: parent - cellWidth: width / root.columns - cellHeight: cellWidth / root.previewAspectRatio - clip: true - interactive: true - keyNavigationWraps: true - boundsBehavior: Flickable.StopAtBounds - cacheBuffer: cellHeight * 2 - ScrollBar.horizontal: ScrollBar { - policy: ScrollBar.AsNeeded - } - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded - } + GridView { + id: grid + visible: root.wallpapers.length > 0 - model: ScriptModel { - values: { - return root.filteredWallpapers.filter(w => (w.toLowerCase().includes(root.filterQuery.toLowerCase()))); + property int currentIndex: 0 + readonly property int columns: root.columns + readonly property int rows: Math.max(1, Math.ceil(count / columns)) + + anchors.fill: parent + cellWidth: width / root.columns + cellHeight: cellWidth / root.previewCellAspectRatio + clip: true + interactive: true + keyNavigationWraps: true + boundsBehavior: Flickable.StopAtBounds + + ScrollBar.horizontal: ScrollBar { + policy: ScrollBar.AsNeeded + } + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded } - } - onModelChanged: currentIndex = 0 - function moveSelection(delta) { - for (let i = 0; i < count; i++) { - const item = itemAtIndex(i); - if (item) { - item.isHovered = false; + model: ScriptModel { + values: { + let filtered = root.wallpapers.filter(w => (w.toLowerCase().includes(root.filterQuery.toLowerCase()))); + // Add 'columns' empty entries to the end + for (let i = 0; i < root.columns; i++) { + filtered.push(""); + } + return filtered; } } - currentIndex = Math.max(0, Math.min(count - 1, currentIndex + delta)); - positionViewAtIndex(currentIndex, GridView.Contain); - } - function activateCurrent() { - const path = model[currentIndex]; - if (!path) - return; - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(path); - } + onModelChanged: currentIndex = 0 - delegate: Item { - width: grid.cellWidth - height: grid.cellHeight - property bool isHovered: false - - Image { - id: thumbnailImage - anchors { - fill: parent - margins: 8 - } - source: { - const resolvedUrl = Qt.resolvedUrl(modelData); - const md5Hash = Qt.md5(resolvedUrl); - const cacheSize = "normal"; - const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; - return thumbnailPath; - } - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: false - smooth: true - mipmap: false - - sourceSize.width: Math.min(128, grid.cellWidth - 16) - sourceSize.height: Math.min(96, grid.cellHeight - 16) - - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: thumbnailImage.width - height: thumbnailImage.height - radius: Appearance.rounding.small + function moveSelection(delta) { + for (let i = 0; i < count; i++) { + const item = itemAtIndex(i); + if (item) { + item.isHovered = false; } } + currentIndex = Math.max(0, Math.min(root.wallpapers.length - 1, currentIndex + delta)); + positionViewAtIndex(currentIndex, GridView.Contain); + } + function activateCurrent() { + const path = model[currentIndex]; + if (!path) + return; + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(path); + } + + delegate: Item { + id: wallpaperItem + required property var modelData + required property int index + visible: modelData.length > 0 + width: grid.cellWidth + height: grid.cellHeight + property bool isHovered: false Rectangle { - anchors.fill: parent - color: "transparent" - radius: Appearance.rounding.small - border.width: (index === grid.currentIndex || parent.isHovered) ? 2 : 1 - border.color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colSecondary : Appearance.colors.colOutlineVariant - } - } + anchors { + fill: parent + margins: 8 + } + radius: Appearance.rounding.normal + color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colPrimary : ColorUtils.transparentize(Appearance.colors.colPrimary) - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - for (let i = 0; i < grid.count; i++) { - const item = grid.itemAtIndex(i); - if (item && item !== parent) { - item.isHovered = false; + ColumnLayout { + id: wallpaperItemColumnLayout + anchors { + fill: parent + margins: 6 + } + spacing: 4 + + Item { + id: wallpaperItemImageContainer + Layout.fillHeight: true + Layout.fillWidth: true + + StyledRectangularShadow { + target: thumbnailImageLoader + radius: Appearance.rounding.small + } + + Loader { + id: thumbnailImageLoader + anchors.fill: parent + active: wallpaperItem.visible + sourceComponent: Image { + id: thumbnailImage + source: { + if (wallpaperItem.modelData.length == 0) + return; + const resolvedUrl = Qt.resolvedUrl(wallpaperItem.modelData); + const md5Hash = Qt.md5(resolvedUrl); + const cacheSize = "normal"; + const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; + return thumbnailPath; + } + asynchronous: true + cache: false + smooth: true + mipmap: false + + fillMode: Image.PreserveAspectCrop + clip: true + sourceSize.width: wallpaperItemColumnLayout.width + sourceSize.height: wallpaperItemColumnLayout.height - wallpaperItemColumnLayout.spacing - wallpaperItemName.height + + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: wallpaperItemImageContainer.width + height: wallpaperItemImageContainer.height + radius: Appearance.rounding.small + } + } + } + } + } + + StyledText { + id: wallpaperItemName + Layout.fillWidth: true + Layout.leftMargin: 10 + Layout.rightMargin: 10 + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + font.pixelSize: Appearance.font.pixelSize.smaller + color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer0 + text: FileUtils.fileNameForPath(wallpaperItem.modelData) } } - parent.isHovered = true; - grid.currentIndex = index; } - onExited: { - parent.isHovered = false; - } - onClicked: { - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(modelData); + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + for (let i = 0; i < grid.count; i++) { + const item = grid.itemAtIndex(i); + if (item && item !== parent) { + item.isHovered = false; + } + } + parent.isHovered = true; + grid.currentIndex = index; + } + onExited: { + parent.isHovered = false; + } + onClicked: { + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(wallpaperItem.modelData); + } } } } - } - Label { - id: noWallpapersFoundLabel - visible: root.filteredWallpapers.length === 0 - anchors.centerIn: parent - text: "No wallpapers found" - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - color: Appearance.colors.colSubtext + Label { + id: noWallpapersFoundLabel + visible: grid.model.values.length === 0 + anchors.centerIn: parent + text: "No wallpapers found" + font.family: Appearance.font.family.main + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colSubtext + } + + StyledRectangularShadow { + target: extraOptionsBackground + } + + Rectangle { // Bottom toolbar + id: extraOptionsBackground + property real padding: 6 + anchors { + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + bottomMargin: 8 + } + color: Appearance.colors.colLayer2 + implicitHeight: extraOptionsRowLayout.implicitHeight + padding * 2 + implicitWidth: extraOptionsRowLayout.implicitWidth + padding * 2 + radius: Appearance.rounding.full + + RowLayout { + id: extraOptionsRowLayout + anchors { + fill: parent + margins: extraOptionsBackground.padding + } + + RippleButton { + Layout.fillHeight: true + Layout.topMargin: 2 + Layout.bottomMargin: 2 + buttonRadius: Appearance.rounding.full + onClicked: { + Wallpapers.openFallbackPicker(); + GlobalStates.wallpaperSelectorOpen = false; + } + contentItem: RowLayout { + MaterialSymbol { + text: "files" + iconSize: Appearance.font.pixelSize.larger + } + StyledText { + text: Translation.tr("System") + } + } + StyledToolTip { + content: "Use the system file picker instead" + } + } + + TextField { + id: filterField + Layout.fillHeight: true + Layout.topMargin: 2 + Layout.bottomMargin: 2 + implicitWidth: 200 + padding: 10 + placeholderText: Translation.tr("Search wallpapers...") + placeholderTextColor: Appearance.colors.colSubtext + color: Appearance.colors.colOnLayer0 + font.pixelSize: Appearance.font.pixelSize.small + renderType: Text.NativeRendering + selectedTextColor: Appearance.m3colors.m3onSecondaryContainer + selectionColor: Appearance.colors.colSecondaryContainer + background: Rectangle { + color: Appearance.colors.colLayer1 + radius: Appearance.rounding.full + } + + onTextChanged: { + root.filterQuery = text; + } + + Keys.onPressed: event => { + if (text.length === 0) { + if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { + wallpaperGrid.forceActiveFocus(); + if (event.key === Qt.Key_Down) + grid.moveSelection(grid.columns); + else if (event.key === Qt.Key_Left) + grid.moveSelection(-1); + else if (event.key === Qt.Key_Right) + grid.moveSelection(1); + event.accepted = true; + } + } else { + if (event.key === Qt.Key_Down) { + grid.moveSelection(grid.columns); + event.accepted = true; + wallpaperGrid.forceActiveFocus(); + } + } + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + grid.activateCurrent(); + event.accepted = true; + } else if (event.key === Qt.Key_Escape) { + if (filterField.text.length > 0) { + filterField.text = ""; + } else { + GlobalStates.wallpaperSelectorOpen = false; + } + event.accepted = true; + } + } + } + + RippleButton { + Layout.fillHeight: true + Layout.topMargin: 2 + Layout.bottomMargin: 2 + buttonRadius: Appearance.rounding.full + onClicked: { + GlobalStates.wallpaperSelectorOpen = false; + } + implicitWidth: height + + contentItem: MaterialSymbol { + text: "close" + iconSize: Appearance.font.pixelSize.larger + } + + StyledToolTip { + content: "Cancel" + } + } + } + } } } } diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index 98b78b0b6..ef28e9470 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -33,6 +33,10 @@ Singleton { } onSearchDirsChanged: reload() + function openFallbackPicker() { + applyProc.exec([Directories.wallpaperSwitchScriptPath]) + } + function apply(path) { if (!path || path.length === 0) return applyProc.exec([ From c0933a3b20f99315a46c94621fb9a84453f17d72 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 23 Aug 2025 15:59:30 +0700 Subject: [PATCH 23/35] wallpaper selector: make it slide from top --- .config/hypr/hyprland/rules.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/.config/hypr/hyprland/rules.conf b/.config/hypr/hyprland/rules.conf index b31b30581..482114f15 100644 --- a/.config/hypr/hyprland/rules.conf +++ b/.config/hypr/hyprland/rules.conf @@ -135,6 +135,7 @@ layerrule = animation slide, quickshell:verticalBar layerrule = animation fade, quickshell:screenCorners layerrule = animation slide right, quickshell:sidebarRight layerrule = animation slide left, quickshell:sidebarLeft +layerrule = animation slide top, quickshell:wallpaperSelector layerrule = animation slide bottom, quickshell:osk layerrule = animation slide bottom, quickshell:dock layerrule = blur, quickshell:session From 18ad260ce93497b9d2dc21288f27a7e3ed316c03 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 23 Aug 2025 15:59:50 +0700 Subject: [PATCH 24/35] wallpaper service: simplify dir setting --- .config/quickshell/ii/services/Wallpapers.qml | 41 +++++-------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index ef28e9470..eaa5fd22d 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -9,30 +9,23 @@ pragma ComponentBehavior: Bound /** * Provides a list of wallpapers and an "apply" action that calls the existing - * switchwall.sh script. Uses QML's built-in image scaling for thumbnails. + * switchwall.sh script. Pretty much a limited file browsing service. */ Singleton { id: root - // Directory to search for wallpapers (new location) - // Resolves to: ~/Pictures/Wallpapers - property list searchDirs: [ FileUtils.trimFileProtocol(`${Directories.pictures}/Wallpapers`) ] - - // Supported image extensions. Videos are intentionally excluded for now - // to keep the overview lightweight. - readonly property list extensions: [ + property string searchDir: FileUtils.trimFileProtocol(`${Directories.pictures}/Wallpapers`) + readonly property list extensions: [ // TODO: add videos "jpg", "jpeg", "png", "webp", "avif", "bmp", "svg" ] + property alias filesModel: files // Expose for direct binding when needed + property list wallpapers: [] // List of absolute file paths (without file://) - // Resulting list of absolute file paths (without file:// prefix) - property list wallpapers: [] - - // Public API (FolderListModel driven) - function reload() { - files.folder = Qt.resolvedUrl(root.searchDirs[0]) + // Executions + Process { + id: applyProc } - onSearchDirsChanged: reload() - + function openFallbackPicker() { applyProc.exec([Directories.wallpaperSwitchScriptPath]) } @@ -49,14 +42,14 @@ Singleton { // Folder model FolderListModel { id: files - nameFilters: extensions.map(ext => `*.${ext}`) + folder: Qt.resolvedUrl(root.searchDir) + nameFilters: root.extensions.map(ext => `*.${ext}`) showDirs: false showDotAndDotDot: false showOnlyReadable: true sortField: FolderListModel.Time sortReversed: false onCountChanged: { - console.log(`[Wallpapers] FolderListModel count=${files.count} folder=${files.folder}`) root.wallpapers = [] for (let i = 0; i < files.count; i++) { const path = files.get(i, "filePath") || FileUtils.trimFileProtocol(files.get(i, "fileURL")) @@ -64,16 +57,4 @@ Singleton { } } } - - // Expose the model for direct binding when needed - property alias filesModel: files - - // Internal: applying a wallpaper - Process { - id: applyProc - } - - Component.onCompleted: reload() } - - From 8e6582b8012707ed00aafbc97fce5c30ad691a7a Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 23 Aug 2025 21:06:36 +0700 Subject: [PATCH 25/35] wallpaper selector: add address bar --- .../ii/modules/common/AddressBar.qml | 94 +++ .../ii/modules/common/functions/FileUtils.qml | 28 + .../common/widgets/AddressBreadcrumb.qml | 38 ++ .../wallpaperSelector/WallpaperSelector.qml | 1 - .../WallpaperSelectorContent.qml | 548 +++++++++--------- .config/quickshell/ii/services/Wallpapers.qml | 4 +- 6 files changed, 444 insertions(+), 269 deletions(-) create mode 100644 .config/quickshell/ii/modules/common/AddressBar.qml create mode 100644 .config/quickshell/ii/modules/common/widgets/AddressBreadcrumb.qml diff --git a/.config/quickshell/ii/modules/common/AddressBar.qml b/.config/quickshell/ii/modules/common/AddressBar.qml new file mode 100644 index 000000000..b2c1f2bed --- /dev/null +++ b/.config/quickshell/ii/modules/common/AddressBar.qml @@ -0,0 +1,94 @@ +import QtQuick +import QtQuick.Layouts +import qs +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions + +Rectangle { + id: root + required property var directory + property bool showBreadcrumb: true // TODO: make this work + + signal navigateToDirectory(string path) + + property real padding: 6 + implicitWidth: mainLayout.implicitWidth + padding * 2 + implicitHeight: mainLayout.implicitHeight + padding * 2 + color: Appearance.colors.colLayer2 + + RowLayout { + id: mainLayout + anchors { + fill: parent + margins: root.padding + } + spacing: 8 + + RippleButton { + id: parentDirButton + onClicked: root.navigateToDirectory(FileUtils.parentDirectory(root.directory)) + contentItem: MaterialSymbol { + text: "drive_folder_upload" + iconSize: Appearance.font.pixelSize.larger + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + Loader { + active: !root.showBreadcrumb + visible: !root.showBreadcrumb + anchors.fill: parent + sourceComponent: Rectangle { + color: Appearance.colors.colLayer1 + radius: Appearance.rounding.full + implicitWidth: addressInput.implicitWidth + implicitHeight: addressInput.implicitHeight + + StyledTextInput { + id: addressInput + anchors.fill: parent + padding: 10 + text: root.directory + + onAccepted: root.navigateToDirectory(text) + + MouseArea { + // I-beam cursor + anchors.fill: parent + acceptedButtons: Qt.NoButton + hoverEnabled: true + cursorShape: Qt.IBeamCursor + } + } + } + } + + Loader { + active: root.showBreadcrumb + visible: root.showBreadcrumb + anchors.fill: parent + sourceComponent: AddressBreadcrumb { + directory: root.directory + onNavigateToDirectory: (dir) => { + root.navigateToDirectory(dir) + } + } + } + } + + RippleButton { + id: dirEditButton + toggled: !root.showBreadcrumb + onClicked: root.showBreadcrumb = !root.showBreadcrumb + contentItem: MaterialSymbol { + text: "edit" + iconSize: Appearance.font.pixelSize.larger + color: dirEditButton.toggled ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer2 + } + } + } +} diff --git a/.config/quickshell/ii/modules/common/functions/FileUtils.qml b/.config/quickshell/ii/modules/common/functions/FileUtils.qml index c051674ea..4ed8d8cb1 100644 --- a/.config/quickshell/ii/modules/common/functions/FileUtils.qml +++ b/.config/quickshell/ii/modules/common/functions/FileUtils.qml @@ -24,6 +24,20 @@ Singleton { return trimmed.split(/[\\/]/).pop(); } + /** + * Extracts the folder name from a directory path + * @param {string} str + * @returns {string} + */ + function folderNameForPath(str) { + if (typeof str !== "string") return ""; + const trimmed = trimFileProtocol(str); + // Remove trailing slash if present + const noTrailing = trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed; + if (!noTrailing) return ""; + return noTrailing.split(/[\\/]/).pop(); + } + /** * Removes the file extension from a file path or name * @param {string} str @@ -38,4 +52,18 @@ Singleton { } return trimmed; } + + /** + * Returns the parent directory of a given file path + * @param {string} str + * @returns {string} + */ + function parentDirectory(str) { + if (typeof str !== "string") return ""; + const trimmed = trimFileProtocol(str); + const parts = trimmed.split(/[\\/]/); + if (parts.length <= 1) return ""; + parts.pop(); + return parts.join("/"); + } } diff --git a/.config/quickshell/ii/modules/common/widgets/AddressBreadcrumb.qml b/.config/quickshell/ii/modules/common/widgets/AddressBreadcrumb.qml new file mode 100644 index 000000000..a2d3e0e39 --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/AddressBreadcrumb.qml @@ -0,0 +1,38 @@ +import QtQuick +import QtQuick.Layouts +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions + +ListView { + id: root + required property var directory + property var breadcrumbDirectory: "" + Component.onCompleted: breadcrumbDirectory = directory; + onDirectoryChanged: { + if (breadcrumbDirectory.startsWith(directory)) return; + breadcrumbDirectory = directory + } + + signal navigateToDirectory(string path) + + orientation: ListView.Horizontal + clip: true + spacing: 2 + + model: breadcrumbDirectory.split("/") + delegate: SelectionGroupButton { + id: folderButton + required property var modelData + required property int index + buttonText: index === 0 ? "/" : modelData + toggled: index === directory.split("/").length - 1 + leftmost: index === 0 + rightmost: index === breadcrumbDirectory.split("/").length - 1 + + onClicked: { + root.navigateToDirectory(breadcrumbDirectory.split("/").slice(0, index + 1).join("/")) + } + } +} diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml index 24cad7972..0984c85fa 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelector.qml @@ -25,7 +25,6 @@ Scope { exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "quickshell:wallpaperSelector" WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand color: "transparent" anchors.top: true diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index c47cf2ef8..7ddf4033c 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -86,324 +86,340 @@ Item { border.color: Appearance.colors.colLayer0Border color: Appearance.colors.colLayer0 radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1 - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: wallpaperGridBackground.width - height: wallpaperGridBackground.height - radius: wallpaperGridBackground.radius - } - } property int calculatedRows: Math.ceil(grid.count / grid.columns) // implicitWidth: gridColumnLayout.implicitWidth // implicitHeight: gridColumnLayout.implicitHeight - Item { + ColumnLayout { // The grid anchors.fill: parent - GridView { - id: grid - visible: root.wallpapers.length > 0 - - property int currentIndex: 0 - readonly property int columns: root.columns - readonly property int rows: Math.max(1, Math.ceil(count / columns)) - - anchors.fill: parent - cellWidth: width / root.columns - cellHeight: cellWidth / root.previewCellAspectRatio - clip: true - interactive: true - keyNavigationWraps: true - boundsBehavior: Flickable.StopAtBounds - - ScrollBar.horizontal: ScrollBar { - policy: ScrollBar.AsNeeded - } - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded + AddressBar { + id: addressBar + Layout.margins: 4 + Layout.fillWidth: true + Layout.fillHeight: false + directory: Wallpapers.directory + onNavigateToDirectory: path => { + Wallpapers.directory = path; } + radius: wallpaperGridBackground.radius - Layout.margins + } - model: ScriptModel { - values: { - let filtered = root.wallpapers.filter(w => (w.toLowerCase().includes(root.filterQuery.toLowerCase()))); - // Add 'columns' empty entries to the end - for (let i = 0; i < root.columns; i++) { - filtered.push(""); - } - return filtered; + Item { + id: gridDisplayRegion + Layout.fillWidth: true + Layout.fillHeight: true + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: gridDisplayRegion.width + height: gridDisplayRegion.height + radius: wallpaperGridBackground.radius } } - onModelChanged: currentIndex = 0 - function moveSelection(delta) { - for (let i = 0; i < count; i++) { - const item = itemAtIndex(i); - if (item) { - item.isHovered = false; - } + GridView { + id: grid + visible: root.wallpapers.length > 0 + + readonly property int columns: root.columns + readonly property int rows: Math.max(1, Math.ceil(count / columns)) + property int currentIndex: 0 + + anchors.fill: parent + cellWidth: width / root.columns + cellHeight: cellWidth / root.previewCellAspectRatio + interactive: true + clip: true + keyNavigationWraps: true + boundsBehavior: Flickable.StopAtBounds + bottomMargin: extraOptions.implicitHeight + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded } - currentIndex = Math.max(0, Math.min(root.wallpapers.length - 1, currentIndex + delta)); - positionViewAtIndex(currentIndex, GridView.Contain); - } - function activateCurrent() { - const path = model[currentIndex]; - if (!path) - return; - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(path); - } - delegate: Item { - id: wallpaperItem - required property var modelData - required property int index - visible: modelData.length > 0 - width: grid.cellWidth - height: grid.cellHeight - property bool isHovered: false - - Rectangle { - anchors { - fill: parent - margins: 8 + function moveSelection(delta) { + for (let i = 0; i < count; i++) { + const item = itemAtIndex(i); + if (item) { + item.isHovered = false; + } } - radius: Appearance.rounding.normal - color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colPrimary : ColorUtils.transparentize(Appearance.colors.colPrimary) + currentIndex = Math.max(0, Math.min(root.wallpapers.length - 1, currentIndex + delta)); + positionViewAtIndex(currentIndex, GridView.Contain); + } - ColumnLayout { - id: wallpaperItemColumnLayout + function activateCurrent() { + const path = model[currentIndex]; + if (!path) + return; + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(path); + } + + model: ScriptModel { + values: root.wallpapers.filter(w => (w.toLowerCase().includes(root.filterQuery.toLowerCase()))) + } + onModelChanged: currentIndex = 0 + + delegate: Item { + id: wallpaperItem + required property var modelData + required property int index + visible: modelData.length > 0 + width: grid.cellWidth + height: grid.cellHeight + property bool isHovered: false + + Rectangle { anchors { fill: parent - margins: 6 + margins: 8 + } + radius: Appearance.rounding.normal + color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colPrimary : ColorUtils.transparentize(Appearance.colors.colPrimary) + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) } - spacing: 4 - Item { - id: wallpaperItemImageContainer - Layout.fillHeight: true - Layout.fillWidth: true - - StyledRectangularShadow { - target: thumbnailImageLoader - radius: Appearance.rounding.small + ColumnLayout { + id: wallpaperItemColumnLayout + anchors { + fill: parent + margins: 6 } + spacing: 4 - Loader { - id: thumbnailImageLoader - anchors.fill: parent - active: wallpaperItem.visible - sourceComponent: Image { - id: thumbnailImage - source: { - if (wallpaperItem.modelData.length == 0) - return; - const resolvedUrl = Qt.resolvedUrl(wallpaperItem.modelData); - const md5Hash = Qt.md5(resolvedUrl); - const cacheSize = "normal"; - const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; - return thumbnailPath; - } - asynchronous: true - cache: false - smooth: true - mipmap: false + Item { + id: wallpaperItemImageContainer + Layout.fillHeight: true + Layout.fillWidth: true - fillMode: Image.PreserveAspectCrop - clip: true - sourceSize.width: wallpaperItemColumnLayout.width - sourceSize.height: wallpaperItemColumnLayout.height - wallpaperItemColumnLayout.spacing - wallpaperItemName.height + StyledRectangularShadow { + target: thumbnailImageLoader + radius: Appearance.rounding.small + } - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } + Loader { + id: thumbnailImageLoader + anchors.fill: parent + active: wallpaperItem.visible + sourceComponent: Image { + id: thumbnailImage + source: { + if (wallpaperItem.modelData.length == 0) + return; + const resolvedUrl = Qt.resolvedUrl(wallpaperItem.modelData); + const md5Hash = Qt.md5(resolvedUrl); + const cacheSize = "normal"; + const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; + return thumbnailPath; + } + asynchronous: true + cache: false + smooth: true + mipmap: false - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: wallpaperItemImageContainer.width - height: wallpaperItemImageContainer.height - radius: Appearance.rounding.small + fillMode: Image.PreserveAspectCrop + clip: true + sourceSize.width: wallpaperItemColumnLayout.width + sourceSize.height: wallpaperItemColumnLayout.height - wallpaperItemColumnLayout.spacing - wallpaperItemName.height + + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: wallpaperItemImageContainer.width + height: wallpaperItemImageContainer.height + radius: Appearance.rounding.small + } } } } } - } - StyledText { - id: wallpaperItemName - Layout.fillWidth: true - Layout.leftMargin: 10 - Layout.rightMargin: 10 - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - font.pixelSize: Appearance.font.pixelSize.smaller - color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer0 - text: FileUtils.fileNameForPath(wallpaperItem.modelData) - } - } - } + StyledText { + id: wallpaperItemName + Layout.fillWidth: true + Layout.leftMargin: 10 + Layout.rightMargin: 10 - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - for (let i = 0; i < grid.count; i++) { - const item = grid.itemAtIndex(i); - if (item && item !== parent) { - item.isHovered = false; + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + font.pixelSize: Appearance.font.pixelSize.smaller + color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer0 + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + text: FileUtils.fileNameForPath(wallpaperItem.modelData) } } - parent.isHovered = true; - grid.currentIndex = index; } - onExited: { - parent.isHovered = false; - } - onClicked: { - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(wallpaperItem.modelData); + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + for (let i = 0; i < grid.count; i++) { + const item = grid.itemAtIndex(i); + if (item && item !== parent) { + item.isHovered = false; + } + } + parent.isHovered = true; + grid.currentIndex = index; + } + onExited: { + parent.isHovered = false; + } + onClicked: { + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(wallpaperItem.modelData); + } } } } - } - Label { - id: noWallpapersFoundLabel - visible: grid.model.values.length === 0 - anchors.centerIn: parent - text: "No wallpapers found" - font.family: Appearance.font.family.main - font.pixelSize: Appearance.font.pixelSize.normal - color: Appearance.colors.colSubtext - } - - StyledRectangularShadow { - target: extraOptionsBackground - } - - Rectangle { // Bottom toolbar - id: extraOptionsBackground - property real padding: 6 - anchors { - bottom: parent.bottom - horizontalCenter: parent.horizontalCenter - bottomMargin: 8 - } - color: Appearance.colors.colLayer2 - implicitHeight: extraOptionsRowLayout.implicitHeight + padding * 2 - implicitWidth: extraOptionsRowLayout.implicitWidth + padding * 2 - radius: Appearance.rounding.full - - RowLayout { - id: extraOptionsRowLayout + Item { + id: extraOptions anchors { - fill: parent - margins: extraOptionsBackground.padding + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + } + implicitHeight: extraOptionsBackground.implicitHeight + extraOptionsBackground.anchors.margins * 2 + implicitWidth: extraOptionsBackground.implicitWidth + extraOptionsBackground.anchors.margins * 2 + + StyledRectangularShadow { + target: extraOptionsBackground } - RippleButton { - Layout.fillHeight: true - Layout.topMargin: 2 - Layout.bottomMargin: 2 - buttonRadius: Appearance.rounding.full - onClicked: { - Wallpapers.openFallbackPicker(); - GlobalStates.wallpaperSelectorOpen = false; + Rectangle { // Bottom toolbar + id: extraOptionsBackground + property real padding: 6 + anchors { + fill: parent + margins: 8 } - contentItem: RowLayout { - MaterialSymbol { - text: "files" - iconSize: Appearance.font.pixelSize.larger - } - StyledText { - text: Translation.tr("System") - } - } - StyledToolTip { - content: "Use the system file picker instead" - } - } + color: Appearance.colors.colLayer2 + implicitHeight: extraOptionsRowLayout.implicitHeight + padding * 2 + implicitWidth: extraOptionsRowLayout.implicitWidth + padding * 2 + radius: Appearance.rounding.full - TextField { - id: filterField - Layout.fillHeight: true - Layout.topMargin: 2 - Layout.bottomMargin: 2 - implicitWidth: 200 - padding: 10 - placeholderText: Translation.tr("Search wallpapers...") - placeholderTextColor: Appearance.colors.colSubtext - color: Appearance.colors.colOnLayer0 - font.pixelSize: Appearance.font.pixelSize.small - renderType: Text.NativeRendering - selectedTextColor: Appearance.m3colors.m3onSecondaryContainer - selectionColor: Appearance.colors.colSecondaryContainer - background: Rectangle { - color: Appearance.colors.colLayer1 - radius: Appearance.rounding.full - } - - onTextChanged: { - root.filterQuery = text; - } - - Keys.onPressed: event => { - if (text.length === 0) { - if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { - wallpaperGrid.forceActiveFocus(); - if (event.key === Qt.Key_Down) - grid.moveSelection(grid.columns); - else if (event.key === Qt.Key_Left) - grid.moveSelection(-1); - else if (event.key === Qt.Key_Right) - grid.moveSelection(1); - event.accepted = true; - } - } else { - if (event.key === Qt.Key_Down) { - grid.moveSelection(grid.columns); - event.accepted = true; - wallpaperGrid.forceActiveFocus(); - } + RowLayout { + id: extraOptionsRowLayout + anchors { + fill: parent + margins: extraOptionsBackground.padding } - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - grid.activateCurrent(); - event.accepted = true; - } else if (event.key === Qt.Key_Escape) { - if (filterField.text.length > 0) { - filterField.text = ""; - } else { + + RippleButton { + Layout.fillHeight: true + Layout.topMargin: 2 + Layout.bottomMargin: 2 + buttonRadius: Appearance.rounding.full + onClicked: { + Wallpapers.openFallbackPicker(); GlobalStates.wallpaperSelectorOpen = false; } - event.accepted = true; + contentItem: RowLayout { + MaterialSymbol { + text: "files" + iconSize: Appearance.font.pixelSize.larger + } + StyledText { + text: Translation.tr("System") + } + } + StyledToolTip { + content: "Use the system file picker instead" + } } - } - } - RippleButton { - Layout.fillHeight: true - Layout.topMargin: 2 - Layout.bottomMargin: 2 - buttonRadius: Appearance.rounding.full - onClicked: { - GlobalStates.wallpaperSelectorOpen = false; - } - implicitWidth: height + TextField { + id: filterField + Layout.fillHeight: true + Layout.topMargin: 2 + Layout.bottomMargin: 2 + implicitWidth: 200 + padding: 10 + placeholderText: Translation.tr("Search wallpapers...") + placeholderTextColor: Appearance.colors.colSubtext + color: Appearance.colors.colOnLayer0 + font.pixelSize: Appearance.font.pixelSize.small + renderType: Text.NativeRendering + selectedTextColor: Appearance.m3colors.m3onSecondaryContainer + selectionColor: Appearance.colors.colSecondaryContainer + background: Rectangle { + color: Appearance.colors.colLayer1 + radius: Appearance.rounding.full + } - contentItem: MaterialSymbol { - text: "close" - iconSize: Appearance.font.pixelSize.larger - } + onTextChanged: { + root.filterQuery = text; + } - StyledToolTip { - content: "Cancel" + Keys.onPressed: event => { + if (text.length === 0) { + if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { + wallpaperGrid.forceActiveFocus(); + if (event.key === Qt.Key_Down) + grid.moveSelection(grid.columns); + else if (event.key === Qt.Key_Left) + grid.moveSelection(-1); + else if (event.key === Qt.Key_Right) + grid.moveSelection(1); + event.accepted = true; + } + } else { + if (event.key === Qt.Key_Down) { + grid.moveSelection(grid.columns); + event.accepted = true; + wallpaperGrid.forceActiveFocus(); + } + } + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + grid.activateCurrent(); + event.accepted = true; + } else if (event.key === Qt.Key_Escape) { + if (filterField.text.length > 0) { + filterField.text = ""; + } else { + GlobalStates.wallpaperSelectorOpen = false; + } + event.accepted = true; + } + } + } + + RippleButton { + Layout.fillHeight: true + Layout.topMargin: 2 + Layout.bottomMargin: 2 + buttonRadius: Appearance.rounding.full + onClicked: { + GlobalStates.wallpaperSelectorOpen = false; + } + implicitWidth: height + + contentItem: MaterialSymbol { + text: "close" + iconSize: Appearance.font.pixelSize.larger + } + + StyledToolTip { + content: "Cancel" + } + } } } } diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index eaa5fd22d..91c3c4b70 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -14,7 +14,7 @@ pragma ComponentBehavior: Bound Singleton { id: root - property string searchDir: FileUtils.trimFileProtocol(`${Directories.pictures}/Wallpapers`) + property string directory: FileUtils.trimFileProtocol(`${Directories.pictures}/Wallpapers`) readonly property list extensions: [ // TODO: add videos "jpg", "jpeg", "png", "webp", "avif", "bmp", "svg" ] @@ -42,7 +42,7 @@ Singleton { // Folder model FolderListModel { id: files - folder: Qt.resolvedUrl(root.searchDir) + folder: Qt.resolvedUrl(root.directory) nameFilters: root.extensions.map(ext => `*.${ext}`) showDirs: false showDotAndDotDot: false From 80af8666503aa16bccf836c956c9cfb92bfc10c5 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 23 Aug 2025 22:11:41 +0700 Subject: [PATCH 26/35] wallpaper selector: directory validation, common file browser keybinds --- .../ii/modules/common/AddressBar.qml | 62 +++++++----- .../common/widgets/StyledTextInput.qml | 1 + .../WallpaperSelectorContent.qml | 95 +++++++++---------- .config/quickshell/ii/services/Wallpapers.qml | 25 ++++- 4 files changed, 106 insertions(+), 77 deletions(-) diff --git a/.config/quickshell/ii/modules/common/AddressBar.qml b/.config/quickshell/ii/modules/common/AddressBar.qml index b2c1f2bed..bc9b7080f 100644 --- a/.config/quickshell/ii/modules/common/AddressBar.qml +++ b/.config/quickshell/ii/modules/common/AddressBar.qml @@ -17,6 +17,11 @@ Rectangle { implicitHeight: mainLayout.implicitHeight + padding * 2 color: Appearance.colors.colLayer2 + function focusBreadcrumb() { + root.showBreadcrumb = false; + addressInput.forceActiveFocus(); + } + RowLayout { id: mainLayout anchors { @@ -38,43 +43,54 @@ Rectangle { Layout.fillWidth: true Layout.fillHeight: true - Loader { - active: !root.showBreadcrumb + Rectangle { + id: directoryEntry visible: !root.showBreadcrumb anchors.fill: parent - sourceComponent: Rectangle { - color: Appearance.colors.colLayer1 - radius: Appearance.rounding.full - implicitWidth: addressInput.implicitWidth - implicitHeight: addressInput.implicitHeight + color: Appearance.colors.colLayer1 + radius: Appearance.rounding.full + implicitWidth: addressInput.implicitWidth + implicitHeight: addressInput.implicitHeight - StyledTextInput { - id: addressInput + Keys.onPressed: event => { + if (directoryEntry.visible && event.key === Qt.Key_Escape) { + root.showBreadcrumb = true; + event.accepted = true; + return; + } + event.accepted = false; + } + + StyledTextInput { + id: addressInput + anchors.fill: parent + padding: 10 + text: root.directory + + onAccepted: { + root.navigateToDirectory(text); + root.showBreadcrumb = true; + } + + MouseArea { + // I-beam cursor anchors.fill: parent - padding: 10 - text: root.directory - - onAccepted: root.navigateToDirectory(text) - - MouseArea { - // I-beam cursor - anchors.fill: parent - acceptedButtons: Qt.NoButton - hoverEnabled: true - cursorShape: Qt.IBeamCursor - } + acceptedButtons: Qt.NoButton + hoverEnabled: true + cursorShape: Qt.IBeamCursor } } } Loader { + id: breadcrumbLoader active: root.showBreadcrumb visible: root.showBreadcrumb anchors.fill: parent sourceComponent: AddressBreadcrumb { directory: root.directory - onNavigateToDirectory: (dir) => { - root.navigateToDirectory(dir) + onNavigateToDirectory: dir => { + root.navigateToDirectory(dir); } } } diff --git a/.config/quickshell/ii/modules/common/widgets/StyledTextInput.qml b/.config/quickshell/ii/modules/common/widgets/StyledTextInput.qml index 57d0c7262..ff98af721 100644 --- a/.config/quickshell/ii/modules/common/widgets/StyledTextInput.qml +++ b/.config/quickshell/ii/modules/common/widgets/StyledTextInput.qml @@ -6,6 +6,7 @@ import QtQuick.Controls * Does not include visual layout, but includes the easily neglected colors. */ TextInput { + color: Appearance.colors.colOnLayer1 renderType: Text.NativeRendering selectedTextColor: Appearance.m3colors.m3onSecondaryContainer selectionColor: Appearance.colors.colSecondaryContainer diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 7ddf4033c..8f49f349d 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -20,11 +20,15 @@ Item { implicitWidth: columnLayout.implicitWidth property var wallpapers: Wallpapers.wallpapers property string filterQuery: "" + property bool useDarkMode: Appearance.m3colors.darkmode Keys.onPressed: event => { if (event.key === Qt.Key_Escape) { GlobalStates.wallpaperSelectorOpen = false; event.accepted = true; + } else if (event.modifiers & Qt.AltModifier && event.key === Qt.Key_Up) { + Wallpapers.setDirectory(FileUtils.parentDirectory(Wallpapers.directory)); + event.accepted = true; } else if (event.key === Qt.Key_Left) { grid.moveSelection(-1); event.accepted = true; @@ -32,11 +36,7 @@ Item { grid.moveSelection(1); event.accepted = true; } else if (event.key === Qt.Key_Up) { - if (grid.currentIndex < grid.columns) { - filterField.forceActiveFocus(); - } else { - grid.moveSelection(-grid.columns); - } + grid.moveSelection(-grid.columns); event.accepted = true; } else if (event.key === Qt.Key_Down) { grid.moveSelection(grid.columns); @@ -50,6 +50,9 @@ Item { } filterField.forceActiveFocus(); event.accepted = true; + } else if (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_L) { + addressBar.focusBreadcrumb(); + event.accepted = true; } else { filterField.forceActiveFocus(); if (event.text.length > 0) { @@ -103,7 +106,7 @@ Item { Layout.fillHeight: false directory: Wallpapers.directory onNavigateToDirectory: path => { - Wallpapers.directory = path; + Wallpapers.setDirectory(path); } radius: wallpaperGridBackground.radius - Layout.margins } @@ -154,12 +157,13 @@ Item { } function activateCurrent() { - const path = model[currentIndex]; + print("ACTIVATE"); + const path = grid.model.values[currentIndex]; if (!path) return; GlobalStates.wallpaperSelectorOpen = false; filterField.text = ""; - Wallpapers.apply(path); + Wallpapers.apply(path, root.useDarkMode); } model: ScriptModel { @@ -284,7 +288,7 @@ Item { onClicked: { GlobalStates.wallpaperSelectorOpen = false; filterField.text = ""; - Wallpapers.apply(wallpaperItem.modelData); + Wallpapers.apply(wallpaperItem.modelData, root.useDarkMode); } } } @@ -326,22 +330,34 @@ Item { Layout.fillHeight: true Layout.topMargin: 2 Layout.bottomMargin: 2 + implicitWidth: height buttonRadius: Appearance.rounding.full onClicked: { Wallpapers.openFallbackPicker(); GlobalStates.wallpaperSelectorOpen = false; } - contentItem: RowLayout { - MaterialSymbol { - text: "files" - iconSize: Appearance.font.pixelSize.larger - } - StyledText { - text: Translation.tr("System") - } + contentItem: MaterialSymbol { + text: "files" + iconSize: Appearance.font.pixelSize.larger } StyledToolTip { - content: "Use the system file picker instead" + content: Translation.tr("Use the system file picker instead") + } + } + + RippleButton { + Layout.fillHeight: true + Layout.topMargin: 2 + Layout.bottomMargin: 2 + implicitWidth: height + buttonRadius: Appearance.rounding.full + onClicked: root.useDarkMode = !root.useDarkMode + contentItem: MaterialSymbol { + text: root.useDarkMode ? "dark_mode" : "light_mode" + iconSize: Appearance.font.pixelSize.larger + } + StyledToolTip { + content: Translation.tr("Click to toggle light/dark mode (applied when wallpaper is chosen)") } } @@ -369,35 +385,20 @@ Item { } Keys.onPressed: event => { - if (text.length === 0) { - if (event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right) { - wallpaperGrid.forceActiveFocus(); - if (event.key === Qt.Key_Down) - grid.moveSelection(grid.columns); - else if (event.key === Qt.Key_Left) - grid.moveSelection(-1); - else if (event.key === Qt.Key_Right) - grid.moveSelection(1); - event.accepted = true; - } - } else { + if (text.length !== 0) { + // No filtering, just navigate grid if (event.key === Qt.Key_Down) { grid.moveSelection(grid.columns); - event.accepted = true; wallpaperGrid.forceActiveFocus(); + event.accepted = true; + } + if (event.key === Qt.Key_Up) { + grid.moveSelection(-grid.columns); + wallpaperGrid.forceActiveFocus(); + event.accepted = true; } } - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - grid.activateCurrent(); - event.accepted = true; - } else if (event.key === Qt.Key_Escape) { - if (filterField.text.length > 0) { - filterField.text = ""; - } else { - GlobalStates.wallpaperSelectorOpen = false; - } - event.accepted = true; - } + event.accepted = false; } } @@ -409,15 +410,9 @@ Item { onClicked: { GlobalStates.wallpaperSelectorOpen = false; } - implicitWidth: height - contentItem: MaterialSymbol { - text: "close" - iconSize: Appearance.font.pixelSize.larger - } - - StyledToolTip { - content: "Cancel" + contentItem: StyledText { + text: "Cancel" } } } diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index 91c3c4b70..60c4e041b 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -30,15 +30,32 @@ Singleton { applyProc.exec([Directories.wallpaperSwitchScriptPath]) } - function apply(path) { + function apply(path, darkMode = Appearance.m3colors.darkmode) { if (!path || path.length === 0) return applyProc.exec([ - "bash", "-c", - `${StringUtils.shellSingleQuoteEscape(Directories.wallpaperSwitchScriptPath)} ` + - `--image ${StringUtils.shellSingleQuoteEscape(path)}` + Directories.wallpaperSwitchScriptPath, + "--image", path, + "--mode", (darkMode ? "dark" : "light") ]) } + Process { + id: validateDirProc + property string nicePath: "" + function setDirectoryIfValid(path) { + validateDirProc.nicePath = FileUtils.trimFileProtocol(path).replace(/\/+$/, "") + validateDirProc.exec(["test", "-d", nicePath]) + } + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + root.directory = validateDirProc.nicePath + } + } + } + function setDirectory(path) { + validateDirProc.setDirectoryIfValid(path) + } + // Folder model FolderListModel { id: files From 29a149b3403840bb9d8db17cc84ef8d117c20314 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 23 Aug 2025 22:23:28 +0700 Subject: [PATCH 27/35] wallpaper selector: fix some key focus/fallthrough problems --- .config/quickshell/ii/modules/common/AddressBar.qml | 9 ++++++--- .../wallpaperSelector/WallpaperSelectorContent.qml | 7 +++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.config/quickshell/ii/modules/common/AddressBar.qml b/.config/quickshell/ii/modules/common/AddressBar.qml index bc9b7080f..c25e1c0a4 100644 --- a/.config/quickshell/ii/modules/common/AddressBar.qml +++ b/.config/quickshell/ii/modules/common/AddressBar.qml @@ -67,9 +67,12 @@ Rectangle { padding: 10 text: root.directory - onAccepted: { - root.navigateToDirectory(text); - root.showBreadcrumb = true; + Keys.onPressed: event => { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + root.navigateToDirectory(text); + root.showBreadcrumb = true; + event.accepted = true; + } } MouseArea { diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 8f49f349d..668e57dca 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -53,11 +53,14 @@ Item { } else if (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_L) { addressBar.focusBreadcrumb(); event.accepted = true; - } else { + } else if (event.key === Qt.Key_Slash) { filterField.forceActiveFocus(); + event.accepted = true; + } else { if (event.text.length > 0) { filterField.text += event.text; filterField.cursorPosition = filterField.text.length; + filterField.forceActiveFocus(); } event.accepted = true; } @@ -368,7 +371,7 @@ Item { Layout.bottomMargin: 2 implicitWidth: 200 padding: 10 - placeholderText: Translation.tr("Search wallpapers...") + placeholderText: focus ? Translation.tr("Search wallpapers") : Translation.tr("Hit \"/\" to search") placeholderTextColor: Appearance.colors.colSubtext color: Appearance.colors.colOnLayer0 font.pixelSize: Appearance.font.pixelSize.small From bdc0ade117da90dacdad49d76cdfc25793c68222 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 23 Aug 2025 23:16:18 +0700 Subject: [PATCH 28/35] wallpaper selector: quick places --- .../ii/modules/common/AddressBar.qml | 4 + .../ii/modules/common/Directories.qml | 7 +- .../common/widgets/AddressBreadcrumb.qml | 5 +- .../WallpaperDirectoryItem.qml | 131 ++++ .../WallpaperSelectorContent.qml | 610 ++++++++---------- .config/quickshell/ii/services/Wallpapers.qml | 3 +- 6 files changed, 428 insertions(+), 332 deletions(-) create mode 100644 .config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml diff --git a/.config/quickshell/ii/modules/common/AddressBar.qml b/.config/quickshell/ii/modules/common/AddressBar.qml index c25e1c0a4..a5328d446 100644 --- a/.config/quickshell/ii/modules/common/AddressBar.qml +++ b/.config/quickshell/ii/modules/common/AddressBar.qml @@ -108,6 +108,10 @@ Rectangle { iconSize: Appearance.font.pixelSize.larger color: dirEditButton.toggled ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer2 } + + StyledToolTip { + content: Translation.tr("Edit directory") + } } } } diff --git a/.config/quickshell/ii/modules/common/Directories.qml b/.config/quickshell/ii/modules/common/Directories.qml index 2b235c691..49f0e46da 100644 --- a/.config/quickshell/ii/modules/common/Directories.qml +++ b/.config/quickshell/ii/modules/common/Directories.qml @@ -8,13 +8,16 @@ import Quickshell Singleton { // XDG Dirs, with "file://" + readonly property string home: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0] readonly property string config: StandardPaths.standardLocations(StandardPaths.ConfigLocation)[0] readonly property string state: StandardPaths.standardLocations(StandardPaths.StateLocation)[0] readonly property string cache: StandardPaths.standardLocations(StandardPaths.CacheLocation)[0] readonly property string genericCache: StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0] - readonly property string pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0] + readonly property string documents: StandardPaths.standardLocations(StandardPaths.DocumentsLocation)[0] readonly property string downloads: StandardPaths.standardLocations(StandardPaths.DownloadLocation)[0] - + readonly property string pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0] + readonly property string videos: StandardPaths.standardLocations(StandardPaths.MoviesLocation)[0] + // Other dirs used by the shell, without "file://" property string assetsPath: Quickshell.shellPath("assets") property string scriptPath: Quickshell.shellPath("scripts") diff --git a/.config/quickshell/ii/modules/common/widgets/AddressBreadcrumb.qml b/.config/quickshell/ii/modules/common/widgets/AddressBreadcrumb.qml index a2d3e0e39..d1d6b52df 100644 --- a/.config/quickshell/ii/modules/common/widgets/AddressBreadcrumb.qml +++ b/.config/quickshell/ii/modules/common/widgets/AddressBreadcrumb.qml @@ -27,7 +27,10 @@ ListView { required property var modelData required property int index buttonText: index === 0 ? "/" : modelData - toggled: index === directory.split("/").length - 1 + toggled: { + if (directory.trim() === "/") return index === 0; + return index === directory.split("/").length - 1 + } leftmost: index === 0 rightmost: index === breadcrumbDirectory.split("/").length - 1 diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml new file mode 100644 index 000000000..1e940ece2 --- /dev/null +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml @@ -0,0 +1,131 @@ +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell +import Quickshell.Io + +Item { + id: root + required property string path + property bool isHovered: false + + property alias color: background.color + property alias radius: background.radius + property alias padding: background.anchors.margins + + signal activated() + + Rectangle { + id: background + anchors { + fill: parent + margins: 8 + } + radius: Appearance.rounding.normal + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + + ColumnLayout { + id: wallpaperItemColumnLayout + anchors { + fill: parent + margins: 6 + } + spacing: 4 + + Item { + id: wallpaperItemImageContainer + Layout.fillHeight: true + Layout.fillWidth: true + + StyledRectangularShadow { + target: thumbnailImageLoader + radius: Appearance.rounding.small + } + + Loader { + id: thumbnailImageLoader + anchors.fill: parent + active: root.visible + sourceComponent: Image { + id: thumbnailImage + source: { + if (root.path.length == 0) + return; + const resolvedUrl = Qt.resolvedUrl(root.path); + const md5Hash = Qt.md5(resolvedUrl); + const cacheSize = "normal"; + const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; + return thumbnailPath; + } + asynchronous: true + cache: false + smooth: true + mipmap: false + + fillMode: Image.PreserveAspectCrop + clip: true + sourceSize.width: wallpaperItemColumnLayout.width + sourceSize.height: wallpaperItemColumnLayout.height - wallpaperItemColumnLayout.spacing - wallpaperItemName.height + + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: wallpaperItemImageContainer.width + height: wallpaperItemImageContainer.height + radius: Appearance.rounding.small + } + } + } + } + } + + StyledText { + id: wallpaperItemName + Layout.fillWidth: true + Layout.leftMargin: 10 + Layout.rightMargin: 10 + + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + font.pixelSize: Appearance.font.pixelSize.smaller + color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer0 + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + text: FileUtils.fileNameForPath(root.path) + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + for (let i = 0; i < grid.count; i++) { + const item = grid.itemAtIndex(i); + if (item && item !== parent) { + item.isHovered = false; + } + } + parent.isHovered = true; + grid.currentIndex = index; + } + onExited: { + parent.isHovered = false; + } + onClicked: root.activated() + } +} \ No newline at end of file diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 668e57dca..2fc14a9ef 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -9,15 +9,11 @@ import QtQuick.Layouts import Qt5Compat.GraphicalEffects import Quickshell import Quickshell.Io -import Quickshell.Wayland -import Quickshell.Hyprland Item { id: root property int columns: 4 property real previewCellAspectRatio: 4 / 3 - implicitHeight: columnLayout.implicitHeight - implicitWidth: columnLayout.implicitWidth property var wallpapers: Wallpapers.wallpapers property string filterQuery: "" property bool useDarkMode: Appearance.m3colors.darkmode @@ -66,357 +62,315 @@ Item { } } - ColumnLayout { - id: columnLayout - anchors.fill: parent - spacing: -Appearance.sizes.elevationMargin + implicitHeight: mainLayout.implicitHeight + implicitWidth: mainLayout.implicitWidth - Item { // The grid - id: wallpaperGrid - Layout.fillWidth: true - Layout.fillHeight: true - implicitWidth: wallpaperGridBackground.implicitWidth + Appearance.sizes.elevationMargin * 2 - implicitHeight: wallpaperGridBackground.implicitHeight + Appearance.sizes.elevationMargin * 2 + StyledRectangularShadow { + target: wallpaperGridBackground + } + Rectangle { + id: wallpaperGridBackground + anchors { + fill: parent + margins: Appearance.sizes.elevationMargin + } + focus: true + border.width: 1 + border.color: Appearance.colors.colLayer0Border + color: Appearance.colors.colLayer0 + radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1 + + property int calculatedRows: Math.ceil(grid.count / grid.columns) + + implicitWidth: gridColumnLayout.implicitWidth + implicitHeight: gridColumnLayout.implicitHeight + + RowLayout { + id: mainLayout + anchors.fill: parent + spacing: -4 - StyledRectangularShadow { - target: wallpaperGridBackground - } Rectangle { - id: wallpaperGridBackground - anchors { - fill: parent - margins: Appearance.sizes.elevationMargin - } - focus: true - border.width: 1 - border.color: Appearance.colors.colLayer0Border - color: Appearance.colors.colLayer0 - radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1 - - property int calculatedRows: Math.ceil(grid.count / grid.columns) - - // implicitWidth: gridColumnLayout.implicitWidth - // implicitHeight: gridColumnLayout.implicitHeight + Layout.fillHeight: true + Layout.margins: 4 + implicitWidth: quickDirColumnLayout.implicitWidth + implicitHeight: quickDirColumnLayout.implicitHeight + color: Appearance.colors.colLayer1 + radius: wallpaperGridBackground.radius - Layout.margins ColumnLayout { - // The grid + id: quickDirColumnLayout anchors.fill: parent - - AddressBar { - id: addressBar - Layout.margins: 4 - Layout.fillWidth: true - Layout.fillHeight: false - directory: Wallpapers.directory - onNavigateToDirectory: path => { - Wallpapers.setDirectory(path); + spacing: 0 + + StyledText { + Layout.margins: 12 + font { + pixelSize: Appearance.font.pixelSize.normal + weight: Font.Medium + } + text: Translation.tr("Pick a wallpaper") + } + ListView { + // Quick dirs + Layout.fillHeight: true + Layout.margins: 4 + implicitWidth: 140 + clip: true + model: [ + { icon: "home", name: "Home", path: Directories.home }, + { icon: "folder", name: "Documents", path: Directories.documents }, + { icon: "download", name: "Downloads", path: Directories.downloads }, + { icon: "image", name: "Pictures", path: Directories.pictures }, + { icon: "movie", name: "Videos", path: Directories.videos }, + { icon: "", name: "---", path: "INTENTIONALLY_INVALID_DIR" }, + { icon: "wallpaper", name: "Wallpapers", path: `${Directories.pictures}/Wallpapers` }, + { icon: "favorite", name: "Homework", path: `${Directories.pictures}/homework` }, + ] + delegate: RippleButton { + id: quickDirButton + required property var modelData + anchors { + left: parent.left + right: parent.right + } + onClicked: Wallpapers.setDirectory(quickDirButton.modelData.path) + enabled: modelData.icon.length > 0 + toggled: Wallpapers.directory === FileUtils.trimFileProtocol(modelData.path) + colBackgroundToggled: Appearance.colors.colSecondaryContainer + colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover + colRippleToggled: Appearance.colors.colSecondaryContainerActive + + contentItem: RowLayout { + MaterialSymbol { + color: quickDirButton.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1 + iconSize: Appearance.font.pixelSize.larger + text: quickDirButton.modelData.icon + } + StyledText { + Layout.fillWidth: true + horizontalAlignment: Text.AlignLeft + color: quickDirButton.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1 + text: quickDirButton.modelData.name + } + } + } + } + } + } + + ColumnLayout { + id: gridColumnLayout + Layout.fillWidth: true + Layout.fillHeight: true + + AddressBar { + id: addressBar + Layout.margins: 4 + Layout.fillWidth: true + Layout.fillHeight: false + directory: Wallpapers.directory + onNavigateToDirectory: path => { + Wallpapers.setDirectory(path.length == 0 ? "/" : path); + } + radius: wallpaperGridBackground.radius - Layout.margins + } + + Item { + id: gridDisplayRegion + Layout.fillWidth: true + Layout.fillHeight: true + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: gridDisplayRegion.width + height: gridDisplayRegion.height + radius: wallpaperGridBackground.radius + } + } + + GridView { + id: grid + visible: root.wallpapers.length > 0 + + readonly property int columns: root.columns + readonly property int rows: Math.max(1, Math.ceil(count / columns)) + property int currentIndex: 0 + + anchors.fill: parent + cellWidth: width / root.columns + cellHeight: cellWidth / root.previewCellAspectRatio + interactive: true + clip: true + keyNavigationWraps: true + boundsBehavior: Flickable.StopAtBounds + bottomMargin: extraOptions.implicitHeight + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + } + + function moveSelection(delta) { + for (let i = 0; i < count; i++) { + const item = itemAtIndex(i); + if (item) { + item.isHovered = false; + } + } + currentIndex = Math.max(0, Math.min(root.wallpapers.length - 1, currentIndex + delta)); + positionViewAtIndex(currentIndex, GridView.Contain); + } + + function activateCurrent() { + print("ACTIVATE"); + const path = grid.model.values[currentIndex]; + if (!path) + return; + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + Wallpapers.apply(path, root.useDarkMode); + } + + model: ScriptModel { + values: root.wallpapers.filter(w => (w.toLowerCase().includes(root.filterQuery.toLowerCase()))) + } + onModelChanged: currentIndex = 0 + + delegate: WallpaperDirectoryItem { + required property var modelData + required property int index + visible: modelData.length > 0 + width: grid.cellWidth + height: grid.cellHeight + path: modelData + color: (index === grid?.currentIndex || parent?.isHovered) ? Appearance.colors.colPrimary : ColorUtils.transparentize(Appearance.colors.colPrimary) + onActivated: { + Wallpapers.apply(path, root.useDarkMode); + GlobalStates.wallpaperSelectorOpen = false; + filterField.text = ""; + } } - radius: wallpaperGridBackground.radius - Layout.margins } Item { - id: gridDisplayRegion - Layout.fillWidth: true - Layout.fillHeight: true - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: gridDisplayRegion.width - height: gridDisplayRegion.height - radius: wallpaperGridBackground.radius - } + id: extraOptions + anchors { + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + } + implicitHeight: extraOptionsBackground.implicitHeight + extraOptionsBackground.anchors.margins * 2 + implicitWidth: extraOptionsBackground.implicitWidth + extraOptionsBackground.anchors.margins * 2 + + StyledRectangularShadow { + target: extraOptionsBackground } - GridView { - id: grid - visible: root.wallpapers.length > 0 - - readonly property int columns: root.columns - readonly property int rows: Math.max(1, Math.ceil(count / columns)) - property int currentIndex: 0 - - anchors.fill: parent - cellWidth: width / root.columns - cellHeight: cellWidth / root.previewCellAspectRatio - interactive: true - clip: true - keyNavigationWraps: true - boundsBehavior: Flickable.StopAtBounds - bottomMargin: extraOptions.implicitHeight - - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded - } - - function moveSelection(delta) { - for (let i = 0; i < count; i++) { - const item = itemAtIndex(i); - if (item) { - item.isHovered = false; - } - } - currentIndex = Math.max(0, Math.min(root.wallpapers.length - 1, currentIndex + delta)); - positionViewAtIndex(currentIndex, GridView.Contain); - } - - function activateCurrent() { - print("ACTIVATE"); - const path = grid.model.values[currentIndex]; - if (!path) - return; - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(path, root.useDarkMode); - } - - model: ScriptModel { - values: root.wallpapers.filter(w => (w.toLowerCase().includes(root.filterQuery.toLowerCase()))) - } - onModelChanged: currentIndex = 0 - - delegate: Item { - id: wallpaperItem - required property var modelData - required property int index - visible: modelData.length > 0 - width: grid.cellWidth - height: grid.cellHeight - property bool isHovered: false - - Rectangle { - anchors { - fill: parent - margins: 8 - } - radius: Appearance.rounding.normal - color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colPrimary : ColorUtils.transparentize(Appearance.colors.colPrimary) - Behavior on color { - animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) - } - - ColumnLayout { - id: wallpaperItemColumnLayout - anchors { - fill: parent - margins: 6 - } - spacing: 4 - - Item { - id: wallpaperItemImageContainer - Layout.fillHeight: true - Layout.fillWidth: true - - StyledRectangularShadow { - target: thumbnailImageLoader - radius: Appearance.rounding.small - } - - Loader { - id: thumbnailImageLoader - anchors.fill: parent - active: wallpaperItem.visible - sourceComponent: Image { - id: thumbnailImage - source: { - if (wallpaperItem.modelData.length == 0) - return; - const resolvedUrl = Qt.resolvedUrl(wallpaperItem.modelData); - const md5Hash = Qt.md5(resolvedUrl); - const cacheSize = "normal"; - const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; - return thumbnailPath; - } - asynchronous: true - cache: false - smooth: true - mipmap: false - - fillMode: Image.PreserveAspectCrop - clip: true - sourceSize.width: wallpaperItemColumnLayout.width - sourceSize.height: wallpaperItemColumnLayout.height - wallpaperItemColumnLayout.spacing - wallpaperItemName.height - - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: wallpaperItemImageContainer.width - height: wallpaperItemImageContainer.height - radius: Appearance.rounding.small - } - } - } - } - } - - StyledText { - id: wallpaperItemName - Layout.fillWidth: true - Layout.leftMargin: 10 - Layout.rightMargin: 10 - - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - font.pixelSize: Appearance.font.pixelSize.smaller - color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer0 - Behavior on color { - animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) - } - text: FileUtils.fileNameForPath(wallpaperItem.modelData) - } - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - for (let i = 0; i < grid.count; i++) { - const item = grid.itemAtIndex(i); - if (item && item !== parent) { - item.isHovered = false; - } - } - parent.isHovered = true; - grid.currentIndex = index; - } - onExited: { - parent.isHovered = false; - } - onClicked: { - GlobalStates.wallpaperSelectorOpen = false; - filterField.text = ""; - Wallpapers.apply(wallpaperItem.modelData, root.useDarkMode); - } - } - } - } - - Item { - id: extraOptions + Rectangle { // Bottom toolbar + id: extraOptionsBackground + property real padding: 6 anchors { - bottom: parent.bottom - horizontalCenter: parent.horizontalCenter + fill: parent + margins: 8 } - implicitHeight: extraOptionsBackground.implicitHeight + extraOptionsBackground.anchors.margins * 2 - implicitWidth: extraOptionsBackground.implicitWidth + extraOptionsBackground.anchors.margins * 2 + color: Appearance.colors.colLayer2 + implicitHeight: extraOptionsRowLayout.implicitHeight + padding * 2 + implicitWidth: extraOptionsRowLayout.implicitWidth + padding * 2 + radius: Appearance.rounding.full - StyledRectangularShadow { - target: extraOptionsBackground - } - - Rectangle { // Bottom toolbar - id: extraOptionsBackground - property real padding: 6 + RowLayout { + id: extraOptionsRowLayout anchors { fill: parent - margins: 8 + margins: extraOptionsBackground.padding } - color: Appearance.colors.colLayer2 - implicitHeight: extraOptionsRowLayout.implicitHeight + padding * 2 - implicitWidth: extraOptionsRowLayout.implicitWidth + padding * 2 - radius: Appearance.rounding.full - RowLayout { - id: extraOptionsRowLayout - anchors { - fill: parent - margins: extraOptionsBackground.padding + RippleButton { + Layout.fillHeight: true + Layout.topMargin: 2 + Layout.bottomMargin: 2 + implicitWidth: height + buttonRadius: Appearance.rounding.full + onClicked: { + Wallpapers.openFallbackPicker(); + GlobalStates.wallpaperSelectorOpen = false; + } + contentItem: MaterialSymbol { + text: "open_in_new" + iconSize: Appearance.font.pixelSize.larger + } + StyledToolTip { + content: Translation.tr("Use the system file picker instead") + } + } + + RippleButton { + Layout.fillHeight: true + Layout.topMargin: 2 + Layout.bottomMargin: 2 + implicitWidth: height + buttonRadius: Appearance.rounding.full + onClicked: root.useDarkMode = !root.useDarkMode + contentItem: MaterialSymbol { + text: root.useDarkMode ? "dark_mode" : "light_mode" + iconSize: Appearance.font.pixelSize.larger + } + StyledToolTip { + content: Translation.tr("Click to toggle light/dark mode (applied when wallpaper is chosen)") + } + } + + TextField { + id: filterField + Layout.fillHeight: true + Layout.topMargin: 2 + Layout.bottomMargin: 2 + implicitWidth: 200 + padding: 10 + placeholderText: focus ? Translation.tr("Search wallpapers") : Translation.tr("Hit \"/\" to search") + placeholderTextColor: Appearance.colors.colSubtext + color: Appearance.colors.colOnLayer0 + font.pixelSize: Appearance.font.pixelSize.small + renderType: Text.NativeRendering + selectedTextColor: Appearance.m3colors.m3onSecondaryContainer + selectionColor: Appearance.colors.colSecondaryContainer + background: Rectangle { + color: Appearance.colors.colLayer1 + radius: Appearance.rounding.full } - RippleButton { - Layout.fillHeight: true - Layout.topMargin: 2 - Layout.bottomMargin: 2 - implicitWidth: height - buttonRadius: Appearance.rounding.full - onClicked: { - Wallpapers.openFallbackPicker(); - GlobalStates.wallpaperSelectorOpen = false; - } - contentItem: MaterialSymbol { - text: "files" - iconSize: Appearance.font.pixelSize.larger - } - StyledToolTip { - content: Translation.tr("Use the system file picker instead") - } + onTextChanged: { + root.filterQuery = text; } - RippleButton { - Layout.fillHeight: true - Layout.topMargin: 2 - Layout.bottomMargin: 2 - implicitWidth: height - buttonRadius: Appearance.rounding.full - onClicked: root.useDarkMode = !root.useDarkMode - contentItem: MaterialSymbol { - text: root.useDarkMode ? "dark_mode" : "light_mode" - iconSize: Appearance.font.pixelSize.larger - } - StyledToolTip { - content: Translation.tr("Click to toggle light/dark mode (applied when wallpaper is chosen)") - } - } - - TextField { - id: filterField - Layout.fillHeight: true - Layout.topMargin: 2 - Layout.bottomMargin: 2 - implicitWidth: 200 - padding: 10 - placeholderText: focus ? Translation.tr("Search wallpapers") : Translation.tr("Hit \"/\" to search") - placeholderTextColor: Appearance.colors.colSubtext - color: Appearance.colors.colOnLayer0 - font.pixelSize: Appearance.font.pixelSize.small - renderType: Text.NativeRendering - selectedTextColor: Appearance.m3colors.m3onSecondaryContainer - selectionColor: Appearance.colors.colSecondaryContainer - background: Rectangle { - color: Appearance.colors.colLayer1 - radius: Appearance.rounding.full - } - - onTextChanged: { - root.filterQuery = text; - } - - Keys.onPressed: event => { - if (text.length !== 0) { - // No filtering, just navigate grid - if (event.key === Qt.Key_Down) { - grid.moveSelection(grid.columns); - wallpaperGrid.forceActiveFocus(); - event.accepted = true; - } - if (event.key === Qt.Key_Up) { - grid.moveSelection(-grid.columns); - wallpaperGrid.forceActiveFocus(); - event.accepted = true; - } + Keys.onPressed: event => { + if (text.length !== 0) { + // No filtering, just navigate grid + if (event.key === Qt.Key_Down) { + grid.moveSelection(grid.columns); + wallpaperGrid.forceActiveFocus(); + event.accepted = true; + } + if (event.key === Qt.Key_Up) { + grid.moveSelection(-grid.columns); + wallpaperGrid.forceActiveFocus(); + event.accepted = true; } - event.accepted = false; } + event.accepted = false; + } + } + + RippleButton { + Layout.fillHeight: true + Layout.topMargin: 2 + Layout.bottomMargin: 2 + buttonRadius: Appearance.rounding.full + onClicked: { + GlobalStates.wallpaperSelectorOpen = false; } - RippleButton { - Layout.fillHeight: true - Layout.topMargin: 2 - Layout.bottomMargin: 2 - buttonRadius: Appearance.rounding.full - onClicked: { - GlobalStates.wallpaperSelectorOpen = false; - } - - contentItem: StyledText { - text: "Cancel" - } + contentItem: StyledText { + text: "Cancel" } } } diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index 60c4e041b..d01e38034 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -44,6 +44,7 @@ Singleton { property string nicePath: "" function setDirectoryIfValid(path) { validateDirProc.nicePath = FileUtils.trimFileProtocol(path).replace(/\/+$/, "") + if (/^\/*$/.test(validateDirProc.nicePath)) validateDirProc.nicePath = "/"; validateDirProc.exec(["test", "-d", nicePath]) } onExited: (exitCode, exitStatus) => { @@ -61,7 +62,7 @@ Singleton { id: files folder: Qt.resolvedUrl(root.directory) nameFilters: root.extensions.map(ext => `*.${ext}`) - showDirs: false + showDirs: true showDotAndDotDot: false showOnlyReadable: true sortField: FolderListModel.Time From 8bbf040100a02c927a556b486267b6906810d6b0 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 24 Aug 2025 12:20:04 +0700 Subject: [PATCH 29/35] wallpaper selector: show folders --- .../ii/modules/common/Directories.qml | 1 + .../quickshell/ii/modules/common/Images.qml | 12 +++ .../common/{ => widgets}/AddressBar.qml | 0 .../modules/common/widgets/DirectoryIcon.qml | 42 ++++++++++ .../WallpaperDirectoryItem.qml | 41 ++++++--- .../WallpaperSelectorContent.qml | 84 +++++++++++++------ .config/quickshell/ii/services/Wallpapers.qml | 37 ++++++-- 7 files changed, 175 insertions(+), 42 deletions(-) create mode 100644 .config/quickshell/ii/modules/common/Images.qml rename .config/quickshell/ii/modules/common/{ => widgets}/AddressBar.qml (100%) create mode 100644 .config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml diff --git a/.config/quickshell/ii/modules/common/Directories.qml b/.config/quickshell/ii/modules/common/Directories.qml index 49f0e46da..60f587b59 100644 --- a/.config/quickshell/ii/modules/common/Directories.qml +++ b/.config/quickshell/ii/modules/common/Directories.qml @@ -16,6 +16,7 @@ Singleton { readonly property string documents: StandardPaths.standardLocations(StandardPaths.DocumentsLocation)[0] readonly property string downloads: StandardPaths.standardLocations(StandardPaths.DownloadLocation)[0] readonly property string pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0] + readonly property string music: StandardPaths.standardLocations(StandardPaths.MusicLocation)[0] readonly property string videos: StandardPaths.standardLocations(StandardPaths.MoviesLocation)[0] // Other dirs used by the shell, without "file://" diff --git a/.config/quickshell/ii/modules/common/Images.qml b/.config/quickshell/ii/modules/common/Images.qml new file mode 100644 index 000000000..ac76f5118 --- /dev/null +++ b/.config/quickshell/ii/modules/common/Images.qml @@ -0,0 +1,12 @@ +pragma Singleton + +import Quickshell + +Singleton { + readonly property list validImageTypes: ["jpeg", "png", "webp", "tiff", "svg"] + readonly property list validImageExtensions: ["jpg", "jpeg", "png", "webp", "tif", "tiff", "svg"] + + function isValidImageByName(name: string): bool { + return validImageExtensions.some(t => name.endsWith(`.${t}`)); + } +} diff --git a/.config/quickshell/ii/modules/common/AddressBar.qml b/.config/quickshell/ii/modules/common/widgets/AddressBar.qml similarity index 100% rename from .config/quickshell/ii/modules/common/AddressBar.qml rename to .config/quickshell/ii/modules/common/widgets/AddressBar.qml diff --git a/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml b/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml new file mode 100644 index 000000000..b31ad021c --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml @@ -0,0 +1,42 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.modules.common + +// From https://github.com/caelestia-dots/shell with modifications. +// License: GPLv3 + +Image { + id: root + required property var fileModelData + asynchronous: true + fillMode: Image.PreserveAspectFit + + source: { + if (!fileModelData.fileIsDir) + return Quickshell.iconPath("application-x-zerosize"); + + const name = fileModelData.fileName; + const homeDir = Directories.home + if ([Directories.documents, Directories.downloads, Directories.music, Directories.pictures, Directories.videos].includes(name)) + return Quickshell.iconPath(`folder-${name.toLowerCase()}`); + + return Quickshell.iconPath("inode-directory"); + } + + onStatusChanged: { + if (status === Image.Error) + source = Quickshell.iconPath("error"); + } + + Process { + running: !fileModelData.fileIsDir + command: ["file", "--mime", "-b", fileModelData.filePath] + stdout: StdioCollector { + onStreamFinished: { + const mime = text.split(";")[0].replace("/", "-"); + root.source = Images.validImageTypes.some(t => mime === `image-${t}`) ? fileModelData.fileUrl : Quickshell.iconPath(mime, "image-missing"); + } + } + } +} diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml index 1e940ece2..c1f0b83c9 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml @@ -12,14 +12,16 @@ import Quickshell.Io Item { id: root - required property string path + required property var fileModelData + property bool isDirectory: fileModelData.fileIsDir + property bool useThumbnail: Images.isValidImageByName(fileModelData.fileName) property bool isHovered: false property alias color: background.color property alias radius: background.radius property alias padding: background.anchors.margins - signal activated() + signal activated Rectangle { id: background @@ -45,21 +47,27 @@ Item { Layout.fillHeight: true Layout.fillWidth: true - StyledRectangularShadow { - target: thumbnailImageLoader - radius: Appearance.rounding.small + Loader { + id: thumbnailShadowLoader + active: thumbnailImageLoader.active && thumbnailImageLoader.item.status === Image.Ready + anchors.fill: thumbnailImageLoader + sourceComponent: StyledRectangularShadow { + target: thumbnailImageLoader + anchors.fill: undefined + radius: Appearance.rounding.small + } } Loader { id: thumbnailImageLoader anchors.fill: parent - active: root.visible + active: root.useThumbnail sourceComponent: Image { id: thumbnailImage source: { - if (root.path.length == 0) + if (fileModelData.filePath.length == 0) return; - const resolvedUrl = Qt.resolvedUrl(root.path); + const resolvedUrl = Qt.resolvedUrl(fileModelData.filePath); const md5Hash = Qt.md5(resolvedUrl); const cacheSize = "normal"; const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; @@ -79,6 +87,8 @@ Item { Behavior on opacity { animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) } + onStatusChanged: if (status === Image.Error) + root.useThumbnail = false layer.enabled: true layer.effect: OpacityMask { @@ -90,6 +100,17 @@ Item { } } } + + Loader { + id: iconLoader + active: !root.useThumbnail + anchors.fill: parent + sourceComponent: DirectoryIcon { + fileModelData: root.fileModelData + sourceSize.width: wallpaperItemColumnLayout.width + sourceSize.height: wallpaperItemColumnLayout.height - wallpaperItemColumnLayout.spacing - wallpaperItemName.height + } + } } StyledText { @@ -105,7 +126,7 @@ Item { Behavior on color { animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) } - text: FileUtils.fileNameForPath(root.path) + text: fileModelData.fileName } } } @@ -128,4 +149,4 @@ Item { } onClicked: root.activated() } -} \ No newline at end of file +} diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 2fc14a9ef..f6c19b5c1 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -14,8 +14,6 @@ Item { id: root property int columns: 4 property real previewCellAspectRatio: 4 / 3 - property var wallpapers: Wallpapers.wallpapers - property string filterQuery: "" property bool useDarkMode: Appearance.m3colors.darkmode Keys.onPressed: event => { @@ -102,7 +100,7 @@ Item { id: quickDirColumnLayout anchors.fill: parent spacing: 0 - + StyledText { Layout.margins: 12 font { @@ -118,14 +116,46 @@ Item { implicitWidth: 140 clip: true model: [ - { icon: "home", name: "Home", path: Directories.home }, - { icon: "folder", name: "Documents", path: Directories.documents }, - { icon: "download", name: "Downloads", path: Directories.downloads }, - { icon: "image", name: "Pictures", path: Directories.pictures }, - { icon: "movie", name: "Videos", path: Directories.videos }, - { icon: "", name: "---", path: "INTENTIONALLY_INVALID_DIR" }, - { icon: "wallpaper", name: "Wallpapers", path: `${Directories.pictures}/Wallpapers` }, - { icon: "favorite", name: "Homework", path: `${Directories.pictures}/homework` }, + { + icon: "home", + name: "Home", + path: Directories.home + }, + { + icon: "folder", + name: "Documents", + path: Directories.documents + }, + { + icon: "download", + name: "Downloads", + path: Directories.downloads + }, + { + icon: "image", + name: "Pictures", + path: Directories.pictures + }, + { + icon: "movie", + name: "Videos", + path: Directories.videos + }, + { + icon: "", + name: "---", + path: "INTENTIONALLY_INVALID_DIR" + }, + { + icon: "wallpaper", + name: "Wallpapers", + path: `${Directories.pictures}/Wallpapers` + }, + { + icon: "favorite", + name: "Homework", + path: `${Directories.pictures}/homework` + }, ] delegate: RippleButton { id: quickDirButton @@ -191,7 +221,7 @@ Item { GridView { id: grid - visible: root.wallpapers.length > 0 + visible: Wallpapers.folderModel.count > 0 readonly property int columns: root.columns readonly property int rows: Math.max(1, Math.ceil(count / columns)) @@ -217,36 +247,29 @@ Item { item.isHovered = false; } } - currentIndex = Math.max(0, Math.min(root.wallpapers.length - 1, currentIndex + delta)); + currentIndex = Math.max(0, Math.min(grid.model.count - 1, currentIndex + delta)); positionViewAtIndex(currentIndex, GridView.Contain); } function activateCurrent() { - print("ACTIVATE"); - const path = grid.model.values[currentIndex]; - if (!path) - return; - GlobalStates.wallpaperSelectorOpen = false; + const filePath = grid.model.get(currentIndex, "filePath") + Wallpapers.select(filePath, root.useDarkMode); filterField.text = ""; - Wallpapers.apply(path, root.useDarkMode); } - model: ScriptModel { - values: root.wallpapers.filter(w => (w.toLowerCase().includes(root.filterQuery.toLowerCase()))) - } + model: Wallpapers.folderModel onModelChanged: currentIndex = 0 delegate: WallpaperDirectoryItem { required property var modelData required property int index - visible: modelData.length > 0 + fileModelData: modelData width: grid.cellWidth height: grid.cellHeight - path: modelData color: (index === grid?.currentIndex || parent?.isHovered) ? Appearance.colors.colPrimary : ColorUtils.transparentize(Appearance.colors.colPrimary) onActivated: { - Wallpapers.apply(path, root.useDarkMode); - GlobalStates.wallpaperSelectorOpen = false; + Wallpapers.select(fileModelData.filePath, root.useDarkMode); + filterField.text = ""; } } @@ -339,7 +362,7 @@ Item { } onTextChanged: { - root.filterQuery = text; + Wallpapers.searchQuery = text; } Keys.onPressed: event => { @@ -389,4 +412,11 @@ Item { } } } + + Connections { + target: Wallpapers + function onChanged() { + GlobalStates.wallpaperSelectorOpen = false; + } + } } diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index d01e38034..b98d3fe70 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -15,12 +15,15 @@ Singleton { id: root property string directory: FileUtils.trimFileProtocol(`${Directories.pictures}/Wallpapers`) + property alias folderModel: folderModel // Expose for direct binding when needed + property string searchQuery: "" readonly property list extensions: [ // TODO: add videos "jpg", "jpeg", "png", "webp", "avif", "bmp", "svg" ] - property alias filesModel: files // Expose for direct binding when needed property list wallpapers: [] // List of absolute file paths (without file://) + signal changed() + // Executions Process { id: applyProc @@ -37,6 +40,29 @@ Singleton { "--image", path, "--mode", (darkMode ? "dark" : "light") ]) + root.changed() + } + + Process { + id: selectProc + property string filePath: "" + property bool darkMode: Appearance.m3colors.darkmode + function select(filePath, darkMode = Appearance.m3colors.darkmode) { + selectProc.filePath = filePath + selectProc.darkMode = darkMode + selectProc.exec(["test", "-d", FileUtils.trimFileProtocol(filePath)]) + } + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + setDirectory(selectProc.filePath); + return; + } + root.apply(selectProc.filePath, selectProc.darkMode); + } + } + + function select(filePath, darkMode = Appearance.m3colors.darkmode) { + selectProc.select(filePath); } Process { @@ -59,9 +85,10 @@ Singleton { // Folder model FolderListModel { - id: files + id: folderModel folder: Qt.resolvedUrl(root.directory) - nameFilters: root.extensions.map(ext => `*.${ext}`) + caseSensitive: false + nameFilters: root.extensions.map(ext => `*${searchQuery.split(" ").filter(s => s.length > 0).map(s => `*${s}*`)}*.${ext}`) showDirs: true showDotAndDotDot: false showOnlyReadable: true @@ -69,8 +96,8 @@ Singleton { sortReversed: false onCountChanged: { root.wallpapers = [] - for (let i = 0; i < files.count; i++) { - const path = files.get(i, "filePath") || FileUtils.trimFileProtocol(files.get(i, "fileURL")) + for (let i = 0; i < folderModel.count; i++) { + const path = folderModel.get(i, "filePath") || FileUtils.trimFileProtocol(folderModel.get(i, "fileURL")) if (path && path.length) root.wallpapers.push(path) } } From 8277a2d942f383feed7f019b48d352b6c6d4015f Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 24 Aug 2025 14:25:39 +0700 Subject: [PATCH 30/35] wallpaper selector: fix dark mode --- .../modules/common/widgets/DirectoryIcon.qml | 5 +- .../WallpaperDirectoryItem.qml | 4 +- .../WallpaperSelectorContent.qml | 55 ++++--------------- .config/quickshell/ii/services/Wallpapers.qml | 9 ++- 4 files changed, 21 insertions(+), 52 deletions(-) diff --git a/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml b/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml index b31ad021c..acdb65898 100644 --- a/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml +++ b/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml @@ -16,10 +16,9 @@ Image { if (!fileModelData.fileIsDir) return Quickshell.iconPath("application-x-zerosize"); - const name = fileModelData.fileName; const homeDir = Directories.home - if ([Directories.documents, Directories.downloads, Directories.music, Directories.pictures, Directories.videos].includes(name)) - return Quickshell.iconPath(`folder-${name.toLowerCase()}`); + if ([Directories.documents, Directories.downloads, Directories.music, Directories.pictures, Directories.videos].includes(fileModelData.filePath)) + return Quickshell.iconPath(`folder-${fileModelData.fileName.toLowerCase()}`); return Quickshell.iconPath("inode-directory"); } diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml index c1f0b83c9..66f3f9e24 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml @@ -17,7 +17,8 @@ Item { property bool useThumbnail: Images.isValidImageByName(fileModelData.fileName) property bool isHovered: false - property alias color: background.color + property alias colBackground: background.color + property alias colText: wallpaperItemName.color property alias radius: background.radius property alias padding: background.anchors.margins @@ -122,7 +123,6 @@ Item { horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight font.pixelSize: Appearance.font.pixelSize.smaller - color: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer0 Behavior on color { animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) } diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index f6c19b5c1..8a985bfb0 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -116,46 +116,14 @@ Item { implicitWidth: 140 clip: true model: [ - { - icon: "home", - name: "Home", - path: Directories.home - }, - { - icon: "folder", - name: "Documents", - path: Directories.documents - }, - { - icon: "download", - name: "Downloads", - path: Directories.downloads - }, - { - icon: "image", - name: "Pictures", - path: Directories.pictures - }, - { - icon: "movie", - name: "Videos", - path: Directories.videos - }, - { - icon: "", - name: "---", - path: "INTENTIONALLY_INVALID_DIR" - }, - { - icon: "wallpaper", - name: "Wallpapers", - path: `${Directories.pictures}/Wallpapers` - }, - { - icon: "favorite", - name: "Homework", - path: `${Directories.pictures}/homework` - }, + { icon: "home", name: "Home", path: Directories.home }, + { icon: "docs", name: "Documents", path: Directories.documents }, + { icon: "download", name: "Downloads", path: Directories.downloads }, + { icon: "image", name: "Pictures", path: Directories.pictures }, + { icon: "movie", name: "Videos", path: Directories.videos }, + { icon: "", name: "---", path: "INTENTIONALLY_INVALID_DIR" }, + { icon: "wallpaper", name: "Wallpapers", path: `${Directories.pictures}/Wallpapers` }, + { icon: "favorite", name: "Homework", path: `${Directories.pictures}/homework` }, ] delegate: RippleButton { id: quickDirButton @@ -266,7 +234,8 @@ Item { fileModelData: modelData width: grid.cellWidth height: grid.cellHeight - color: (index === grid?.currentIndex || parent?.isHovered) ? Appearance.colors.colPrimary : ColorUtils.transparentize(Appearance.colors.colPrimary) + colBackground: (index === grid?.currentIndex || parent?.isHovered) ? Appearance.colors.colPrimaryContainer : ColorUtils.transparentize(Appearance.colors.colPrimaryContainer) + colText: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colPrimary : Appearance.colors.colOnLayer0 onActivated: { Wallpapers.select(fileModelData.filePath, root.useDarkMode); @@ -314,7 +283,7 @@ Item { implicitWidth: height buttonRadius: Appearance.rounding.full onClicked: { - Wallpapers.openFallbackPicker(); + Wallpapers.openFallbackPicker(root.useDarkMode); GlobalStates.wallpaperSelectorOpen = false; } contentItem: MaterialSymbol { @@ -370,12 +339,10 @@ Item { // No filtering, just navigate grid if (event.key === Qt.Key_Down) { grid.moveSelection(grid.columns); - wallpaperGrid.forceActiveFocus(); event.accepted = true; } if (event.key === Qt.Key_Up) { grid.moveSelection(-grid.columns); - wallpaperGrid.forceActiveFocus(); event.accepted = true; } } diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index b98d3fe70..d1c211518 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -29,8 +29,11 @@ Singleton { id: applyProc } - function openFallbackPicker() { - applyProc.exec([Directories.wallpaperSwitchScriptPath]) + function openFallbackPicker(darkMode = Appearance.m3colors.darkmode) { + applyProc.exec([ + Directories.wallpaperSwitchScriptPath, + "--mode", (darkMode ? "dark" : "light") + ]) } function apply(path, darkMode = Appearance.m3colors.darkmode) { @@ -62,7 +65,7 @@ Singleton { } function select(filePath, darkMode = Appearance.m3colors.darkmode) { - selectProc.select(filePath); + selectProc.select(filePath, darkMode); } Process { From ef4ae4480f66be85fb11a803a0c8ee5d7431af19 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 24 Aug 2025 16:12:37 +0700 Subject: [PATCH 31/35] wallpaper selector: not reinvent MouseArea containsMouse --- .../WallpaperDirectoryItem.qml | 25 +++---------------- .../WallpaperSelectorContent.qml | 16 ++++++------ 2 files changed, 11 insertions(+), 30 deletions(-) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml index 66f3f9e24..382880df4 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml @@ -10,12 +10,11 @@ import Qt5Compat.GraphicalEffects import Quickshell import Quickshell.Io -Item { +MouseArea { id: root required property var fileModelData property bool isDirectory: fileModelData.fileIsDir property bool useThumbnail: Images.isValidImageByName(fileModelData.fileName) - property bool isHovered: false property alias colBackground: background.color property alias colText: wallpaperItemName.color @@ -24,6 +23,9 @@ Item { signal activated + hoverEnabled: true + onClicked: root.activated() + Rectangle { id: background anchors { @@ -130,23 +132,4 @@ Item { } } } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onEntered: { - for (let i = 0; i < grid.count; i++) { - const item = grid.itemAtIndex(i); - if (item && item !== parent) { - item.isHovered = false; - } - } - parent.isHovered = true; - grid.currentIndex = index; - } - onExited: { - parent.isHovered = false; - } - onClicked: root.activated() - } } diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 8a985bfb0..bc6d3408a 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -209,12 +209,6 @@ Item { } function moveSelection(delta) { - for (let i = 0; i < count; i++) { - const item = itemAtIndex(i); - if (item) { - item.isHovered = false; - } - } currentIndex = Math.max(0, Math.min(grid.model.count - 1, currentIndex + delta)); positionViewAtIndex(currentIndex, GridView.Contain); } @@ -234,11 +228,15 @@ Item { fileModelData: modelData width: grid.cellWidth height: grid.cellHeight - colBackground: (index === grid?.currentIndex || parent?.isHovered) ? Appearance.colors.colPrimaryContainer : ColorUtils.transparentize(Appearance.colors.colPrimaryContainer) - colText: (index === grid.currentIndex || parent.isHovered) ? Appearance.colors.colPrimary : Appearance.colors.colOnLayer0 + colBackground: (index === grid?.currentIndex || containsMouse) ? Appearance.colors.colPrimaryContainer : ColorUtils.transparentize(Appearance.colors.colPrimaryContainer) + colText: (index === grid.currentIndex || containsMouse) ? Appearance.colors.colPrimary : Appearance.colors.colOnLayer0 + + onEntered: { + grid.currentIndex = index; + } + onActivated: { Wallpapers.select(fileModelData.filePath, root.useDarkMode); - filterField.text = ""; } } From f3ab3573c3e482b87f59af300990d2f446d42d00 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 24 Aug 2025 16:39:50 +0700 Subject: [PATCH 32/35] wallpaper selector: style the scrollbar --- .../common/widgets/StyledScrollBar.qml | 26 +++++++++++++++++++ .../WallpaperSelectorContent.qml | 4 +-- 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 .config/quickshell/ii/modules/common/widgets/StyledScrollBar.qml diff --git a/.config/quickshell/ii/modules/common/widgets/StyledScrollBar.qml b/.config/quickshell/ii/modules/common/widgets/StyledScrollBar.qml new file mode 100644 index 000000000..ab357e09b --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/StyledScrollBar.qml @@ -0,0 +1,26 @@ +import QtQuick +import QtQuick.Controls +import qs.modules.common +import qs.modules.common.functions + +ScrollBar { + id: root + + policy: ScrollBar.AsNeeded + + contentItem: Rectangle { + implicitWidth: 4 + implicitHeight: root.visualSize + radius: width / 2 + color: Appearance.colors.colOnSurfaceVariant + + opacity: root.policy === ScrollBar.AlwaysOn || (root.active && root.size < 1.0) ? 0.5 : 0 + Behavior on opacity { + NumberAnimation { + duration: 350 + easing.type: Appearance.animation.elementMoveFast.type + easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve + } + } + } +} diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index bc6d3408a..f97909bd3 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -204,9 +204,7 @@ Item { boundsBehavior: Flickable.StopAtBounds bottomMargin: extraOptions.implicitHeight - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded - } + ScrollBar.vertical: StyledScrollBar {} function moveSelection(delta) { currentIndex = Math.max(0, Math.min(grid.model.count - 1, currentIndex + delta)); From b2d14ca101762d7b6c911fb3d8a2d4cd12f6930d Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sun, 24 Aug 2025 18:59:41 +0700 Subject: [PATCH 33/35] wallpaper selector: thumbnail generation, fix xdg dir folder icons --- .../ii/modules/common/ThumbnailImage.qml | 67 +++++++++++++++++++ .../modules/common/widgets/DirectoryIcon.qml | 4 +- .../WallpaperDirectoryItem.qml | 23 +------ .../WallpaperSelectorContent.qml | 4 +- 4 files changed, 73 insertions(+), 25 deletions(-) create mode 100644 .config/quickshell/ii/modules/common/ThumbnailImage.qml diff --git a/.config/quickshell/ii/modules/common/ThumbnailImage.qml b/.config/quickshell/ii/modules/common/ThumbnailImage.qml new file mode 100644 index 000000000..a1a4c204f --- /dev/null +++ b/.config/quickshell/ii/modules/common/ThumbnailImage.qml @@ -0,0 +1,67 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.modules.common +import qs.modules.common.functions + +/** + * Thumbnail image. + * See Freedesktop's spec: https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html + */ +Image { + id: root + + required property string sourcePath + readonly property var thumbnailSizes: ({ + "normal": 128, + "large": 256, + "x-large": 512, + "xx-large": 1024 + }) + property string thumbnailSizeName: { // https://specifications.freedesktop.org/thumbnail-spec/latest/directory.html + const sizeNames = Object.keys(thumbnailSizes); + for(let i = 0; i < sizeNames.length; i++) { + const sizeName = sizeNames[i]; + const maxSize = thumbnailSizes[sizeName]; + if (root.sourceSize.width <= maxSize && root.sourceSize.height <= maxSize) return sizeName; + } + return "xx-large"; + } + property string thumbnailPath: { + if (sourcePath.length == 0) return; + const resolvedUrl = Qt.resolvedUrl(sourcePath); + const md5Hash = Qt.md5(resolvedUrl); + return `${Directories.genericCache}/thumbnails/${thumbnailSizeName}/${md5Hash}.png`; + } + source: thumbnailPath + + asynchronous: true + cache: false + smooth: true + mipmap: false + + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + onSourceSizeChanged: { + thumbnailGeneration.running = false + thumbnailGeneration.running = true + } + Process { + id: thumbnailGeneration + command: { + const maxSize = root.thumbnailSizes[root.thumbnailSizeName]; + return ["bash", "-c", + `[ -f '${FileUtils.trimFileProtocol(root.thumbnailPath)}' ] && exit 0 || { magick '${root.sourcePath}' -resize ${maxSize}x${maxSize} '${FileUtils.trimFileProtocol(root.thumbnailPath)}' && exit 1; }` + ] + } + onExited: (exitCode, exitStatus) => { + if (exitCode === 1) { // Force reload if thumbnail had to be generated + root.source = ""; + root.source = root.thumbnailPath; // Force reload + } + } + } +} diff --git a/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml b/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml index acdb65898..9df2ee2fe 100644 --- a/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml +++ b/.config/quickshell/ii/modules/common/widgets/DirectoryIcon.qml @@ -2,6 +2,7 @@ import QtQuick import Quickshell import Quickshell.Io import qs.modules.common +import qs.modules.common.functions // From https://github.com/caelestia-dots/shell with modifications. // License: GPLv3 @@ -16,8 +17,7 @@ Image { if (!fileModelData.fileIsDir) return Quickshell.iconPath("application-x-zerosize"); - const homeDir = Directories.home - if ([Directories.documents, Directories.downloads, Directories.music, Directories.pictures, Directories.videos].includes(fileModelData.filePath)) + if ([Directories.documents, Directories.downloads, Directories.music, Directories.pictures, Directories.videos].some(dir => FileUtils.trimFileProtocol(dir) === fileModelData.filePath)) return Quickshell.iconPath(`folder-${fileModelData.fileName.toLowerCase()}`); return Quickshell.iconPath("inode-directory"); diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml index 382880df4..2616219c8 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml @@ -65,34 +65,15 @@ MouseArea { id: thumbnailImageLoader anchors.fill: parent active: root.useThumbnail - sourceComponent: Image { + sourceComponent: ThumbnailImage { id: thumbnailImage - source: { - if (fileModelData.filePath.length == 0) - return; - const resolvedUrl = Qt.resolvedUrl(fileModelData.filePath); - const md5Hash = Qt.md5(resolvedUrl); - const cacheSize = "normal"; - const thumbnailPath = `${Directories.genericCache}/thumbnails/${cacheSize}/${md5Hash}.png`; - return thumbnailPath; - } - asynchronous: true - cache: false - smooth: true - mipmap: false + sourcePath: fileModelData.filePath fillMode: Image.PreserveAspectCrop clip: true sourceSize.width: wallpaperItemColumnLayout.width sourceSize.height: wallpaperItemColumnLayout.height - wallpaperItemColumnLayout.spacing - wallpaperItemName.height - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - onStatusChanged: if (status === Image.Error) - root.useThumbnail = false - layer.enabled: true layer.effect: OpacityMask { maskSource: Rectangle { diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index f97909bd3..11526ed3b 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -226,8 +226,8 @@ Item { fileModelData: modelData width: grid.cellWidth height: grid.cellHeight - colBackground: (index === grid?.currentIndex || containsMouse) ? Appearance.colors.colPrimaryContainer : ColorUtils.transparentize(Appearance.colors.colPrimaryContainer) - colText: (index === grid.currentIndex || containsMouse) ? Appearance.colors.colPrimary : Appearance.colors.colOnLayer0 + colBackground: (fileModelData.filePath === Config.options.background.wallpaperPath) ? Appearance.colors.colPrimary : (index === grid?.currentIndex || containsMouse) ? Appearance.colors.colPrimaryContainer : ColorUtils.transparentize(Appearance.colors.colPrimaryContainer) + colText: (fileModelData.filePath === Config.options.background.wallpaperPath) ? Appearance.colors.colOnPrimary : (index === grid.currentIndex || containsMouse) ? Appearance.colors.colPrimary : Appearance.colors.colOnLayer0 onEntered: { grid.currentIndex = index; From eb2c9f2fe12e9bcf23df4ca3ccd83c5fd47db450 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Tue, 26 Aug 2025 13:58:29 +0700 Subject: [PATCH 34/35] wallpaper selector: adjust size and item colors, fix address bar inconsistency --- .config/quickshell/ii/modules/common/Appearance.qml | 4 ++-- .config/quickshell/ii/modules/common/widgets/AddressBar.qml | 5 ++++- .../modules/wallpaperSelector/WallpaperSelectorContent.qml | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.config/quickshell/ii/modules/common/Appearance.qml b/.config/quickshell/ii/modules/common/Appearance.qml index e1f0853a5..ddcbb3ca9 100644 --- a/.config/quickshell/ii/modules/common/Appearance.qml +++ b/.config/quickshell/ii/modules/common/Appearance.qml @@ -343,8 +343,8 @@ Singleton { property real baseVerticalBarWidth: 46 property real verticalBarWidth: Config.options.bar.cornerStyle === 1 ? (baseVerticalBarWidth + root.sizes.hyprlandGapsOut * 2) : baseVerticalBarWidth - property real wallpaperSelectorWidth: 1000 - property real wallpaperSelectorHeight: 580 + property real wallpaperSelectorWidth: 1200 + property real wallpaperSelectorHeight: 690 } syntaxHighlightingTheme: root.m3colors.darkmode ? "Monokai" : "ayu Light" diff --git a/.config/quickshell/ii/modules/common/widgets/AddressBar.qml b/.config/quickshell/ii/modules/common/widgets/AddressBar.qml index a5328d446..c965a536d 100644 --- a/.config/quickshell/ii/modules/common/widgets/AddressBar.qml +++ b/.config/quickshell/ii/modules/common/widgets/AddressBar.qml @@ -8,7 +8,10 @@ import qs.modules.common.functions Rectangle { id: root required property var directory - property bool showBreadcrumb: true // TODO: make this work + property bool showBreadcrumb: true + onShowBreadcrumbChanged: { + addressInput.text = root.directory; + } signal navigateToDirectory(string path) diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 11526ed3b..70d038ae1 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -226,8 +226,8 @@ Item { fileModelData: modelData width: grid.cellWidth height: grid.cellHeight - colBackground: (fileModelData.filePath === Config.options.background.wallpaperPath) ? Appearance.colors.colPrimary : (index === grid?.currentIndex || containsMouse) ? Appearance.colors.colPrimaryContainer : ColorUtils.transparentize(Appearance.colors.colPrimaryContainer) - colText: (fileModelData.filePath === Config.options.background.wallpaperPath) ? Appearance.colors.colOnPrimary : (index === grid.currentIndex || containsMouse) ? Appearance.colors.colPrimary : Appearance.colors.colOnLayer0 + colBackground: (index === grid?.currentIndex || containsMouse) ? Appearance.colors.colPrimary : (fileModelData.filePath === Config.options.background.wallpaperPath) ? Appearance.colors.colSecondaryContainer : ColorUtils.transparentize(Appearance.colors.colPrimaryContainer) + colText: (index === grid.currentIndex || containsMouse) ? Appearance.colors.colOnPrimary : (fileModelData.filePath === Config.options.background.wallpaperPath) ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer0 onEntered: { grid.currentIndex = index; From 1c1a1417015741ed13d195c38b8b2844d627ea5f Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Wed, 27 Aug 2025 21:51:15 +0700 Subject: [PATCH 35/35] wallpaper selector: freedesktop spec-compliant thumbnail generation --- .../ii/modules/common/Appearance.qml | 2 + .../quickshell/ii/modules/common/Images.qml | 19 +++ .../ii/modules/common/ThumbnailImage.qml | 26 ++--- .../WallpaperDirectoryItem.qml | 28 +++-- .../WallpaperSelectorContent.qml | 19 ++- .../ii/scripts/thumbnails/thumbgen.py | 109 ++++++++++++++++++ .config/quickshell/ii/services/Wallpapers.qml | 22 ++++ scriptdata/requirements.in | 5 + scriptdata/requirements.txt | 12 ++ 9 files changed, 211 insertions(+), 31 deletions(-) create mode 100755 .config/quickshell/ii/scripts/thumbnails/thumbgen.py diff --git a/.config/quickshell/ii/modules/common/Appearance.qml b/.config/quickshell/ii/modules/common/Appearance.qml index ddcbb3ca9..f1a8daec0 100644 --- a/.config/quickshell/ii/modules/common/Appearance.qml +++ b/.config/quickshell/ii/modules/common/Appearance.qml @@ -345,6 +345,8 @@ Singleton { (baseVerticalBarWidth + root.sizes.hyprlandGapsOut * 2) : baseVerticalBarWidth property real wallpaperSelectorWidth: 1200 property real wallpaperSelectorHeight: 690 + property real wallpaperSelectorItemMargins: 8 + property real wallpaperSelectorItemPadding: 6 } syntaxHighlightingTheme: root.m3colors.darkmode ? "Monokai" : "ayu Light" diff --git a/.config/quickshell/ii/modules/common/Images.qml b/.config/quickshell/ii/modules/common/Images.qml index ac76f5118..be3701efc 100644 --- a/.config/quickshell/ii/modules/common/Images.qml +++ b/.config/quickshell/ii/modules/common/Images.qml @@ -3,10 +3,29 @@ pragma Singleton import Quickshell Singleton { + // Formats readonly property list validImageTypes: ["jpeg", "png", "webp", "tiff", "svg"] readonly property list validImageExtensions: ["jpg", "jpeg", "png", "webp", "tif", "tiff", "svg"] function isValidImageByName(name: string): bool { return validImageExtensions.some(t => name.endsWith(`.${t}`)); } + + // Thumbnails + // https://specifications.freedesktop.org/thumbnail-spec/latest/directory.html + readonly property var thumbnailSizes: ({ + "normal": 128, + "large": 256, + "x-large": 512, + "xx-large": 1024 + }) + function thumbnailSizeNameForDimensions(width: int, height: int): string { + const sizeNames = Object.keys(thumbnailSizes); + for(let i = 0; i < sizeNames.length; i++) { + const sizeName = sizeNames[i]; + const maxSize = thumbnailSizes[sizeName]; + if (width <= maxSize && height <= maxSize) return sizeName; + } + return "xx-large"; + } } diff --git a/.config/quickshell/ii/modules/common/ThumbnailImage.qml b/.config/quickshell/ii/modules/common/ThumbnailImage.qml index a1a4c204f..ee928eef1 100644 --- a/.config/quickshell/ii/modules/common/ThumbnailImage.qml +++ b/.config/quickshell/ii/modules/common/ThumbnailImage.qml @@ -5,28 +5,15 @@ import qs.modules.common import qs.modules.common.functions /** - * Thumbnail image. + * Thumbnail image. It currently generates to the right place at the right size, but does not handle metadata/maintenance on modification. * See Freedesktop's spec: https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html */ Image { id: root + property bool generateThumbnail: true required property string sourcePath - readonly property var thumbnailSizes: ({ - "normal": 128, - "large": 256, - "x-large": 512, - "xx-large": 1024 - }) - property string thumbnailSizeName: { // https://specifications.freedesktop.org/thumbnail-spec/latest/directory.html - const sizeNames = Object.keys(thumbnailSizes); - for(let i = 0; i < sizeNames.length; i++) { - const sizeName = sizeNames[i]; - const maxSize = thumbnailSizes[sizeName]; - if (root.sourceSize.width <= maxSize && root.sourceSize.height <= maxSize) return sizeName; - } - return "xx-large"; - } + property string thumbnailSizeName: Images.thumbnailSizeNameForDimensions(sourceSize.width, sourceSize.height) property string thumbnailPath: { if (sourcePath.length == 0) return; const resolvedUrl = Qt.resolvedUrl(sourcePath); @@ -46,13 +33,14 @@ Image { } onSourceSizeChanged: { - thumbnailGeneration.running = false - thumbnailGeneration.running = true + if (!root.generateThumbnail) return; + thumbnailGeneration.running = false; + thumbnailGeneration.running = true; } Process { id: thumbnailGeneration command: { - const maxSize = root.thumbnailSizes[root.thumbnailSizeName]; + const maxSize = Images.thumbnailSizes[root.thumbnailSizeName]; return ["bash", "-c", `[ -f '${FileUtils.trimFileProtocol(root.thumbnailPath)}' ] && exit 0 || { magick '${root.sourcePath}' -resize ${maxSize}x${maxSize} '${FileUtils.trimFileProtocol(root.thumbnailPath)}' && exit 1; }` ] diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml index 2616219c8..0b3f877f4 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperDirectoryItem.qml @@ -19,19 +19,19 @@ MouseArea { property alias colBackground: background.color property alias colText: wallpaperItemName.color property alias radius: background.radius - property alias padding: background.anchors.margins + property alias margins: background.anchors.margins + property alias padding: wallpaperItemColumnLayout.anchors.margins + margins: Appearance.sizes.wallpaperSelectorItemMargins + padding: Appearance.sizes.wallpaperSelectorItemPadding - signal activated + signal activated() hoverEnabled: true onClicked: root.activated() Rectangle { id: background - anchors { - fill: parent - margins: 8 - } + anchors.fill: parent radius: Appearance.rounding.normal Behavior on color { animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) @@ -39,10 +39,7 @@ MouseArea { ColumnLayout { id: wallpaperItemColumnLayout - anchors { - fill: parent - margins: 6 - } + anchors.fill: parent spacing: 4 Item { @@ -67,6 +64,7 @@ MouseArea { active: root.useThumbnail sourceComponent: ThumbnailImage { id: thumbnailImage + generateThumbnail: false sourcePath: fileModelData.filePath fillMode: Image.PreserveAspectCrop @@ -74,6 +72,16 @@ MouseArea { sourceSize.width: wallpaperItemColumnLayout.width sourceSize.height: wallpaperItemColumnLayout.height - wallpaperItemColumnLayout.spacing - wallpaperItemName.height + Connections { + target: Wallpapers + function onThumbnailGenerated(directory) { + if (thumbnailImage.status !== Image.Error) return; + if (FileUtils.parentDirectory(thumbnailImage.sourcePath) !== directory) return; + thumbnailImage.source = ""; + thumbnailImage.source = thumbnailImage.thumbnailPath; + } + } + layer.enabled: true layer.effect: OpacityMask { maskSource: Rectangle { diff --git a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml index 70d038ae1..b55233417 100644 --- a/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml +++ b/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml @@ -16,6 +16,19 @@ Item { property real previewCellAspectRatio: 4 / 3 property bool useDarkMode: Appearance.m3colors.darkmode + function updateThumbnails() { + const totalImageMargin = (Appearance.sizes.wallpaperSelectorItemMargins + Appearance.sizes.wallpaperSelectorItemPadding) * 2 + const thumbnailSizeName = Images.thumbnailSizeNameForDimensions(grid.cellWidth - totalImageMargin, grid.cellHeight - totalImageMargin) + Wallpapers.generateThumbnail(thumbnailSizeName) + } + + Connections { + target: Wallpapers + function onDirectoryChanged() { + root.updateThumbnails() + } + } + Keys.onPressed: event => { if (event.key === Qt.Key_Escape) { GlobalStates.wallpaperSelectorOpen = false; @@ -203,9 +216,12 @@ Item { keyNavigationWraps: true boundsBehavior: Flickable.StopAtBounds bottomMargin: extraOptions.implicitHeight - ScrollBar.vertical: StyledScrollBar {} + Component.onCompleted: { + root.updateThumbnails() + } + function moveSelection(delta) { currentIndex = Math.max(0, Math.min(grid.model.count - 1, currentIndex + delta)); positionViewAtIndex(currentIndex, GridView.Contain); @@ -219,7 +235,6 @@ Item { model: Wallpapers.folderModel onModelChanged: currentIndex = 0 - delegate: WallpaperDirectoryItem { required property var modelData required property int index diff --git a/.config/quickshell/ii/scripts/thumbnails/thumbgen.py b/.config/quickshell/ii/scripts/thumbnails/thumbgen.py new file mode 100755 index 000000000..4dc3ccd56 --- /dev/null +++ b/.config/quickshell/ii/scripts/thumbnails/thumbgen.py @@ -0,0 +1,109 @@ +#!/usr/bin/env -S\_/bin/sh\_-xc\_"source\_\$(eval\_echo\_\$ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate&&exec\_python\_-E\_"\$0"\_"\$@"" + +# From https://github.com/difference-engine/thumbnail-generator-ubuntu (MIT License) +# Since the script is small and the maintainers seem inactive to accept my PR (#11) I decided to just copy it over. +# When it gets merged and the python package gets updated we can just use it + +import os +import sys +from multiprocessing import Pool +from pathlib import Path +from typing import List, Union + +import click +import gi +from loguru import logger +from tqdm import tqdm + +gi.require_version("GnomeDesktop", "3.0") +from gi.repository import Gio, GnomeDesktop # isort:skip + +thumbnail_size_map = { + "normal": GnomeDesktop.DesktopThumbnailSize.NORMAL, + "large": GnomeDesktop.DesktopThumbnailSize.LARGE, + "x-large": GnomeDesktop.DesktopThumbnailSize.XLARGE, + "xx-large": GnomeDesktop.DesktopThumbnailSize.XXLARGE, +} + +factory = None +logger.remove() +logger.add(sys.stdout, level="INFO") +logger.add("/tmp/thumbgen.log", level="DEBUG", rotation="100 MB") + +def make_thumbnail(fpath: str) -> bool: + mtime = os.path.getmtime(fpath) + # Use Gio to determine the URI and mime type + f = Gio.file_new_for_path(str(fpath)) + uri = f.get_uri() + info = f.query_info("standard::content-type", Gio.FileQueryInfoFlags.NONE, None) + mime_type = info.get_content_type() + + if factory.lookup(uri, mtime) is not None: + logger.debug("FRESH {}".format(uri)) + return False + + if not factory.can_thumbnail(uri, mime_type, mtime): + logger.debug("UNSUPPORTED {}".format(uri)) + return False + + thumbnail = factory.generate_thumbnail(uri, mime_type) + if thumbnail is None: + logger.debug("ERROR {}".format(uri)) + return False + + logger.debug("OK {}".format(uri)) + factory.save_thumbnail(thumbnail, uri, mtime) + return True + + +@logger.catch() +def thumbnail_folder(*, dir_path: Path, workers: int, only_images: bool, recursive: bool) -> None: + all_files = get_all_files(dir_path=dir_path, recursive=recursive) + if only_images: + all_files = get_all_images(all_files=all_files) + all_files = [str(fpath) for fpath in all_files] + with Pool(processes=workers) as p: + list(tqdm(p.imap(make_thumbnail, all_files), total=len(all_files))) + + +def get_all_images(*, all_files: List[Path]) -> List[Path]: + img_suffixes = [".jpg", ".jpeg", ".png", ".gif"] + all_images = [fpath for fpath in all_files if fpath.suffix in img_suffixes] + print("Found {} images".format(len(all_images))) + return all_images + + +def get_all_files(*, dir_path: Path, recursive: bool) -> List[Path]: + if not (dir_path.exists() and dir_path.is_dir()): + raise ValueError("{} doesn't exist or isn't a valid directory!".format(dir_path.resolve())) + if recursive: + all_files = dir_path.rglob("*") + else: + all_files = dir_path.glob("*") + all_files = [fpath for fpath in all_files if fpath.is_file()] + print("Found {} files in the directory: {}".format(len(all_files), dir_path.resolve())) + return all_files + +@click.command() +@click.option( + "-d", "--img_dirs", required=True, help='directories to generate thumbnails seperated by space, eg: "dir1/dir2 dir3"' +) +@click.option( + "-s", "--size", default="normal", type=click.Choice(["normal", "large", "x-large", "xx-large"]), help="Thumbnail size: normal, large, x-large, xx-large" +) +@click.option("-w", "--workers", default=1, help="no of cpus to use for processing") +@click.option( + "-i", "--only_images", is_flag=True, default=False, help="Whether to only look for images to be thumbnailed" +) +@click.option("-r", "--recursive", is_flag=True, default=False, help="Whether to recursively look for files") +def main(img_dirs: str, size: str, workers: str, only_images: bool, recursive: bool) -> None: + img_dirs = [Path(img_dir) for img_dir in img_dirs.split()] + global factory + factory = GnomeDesktop.DesktopThumbnailFactory.new(thumbnail_size_map[size]) + for img_dir in img_dirs: + thumbnail_folder(dir_path=img_dir, workers=workers, only_images=only_images, recursive=recursive) + print("Thumbnail Generation Completed!") + + +if __name__ == "__main__": + main() diff --git a/.config/quickshell/ii/services/Wallpapers.qml b/.config/quickshell/ii/services/Wallpapers.qml index d1c211518..ebd7c27e5 100644 --- a/.config/quickshell/ii/services/Wallpapers.qml +++ b/.config/quickshell/ii/services/Wallpapers.qml @@ -14,6 +14,7 @@ pragma ComponentBehavior: Bound Singleton { id: root + property string thumbgenScriptPath: `${FileUtils.trimFileProtocol(Directories.scriptPath)}/thumbnails/thumbgen.py` property string directory: FileUtils.trimFileProtocol(`${Directories.pictures}/Wallpapers`) property alias folderModel: folderModel // Expose for direct binding when needed property string searchQuery: "" @@ -23,6 +24,7 @@ Singleton { property list wallpapers: [] // List of absolute file paths (without file://) signal changed() + signal thumbnailGenerated(directory: string) // Executions Process { @@ -105,4 +107,24 @@ Singleton { } } } + + // Thumbnail generation + function generateThumbnail(size: string) { + if (!["normal", "large", "x-large", "xx-large"].includes(size)) throw new Error("Invalid thumbnail size"); + thumbgenProc.directory = root.directory + thumbgenProc.running = false + thumbgenProc.command = [ + thumbgenScriptPath, + "--size", size, + "-d", `${root.directory}` + ] + thumbgenProc.running = true + } + Process { + id: thumbgenProc + property string directory + onExited: (exitCode, exitStatus) => { + root.thumbnailGenerated(thumbgenProc.directory) + } + } } diff --git a/scriptdata/requirements.in b/scriptdata/requirements.in index 0b4ac323e..26a028973 100644 --- a/scriptdata/requirements.in +++ b/scriptdata/requirements.in @@ -8,3 +8,8 @@ materialyoucolor libsass material-color-utilities setproctitle +click +loguru +pycairo +pygobject +tqdm diff --git a/scriptdata/requirements.txt b/scriptdata/requirements.txt index c2f380c4a..e6b96aba1 100644 --- a/scriptdata/requirements.txt +++ b/scriptdata/requirements.txt @@ -4,8 +4,12 @@ build==1.2.2.post1 # via -r scriptdata/requirements.in cffi==1.17.1 # via pywayland +click==8.2.1 + # via -r scriptdata/requirements.in libsass==0.23.0 # via -r scriptdata/requirements.in +loguru==0.7.3 + # via -r scriptdata/requirements.in material-color-utilities==0.2.1 # via -r scriptdata/requirements.in materialyoucolor==2.0.10 @@ -22,8 +26,14 @@ pillow==11.1.0 # material-color-utilities psutil==6.1.1 # via -r scriptdata/requirements.in +pycairo==1.28.0 + # via + # -r scriptdata/requirements.in + # pygobject pycparser==2.22 # via cffi +pygobject==3.52.3 + # via -r scriptdata/requirements.in pyproject-hooks==1.2.0 # via build pywayland==0.4.18 @@ -34,5 +44,7 @@ setuptools==80.9.0 # via setuptools-scm setuptools-scm==8.1.0 # via -r scriptdata/requirements.in +tqdm==4.67.1 + # via -r scriptdata/requirements.in wheel==0.45.1 # via -r scriptdata/requirements.in