dots-hyprland/.config/quickshell/ii/modules/wallpaperSelector/WallpaperSelectorContent.qml
2025-08-23 21:06:36 +07:00

440 lines
22 KiB
QML

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 real previewCellAspectRatio: 4 / 3
implicitHeight: columnLayout.implicitHeight
implicitWidth: columnLayout.implicitWidth
property var wallpapers: 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
anchors.fill: parent
spacing: -Appearance.sizes.elevationMargin
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
ColumnLayout {
// The grid
anchors.fill: parent
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
}
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() {
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: 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);
}
}
}
}
Item {
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
}
Rectangle { // Bottom toolbar
id: extraOptionsBackground
property real padding: 6
anchors {
fill: parent
margins: 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"
}
}
}
}
}
}
}
}
}
}
Connections {
target: GlobalStates
function onWallpaperSelectorOpenChanged() {
if (GlobalStates.wallpaperSelectorOpen && monitorIsFocused) {
filterField.forceActiveFocus();
}
}
}
}