diff options
Diffstat (limited to 'components/filedialog')
| -rw-r--r-- | components/filedialog/CurrentItem.qml | 107 | ||||
| -rw-r--r-- | components/filedialog/DialogButtons.qml | 93 | ||||
| -rw-r--r-- | components/filedialog/FileDialog.qml | 106 | ||||
| -rw-r--r-- | components/filedialog/FolderContents.qml | 224 | ||||
| -rw-r--r-- | components/filedialog/HeaderBar.qml | 142 | ||||
| -rw-r--r-- | components/filedialog/Sidebar.qml | 117 | ||||
| -rw-r--r-- | components/filedialog/Sizes.qml | 8 |
7 files changed, 797 insertions, 0 deletions
diff --git a/components/filedialog/CurrentItem.qml b/components/filedialog/CurrentItem.qml new file mode 100644 index 0000000..e042445 --- /dev/null +++ b/components/filedialog/CurrentItem.qml @@ -0,0 +1,107 @@ +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Shapes + +Item { + id: root + + required property var currentItem + + implicitWidth: content.implicitWidth + Appearance.padding.larger + content.anchors.rightMargin + implicitHeight: currentItem ? content.implicitHeight + Appearance.padding.normal + content.anchors.bottomMargin : 0 + + Shape { + preferredRendererType: Shape.CurveRenderer + + ShapePath { + id: path + + readonly property real rounding: Appearance.rounding.small + readonly property bool flatten: root.implicitHeight < rounding * 2 + readonly property real roundingY: flatten ? root.implicitHeight / 2 : rounding + + strokeWidth: -1 + fillColor: Colours.palette.m3surfaceContainer + + startX: root.implicitWidth + startY: root.implicitHeight + + PathLine { + relativeX: -(root.implicitWidth + path.rounding) + relativeY: 0 + } + PathArc { + relativeX: path.rounding + relativeY: -path.roundingY + radiusX: path.rounding + radiusY: Math.min(path.rounding, root.implicitHeight) + direction: PathArc.Counterclockwise + } + PathLine { + relativeX: 0 + relativeY: -(root.implicitHeight - path.roundingY * 2) + } + PathArc { + relativeX: path.rounding + relativeY: -path.roundingY + radiusX: path.rounding + radiusY: Math.min(path.rounding, root.implicitHeight) + } + PathLine { + relativeX: root.implicitHeight > 0 ? root.implicitWidth - path.rounding * 2 : root.implicitWidth + relativeY: 0 + } + PathArc { + relativeX: path.rounding + relativeY: -path.rounding + radiusX: path.rounding + radiusY: path.rounding + direction: PathArc.Counterclockwise + } + + Behavior on fillColor { + ColorAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } + } + + Item { + anchors.fill: parent + clip: true + + StyledText { + id: content + + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.rightMargin: Appearance.padding.larger - Appearance.padding.small + anchors.bottomMargin: Appearance.padding.normal - Appearance.padding.small + + text: qsTr(`"%1" selected`).arg(root.currentItem?.fileName) + } + } + + Behavior on implicitWidth { + enabled: !!root.currentItem + + NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + Behavior on implicitHeight { + NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } +} diff --git a/components/filedialog/DialogButtons.qml b/components/filedialog/DialogButtons.qml new file mode 100644 index 0000000..a64195a --- /dev/null +++ b/components/filedialog/DialogButtons.qml @@ -0,0 +1,93 @@ +import ".." +import qs.services +import qs.config +import QtQuick.Layouts + +StyledRect { + id: root + + required property var dialog + required property FolderContents folder + + implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 + + color: Colours.palette.m3surfaceContainer + + RowLayout { + id: inner + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Filter:") + } + + StyledRect { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.rightMargin: Appearance.spacing.normal + + color: Colours.palette.m3surfaceContainerHigh + radius: Appearance.rounding.small + + StyledText { + anchors.fill: parent + anchors.margins: Appearance.padding.normal + + text: `${root.dialog.filterLabel} (${root.dialog.filters.map(f => `*.${f}`).join(", ")})` + } + } + + StyledRect { + color: Colours.palette.m3surfaceContainerHigh + radius: Appearance.rounding.small + + implicitWidth: cancelText.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: cancelText.implicitHeight + Appearance.padding.normal * 2 + + StateLayer { + disabled: !root.dialog.selectionValid + + function onClicked(): void { + root.dialog.accepted(root.folder.currentItem.filePath); + } + } + + StyledText { + id: selectText + + anchors.centerIn: parent + anchors.margins: Appearance.padding.normal + + text: qsTr("Select") + color: root.dialog.selectionValid ? Colours.palette.m3onSurface : Colours.palette.m3outline + } + } + + StyledRect { + color: Colours.palette.m3surfaceContainerHigh + radius: Appearance.rounding.small + + implicitWidth: cancelText.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: cancelText.implicitHeight + Appearance.padding.normal * 2 + + StateLayer { + function onClicked(): void { + root.dialog.rejected(); + } + } + + StyledText { + id: cancelText + + anchors.centerIn: parent + anchors.margins: Appearance.padding.normal + + text: qsTr("Cancel") + } + } + } +} diff --git a/components/filedialog/FileDialog.qml b/components/filedialog/FileDialog.qml new file mode 100644 index 0000000..a533243 --- /dev/null +++ b/components/filedialog/FileDialog.qml @@ -0,0 +1,106 @@ +pragma ComponentBehavior: Bound + +import qs.services +import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts + +LazyLoader { + id: loader + + property list<string> cwd: ["Home"] + property string filterLabel: "All files" + property list<string> filters: ["*"] + property string title: qsTr("Select a file") + + signal accepted(path: string) + signal rejected + + function open(): void { + activeAsync = true; + } + + function close(): void { + rejected(); + } + + onAccepted: activeAsync = false + onRejected: activeAsync = false + + FloatingWindow { + id: root + + property list<string> cwd: loader.cwd + property string filterLabel: loader.filterLabel + property list<string> filters: loader.filters + + readonly property bool selectionValid: { + const item = folderContents.currentItem; + return item && !item.fileIsDir && (filters.includes("*") || filters.includes(item.fileSuffix)); + } + + function accepted(path: string): void { + loader.accepted(path); + } + + function rejected(): void { + loader.rejected(); + } + + implicitWidth: 1000 + implicitHeight: 600 + color: Colours.palette.m3surface + title: loader.title + + onVisibleChanged: { + if (!visible) + rejected(); + } + + RowLayout { + anchors.fill: parent + + spacing: 0 + + Sidebar { + Layout.fillHeight: true + dialog: root + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + spacing: 0 + + HeaderBar { + Layout.fillWidth: true + dialog: root + } + + FolderContents { + id: folderContents + + Layout.fillWidth: true + Layout.fillHeight: true + dialog: root + } + + DialogButtons { + Layout.fillWidth: true + dialog: root + folder: folderContents + } + } + } + + Behavior on color { + ColorAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } +} diff --git a/components/filedialog/FolderContents.qml b/components/filedialog/FolderContents.qml new file mode 100644 index 0000000..45930ba --- /dev/null +++ b/components/filedialog/FolderContents.qml @@ -0,0 +1,224 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../controls" +import "../images" +import qs.services +import qs.config +import qs.utils +import Quickshell +import Quickshell.Io +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects +import QtQuick.Controls +import Qt.labs.folderlistmodel + +Item { + id: root + + required property var dialog + property alias currentItem: view.currentItem + + StyledRect { + anchors.fill: parent + color: Colours.palette.m3surfaceContainer + + layer.enabled: true + layer.effect: MultiEffect { + maskSource: mask + maskEnabled: true + maskInverted: true + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + } + + Item { + id: mask + + anchors.fill: parent + layer.enabled: true + visible: false + + Rectangle { + anchors.fill: parent + anchors.margins: Appearance.padding.small + radius: Appearance.rounding.small + } + } + + Loader { + anchors.centerIn: parent + active: view.count === 0 + asynchronous: true + sourceComponent: ColumnLayout { + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "scan_delete" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.extraLarge * 2 + font.weight: 500 + } + + StyledText { + text: qsTr("This folder is empty") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + } + } + + GridView { + id: view + + anchors.fill: parent + anchors.margins: Appearance.padding.small + Appearance.padding.normal + + cellWidth: Sizes.itemWidth + Appearance.spacing.small + cellHeight: Sizes.itemWidth + Appearance.spacing.small * 2 + Appearance.padding.normal * 2 + 1 + + clip: true + focus: true + currentIndex: -1 + Keys.onEscapePressed: currentIndex = -1 + + Keys.onReturnPressed: { + if (root.dialog.selectionValid) + root.dialog.accepted(currentItem.filePath); + } + Keys.onEnterPressed: { + if (root.dialog.selectionValid) + root.dialog.accepted(currentItem.filePath); + } + + ScrollBar.vertical: StyledScrollBar {} + + model: FolderListModel { + showDirsFirst: true + folder: { + let url = "file://"; + if (root.dialog.cwd[0] === "Home") + url += `${Paths.strip(Paths.home)}/${root.dialog.cwd.slice(1).join("/")}`; + else + url += root.dialog.cwd.join("/"); + return url; + } + onFolderChanged: view.currentIndex = -1 + } + + delegate: StyledRect { + id: item + + required property int index + required property string fileName + required property string filePath + required property url fileUrl + required property string fileSuffix + required property bool fileIsDir + + readonly property real nonAnimHeight: icon.implicitHeight + name.anchors.topMargin + name.implicitHeight + Appearance.padding.normal * 2 + + implicitWidth: Sizes.itemWidth + implicitHeight: nonAnimHeight + + radius: Appearance.rounding.normal + color: GridView.isCurrentItem ? Colours.palette.m3surfaceContainerHighest : "transparent" + z: GridView.isCurrentItem || implicitHeight !== nonAnimHeight ? 1 : 0 + clip: true + + StateLayer { + onDoubleClicked: { + if (item.fileIsDir) + root.dialog.cwd.push(item.fileName); + else if (root.dialog.selectionValid) + root.dialog.accepted(item.filePath); + } + + function onClicked(): void { + view.currentIndex = item.index; + } + } + + CachingIconImage { + id: icon + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: Appearance.padding.normal + + asynchronous: true + implicitSize: Sizes.itemWidth - Appearance.padding.normal * 2 + source: { + if (!item.fileIsDir) + return Quickshell.iconPath("application-x-zerosize"); + + const name = item.fileName; + if (root.dialog.cwd.length === 1 && ["Desktop", "Documents", "Downloads", "Music", "Pictures", "Public", "Templates", "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: !item.fileIsDir + command: ["file", "--mime", "-b", item.filePath] + stdout: StdioCollector { + onStreamFinished: { + const mime = text.split(";")[0].replace("/", "-"); + icon.source = Images.validImageTypes.some(t => mime === `image-${t}`) ? item.fileUrl : Quickshell.iconPath(mime, "image-missing"); + } + } + } + } + + StyledText { + id: name + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: icon.bottom + anchors.topMargin: Appearance.spacing.small + anchors.margins: Appearance.padding.normal + + horizontalAlignment: Text.AlignHCenter + text: item.fileName + elide: item.GridView.isCurrentItem ? Text.ElideNone : Text.ElideRight + wrapMode: item.GridView.isCurrentItem ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap + } + + Behavior on implicitHeight { + Anim {} + } + } + + populate: Transition { + Anim { + property: "scale" + from: 0.7 + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + } + } + + CurrentItem { + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Appearance.padding.small + + currentItem: view.currentItem + } + + component Anim: NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } +} diff --git a/components/filedialog/HeaderBar.qml b/components/filedialog/HeaderBar.qml new file mode 100644 index 0000000..4af9672 --- /dev/null +++ b/components/filedialog/HeaderBar.qml @@ -0,0 +1,142 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property var dialog + + implicitWidth: inner.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 + + color: Colours.palette.m3surfaceContainer + + RowLayout { + id: inner + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + spacing: Appearance.spacing.small + + Item { + implicitWidth: implicitHeight + implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 + + StateLayer { + radius: Appearance.rounding.small + disabled: root.dialog.cwd.length === 1 + + function onClicked(): void { + root.dialog.cwd.pop(); + } + } + + MaterialIcon { + id: upIcon + + anchors.centerIn: parent + text: "drive_folder_upload" + color: root.dialog.cwd.length === 1 ? Colours.palette.m3outline : Colours.palette.m3onSurface + grade: 200 + } + } + + StyledRect { + Layout.fillWidth: true + + radius: Appearance.rounding.small + color: Colours.palette.m3surfaceContainerHigh + + implicitHeight: pathComponents.implicitHeight + pathComponents.anchors.margins * 2 + + RowLayout { + id: pathComponents + + anchors.fill: parent + anchors.margins: Appearance.padding.small / 2 + anchors.leftMargin: 0 + + spacing: Appearance.spacing.small + + Repeater { + model: root.dialog.cwd + + RowLayout { + id: folder + + required property string modelData + required property int index + + spacing: 0 + + Loader { + Layout.rightMargin: Appearance.spacing.small + active: folder.index > 0 + asynchronous: true + sourceComponent: StyledText { + text: "/" + color: Colours.palette.m3onSurfaceVariant + font.bold: true + } + } + + Item { + implicitWidth: homeIcon.implicitWidth + (homeIcon.active ? Appearance.padding.small : 0) + folderName.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: folderName.implicitHeight + Appearance.padding.small * 2 + + Loader { + anchors.fill: parent + active: folder.index < root.dialog.cwd.length - 1 + asynchronous: true + sourceComponent: StateLayer { + radius: Appearance.rounding.small + + function onClicked(): void { + root.dialog.cwd = root.dialog.cwd.slice(0, folder.index + 1); + } + } + } + + Loader { + id: homeIcon + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Appearance.padding.normal + + active: folder.index === 0 && folder.modelData === "Home" + asynchronous: true + sourceComponent: MaterialIcon { + text: "home" + color: root.dialog.cwd.length === 1 ? Colours.palette.m3onSurface : Colours.palette.m3onSurfaceVariant + fill: 1 + } + } + + StyledText { + id: folderName + + anchors.left: homeIcon.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: homeIcon.active ? Appearance.padding.small : 0 + + text: folder.modelData + color: folder.index < root.dialog.cwd.length - 1 ? Colours.palette.m3onSurfaceVariant : Colours.palette.m3onSurface + font.bold: true + } + } + } + } + + Item { + Layout.fillWidth: true + } + } + } + } +} diff --git a/components/filedialog/Sidebar.qml b/components/filedialog/Sidebar.qml new file mode 100644 index 0000000..82a1dd5 --- /dev/null +++ b/components/filedialog/Sidebar.qml @@ -0,0 +1,117 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property var dialog + + implicitWidth: Sizes.sidebarWidth + implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 + + color: Colours.palette.m3surfaceContainer + + ColumnLayout { + id: inner + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Appearance.padding.normal + spacing: Appearance.spacing.small / 2 + + StyledText { + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Appearance.padding.small / 2 + Layout.bottomMargin: Appearance.spacing.normal + text: qsTr("Files") + color: Colours.palette.m3onSurface + font.pointSize: Appearance.font.size.larger + font.bold: true + } + + Repeater { + model: ["Home", "Downloads", "Desktop", "Documents", "Music", "Pictures", "Videos"] + + StyledRect { + id: place + + required property string modelData + readonly property bool selected: modelData === root.dialog.cwd[root.dialog.cwd.length - 1] + + Layout.fillWidth: true + implicitHeight: placeInner.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.full + color: selected ? Colours.palette.m3secondaryContainer : "transparent" + + StateLayer { + color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + + function onClicked(): void { + if (place.modelData === "Home") + root.dialog.cwd = ["Home"]; + else + root.dialog.cwd = ["Home", place.modelData]; + } + } + + RowLayout { + id: placeInner + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + anchors.leftMargin: Appearance.padding.large + anchors.rightMargin: Appearance.padding.large + + spacing: Appearance.spacing.normal + + MaterialIcon { + text: { + const p = place.modelData; + if (p === "Home") + return "home"; + if (p === "Downloads") + return "file_download"; + if (p === "Desktop") + return "desktop_windows"; + if (p === "Documents") + return "description"; + if (p === "Music") + return "music_note"; + if (p === "Pictures") + return "image"; + if (p === "Videos") + return "video_library"; + return "folder"; + } + color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + font.pointSize: Appearance.font.size.large + fill: place.selected ? 1 : 0 + + Behavior on fill { + NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } + + StyledText { + Layout.fillWidth: true + text: place.modelData + color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + font.pointSize: Appearance.font.size.normal + elide: Text.ElideRight + } + } + } + } + } +} diff --git a/components/filedialog/Sizes.qml b/components/filedialog/Sizes.qml new file mode 100644 index 0000000..2ad31f9 --- /dev/null +++ b/components/filedialog/Sizes.qml @@ -0,0 +1,8 @@ +pragma Singleton + +import Quickshell + +Singleton { + property int itemWidth: 103 + property int sidebarWidth: 200 +} |