summaryrefslogtreecommitdiff
path: root/components/filedialog
diff options
context:
space:
mode:
Diffstat (limited to 'components/filedialog')
-rw-r--r--components/filedialog/CurrentItem.qml107
-rw-r--r--components/filedialog/DialogButtons.qml93
-rw-r--r--components/filedialog/FileDialog.qml106
-rw-r--r--components/filedialog/FolderContents.qml224
-rw-r--r--components/filedialog/HeaderBar.qml142
-rw-r--r--components/filedialog/Sidebar.qml117
-rw-r--r--components/filedialog/Sizes.qml8
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
+}