// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. import Quickshell import QtQuick import QtQuick.Controls import Qt.labs.folderlistmodel import ".." // A file picker drawn in the card. A system dialog cannot be used: the card // is an overlay layer surface holding the keyboard, so a dialog window would // open beneath it, out of reach. Click a file to stage it and close, // Ctrl+click to stage and stay; the filter narrows this directory. Rectangle { id: picker property var canvas readonly property string home: Quickshell.env("HOME") || "/" property string dir: home color: Qt.alpha(Theme.base, 0.97) radius: 8 onVisibleChanged: if (visible) { dir = canvas.pickDir || home; field.text = ""; field.forceActiveFocus(); } MouseArea { anchors.fill: parent } // keep clicks off what is beneath function enter(path) { dir = path; field.text = ""; rows.currentIndex = 0; } function up() { if (dir !== "/") enter(dir.slice(0, dir.lastIndexOf("/")) || "/"); } function choose(i, keep) { const isDir = files.get(i, "fileIsDir"), path = files.get(i, "filePath"); if (isDir) { enter(path); return; } canvas.pickDir = dir; canvas.stage(path); if (!keep) canvas.pickerOpen = false; } FolderListModel { id: files folder: "file://" + picker.dir showDirsFirst: true showDotAndDotDot: false // Hidden entries only on asking for them. showHidden: field.text.startsWith(".") caseSensitive: false nameFilters: field.text ? ["*" + field.text + "*"] : ["*"] sortCaseSensitive: false } Row { id: bar anchors { top: parent.top; left: parent.left; right: parent.right } spacing: 6 Button { text: "Home"; onClicked: picker.enter(picker.home) } Button { text: "Up"; enabled: picker.dir !== "/"; onClicked: picker.up() } Text { anchors.verticalCenter: parent.verticalCenter width: bar.width - x text: picker.dir.startsWith(picker.home) ? "~" + picker.dir.slice(picker.home.length) : picker.dir elide: Text.ElideLeft color: Theme.subtext font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } } } TextField { id: field anchors { top: bar.bottom; topMargin: 8; left: parent.left; right: parent.right } height: 32 placeholderText: "Filter" color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } background: Rectangle { radius: 6 color: Qt.alpha(Theme.surface, 0.9) border.width: 1 border.color: Qt.alpha(Theme.text, 0.15) } onTextChanged: rows.currentIndex = 0 Keys.onUpPressed: rows.decrementCurrentIndex() Keys.onDownPressed: rows.incrementCurrentIndex() Keys.onReturnPressed: event => { if (rows.count) picker.choose(rows.currentIndex, event.modifiers & Qt.ControlModifier); } Keys.onEnterPressed: event => { if (rows.count) picker.choose(rows.currentIndex, event.modifiers & Qt.ControlModifier); } } ListView { id: rows anchors { top: field.bottom; topMargin: 8; left: parent.left; right: parent.right; bottom: parent.bottom } clip: true spacing: 2 boundsBehavior: Flickable.StopAtBounds model: files highlightMoveDuration: 0 delegate: Rectangle { id: r required property int index required property string fileName required property bool fileIsDir readonly property bool staged: picker.canvas.staged.some(f => f.path === picker.dir.replace(/\/$/, "") + "/" + r.fileName) width: rows.width height: name.implicitHeight + 10 radius: 6 color: rows.currentIndex === r.index ? Qt.alpha(Theme.accent, 0.18) : hover.hovered ? Qt.alpha(Theme.surface, 0.9) : "transparent" HoverHandler { id: hover } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: mouse => { rows.currentIndex = r.index; picker.choose(r.index, mouse.modifiers & Qt.ControlModifier); } } Text { id: name anchors { left: parent.left; leftMargin: 8; right: parent.right; rightMargin: 8; verticalCenter: parent.verticalCenter } text: (r.fileIsDir ? r.fileName + "/" : r.fileName) + (r.staged ? " ✓" : "") elide: Text.ElideMiddle color: r.fileIsDir ? Theme.accent : Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } } } Text { anchors.centerIn: parent visible: rows.count === 0 text: files.status === FolderListModel.Loading ? "Loading…" : "Nothing here" color: Theme.overlay font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } } } }