forked from Shinonome/dots-hyprland
feat(wallpaper selector): add wallpaper selector menu
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string> searchDirs: [ FileUtils.trimFileProtocol(`${Directories.pictures}/wallpapers`) ]
|
||||
|
||||
// Supported image extensions. Videos are intentionally excluded for now
|
||||
// to keep the overview lightweight.
|
||||
readonly property list<string> extensions: [
|
||||
"jpg", "jpeg", "png", "webp", "avif", "bmp"
|
||||
]
|
||||
|
||||
// Resulting list of absolute file paths (without file:// prefix)
|
||||
property list<string> 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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {} }
|
||||
|
||||
Reference in New Issue
Block a user