From abf4bd8e6b361efdef8fae264d9a775e74ec8dc2 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 20 Jul 2025 14:10:07 +1000 Subject: filedialog: add nav rail --- widgets/filedialog/FileDialog.qml | 9 +- widgets/filedialog/FolderContents.qml | 198 ++++++++++++++++++++-------------- widgets/filedialog/Sidebar.qml | 120 +++++++++++++++++++++ widgets/filedialog/Sizes.qml | 1 + 4 files changed, 245 insertions(+), 83 deletions(-) create mode 100644 widgets/filedialog/Sidebar.qml (limited to 'widgets/filedialog') diff --git a/widgets/filedialog/FileDialog.qml b/widgets/filedialog/FileDialog.qml index a709122..53f1b57 100644 --- a/widgets/filedialog/FileDialog.qml +++ b/widgets/filedialog/FileDialog.qml @@ -18,13 +18,18 @@ FloatingWindow { RowLayout { anchors.fill: parent - spacing: Appearance.spacing.normal + spacing: 0 + + Sidebar { + Layout.fillHeight: true + dialog: root + } ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - spacing: Appearance.spacing.small + spacing: 0 HeaderBar { Layout.fillWidth: true diff --git a/widgets/filedialog/FolderContents.qml b/widgets/filedialog/FolderContents.qml index 70ac11f..b5d2c5a 100644 --- a/widgets/filedialog/FolderContents.qml +++ b/widgets/filedialog/FolderContents.qml @@ -8,117 +8,153 @@ import Quickshell import Quickshell.Io import Quickshell.Widgets import QtQuick +import QtQuick.Effects import Qt.labs.folderlistmodel -GridView { +Item { id: root required property var dialog - 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: root.currentIndex = -1 - - 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; + 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 } - onFolderChanged: root.currentIndex = -1 } - delegate: StyledRect { - id: item + Item { + id: mask - required property int index - required property string fileName - required property string filePath - required property url fileUrl - required property string fileSuffix - required property bool fileIsDir + anchors.fill: parent + layer.enabled: true + visible: false - readonly property real nonAnimHeight: icon.implicitHeight + name.anchors.topMargin + name.implicitHeight + Appearance.padding.normal * 2 + Rectangle { + anchors.fill: parent + anchors.margins: Appearance.padding.small + radius: Appearance.rounding.small + } + } - implicitWidth: Sizes.itemWidth - implicitHeight: nonAnimHeight + GridView { + id: view - radius: Appearance.rounding.normal - color: root.currentItem === item ? Colours.palette.m3primary : "transparent" - z: root.currentItem === item || implicitHeight !== nonAnimHeight ? 1 : 0 - clip: true + anchors.fill: parent + anchors.margins: Appearance.padding.small + Appearance.padding.normal - StateLayer { - color: root.currentItem === item ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + cellWidth: Sizes.itemWidth + Appearance.spacing.small + cellHeight: Sizes.itemWidth + Appearance.spacing.small * 2 + Appearance.padding.normal * 2 + 1 - onDoubleClicked: { - if (item.fileIsDir) - root.dialog.cwd.push(item.fileName); + clip: true + focus: true + currentIndex: -1 + Keys.onEscapePressed: view.currentIndex = -1 + + 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 - root.dialog.accepted(item.filePath); - } - - function onClicked(): void { - root.currentIndex = item.index; + url += root.dialog.cwd.join("/"); + return url; } + onFolderChanged: view.currentIndex = -1 } - IconImage { - id: icon + delegate: StyledRect { + id: item - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: Appearance.padding.normal + required property int index + required property string fileName + required property string filePath + required property url fileUrl + required property string fileSuffix + required property bool fileIsDir - asynchronous: true - implicitSize: Sizes.itemWidth - Appearance.padding.normal * 2 - source: Quickshell.iconPath(item.fileIsDir ? "inode-directory" : "application-x-zerosize") - onStatusChanged: { - if (status === Image.Error) - source = Quickshell.iconPath("error"); + 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: view.currentItem === item ? Colours.palette.m3primary : "transparent" + z: view.currentItem === item || implicitHeight !== nonAnimHeight ? 1 : 0 + clip: true + + StateLayer { + color: view.currentItem === item ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + + onDoubleClicked: { + if (item.fileIsDir) + root.dialog.cwd.push(item.fileName); + else + root.dialog.accepted(item.filePath); + } + + function onClicked(): void { + view.currentIndex = item.index; + } } - Process { - running: !item.fileIsDir - command: ["file", "--mime", "-b", item.filePath] - stdout: StdioCollector { - onStreamFinished: { - const mime = text.split(";")[0].replace("/", "-"); - icon.source = mime.startsWith("image-") ? item.fileUrl : Quickshell.iconPath(mime, "image-missing"); + IconImage { + 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: Quickshell.iconPath(item.fileIsDir ? "inode-directory" : "application-x-zerosize") + 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 = mime.startsWith("image-") ? item.fileUrl : Quickshell.iconPath(mime, "image-missing"); + } } } } - } - StyledText { - id: name + 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 + 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 - color: root.currentItem === item ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - elide: root.currentItem === item ? Text.ElideNone : Text.ElideRight - wrapMode: root.currentItem === item ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap - } + horizontalAlignment: Text.AlignHCenter + text: item.fileName + color: view.currentItem === item ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + elide: view.currentItem === item ? Text.ElideNone : Text.ElideRight + wrapMode: view.currentItem === item ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap + } - Behavior on implicitHeight { - 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/widgets/filedialog/Sidebar.qml b/widgets/filedialog/Sidebar.qml new file mode 100644 index 0000000..f0bcc58 --- /dev/null +++ b/widgets/filedialog/Sidebar.qml @@ -0,0 +1,120 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.services +import qs.config +import Quickshell +import Quickshell.Widgets +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] + + anchors.left: parent.left + anchors.right: parent.right + 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/widgets/filedialog/Sizes.qml b/widgets/filedialog/Sizes.qml index d332ab3..c8cacba 100644 --- a/widgets/filedialog/Sizes.qml +++ b/widgets/filedialog/Sizes.qml @@ -4,4 +4,5 @@ import Quickshell Singleton { property int itemWidth: 100 + property int sidebarWidth: 200 } -- cgit v1.2.3-freya