aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/assistant/Picker.qml
blob: 1465d1ba5312681167e5e2e23937a324a2da9099 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
// 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 }
        }
    }
}