FilePicker: back to our custom file picker.

This commit is contained in:
ItsLemmy
2025-09-25 20:59:50 -04:00
parent cb3fc1a45c
commit 22b843587c
9 changed files with 864 additions and 195 deletions
+3 -1
View File
@@ -467,7 +467,9 @@ Singleton {
if (!gotControlCenter) {
//const obj = JSON.parse('{"id": "ControlCenter"}');
adapter.bar.widgets["right"].push(({"id": "ControlCenter"}))
adapter.bar.widgets["right"].push(({
"id": "ControlCenter"
}))
Logger.warn("Settings", "Added a ControlCenter widget to the right section")
}
}
+26 -1
View File
@@ -122,7 +122,32 @@ Singleton {
"bt-device-watch": "device-watch",
"bt-device-speaker": "device-speaker",
"bt-device-tv": "device-tv",
"noctalia": "noctalia"
"noctalia": "noctalia",
"hyprland": "hyprland",
"filepicker-folder": "folder",
"filepicker-refresh": "refresh",
"filepicker-close": "x",
"filepicker-arrow-left": "arrow-left",
"filepicker-arrow-up": "arrow-up",
"filepicker-home": "home",
"filepicker-layout-grid": "layout-grid",
"filepicker-list": "list",
"filepicker-search": "search",
"filepicker-x": "x",
"filepicker-photo": "photo",
"filepicker-check": "check",
"filepicker-file-text": "file-text",
"filepicker-video": "video",
"filepicker-music": "music",
"filepicker-archive": "archive",
"filepicker-table": "table",
"filepicker-presentation": "presentation",
"filepicker-code": "code",
"filepicker-settings": "settings",
"filepicker-file": "file",
"filepicker-text": "file-text",
"filepicker-eye": "eye",
"filepicker-eye-off": "eye-off"
}
// Fonts Codepoints - do not change!
@@ -15,8 +15,6 @@ Popup {
property var widgetData: null
property string widgetId: ""
property bool isMasked: false
// Center popup in parent
x: (parent.width - width) * 0.5
y: (parent.height - height) * 0.5
@@ -43,7 +41,6 @@ Popup {
background: Rectangle {
id: bgRect
opacity: widgetSettings.isMasked ? 0 : 1.0
color: Color.mSurface
radius: Style.radiusL * scaling
border.color: Color.mPrimary
@@ -53,7 +50,6 @@ Popup {
contentItem: ColumnLayout {
id: content
opacity: widgetSettings.isMasked ? 0 : 1.0
width: parent.width
spacing: Style.marginM * scaling
@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Widgets
import qs.Services
@@ -73,7 +74,7 @@ ColumnLayout {
NButton {
enabled: !valueUseDistroLogo
text: I18n.tr("bar.widget-settings.control-center.browse-file")
onClicked: filePicker.open()
onClicked: imagePicker.openFilePicker()
}
}
@@ -87,8 +88,16 @@ ColumnLayout {
}
NFilePicker {
id: filePicker
id: imagePicker
title: I18n.tr("bar.widget-settings.control-center.select-custom-icon")
onAccepted: paths => valueCustomIconPath = paths[0]
selectFiles: true
selectFolders: false
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"]
initialPath: Quickshell.env("HOME")
onAccepted: paths => {
if (paths.length > 0) {
valueCustomIconPath = paths[0] // Use first selected file
}
}
}
}
+10 -5
View File
@@ -41,18 +41,23 @@ ColumnLayout {
buttonTooltip: "Browse for avatar image"
onInputEditingFinished: Settings.data.general.avatarImage = text
onButtonClicked: {
filePicker.open()
avatarPicker.openFilePicker()
}
}
}
NFilePicker {
id: filePicker
pickerType: "file"
id: avatarPicker
title: I18n.tr("settings.general.profile.select-avatar")
selectFiles: true
selectFolders: true
initialPath: Settings.data.general.avatarImage.substr(0, Settings.data.general.avatarImage.lastIndexOf("/")) || Quickshell.env("HOME")
nameFilters: ["Image files (*.jpg *.jpeg *.png *.gif *.pnm *.bmp *.face)", "All files (*)"]
onAccepted: paths => Settings.data.general.avatarImage = paths[0]
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.pnm", "*.bmp"]
onAccepted: paths => {
if (paths.length > 0) {
Settings.data.general.avatarImage = paths[0]
}
}
}
NDivider {
+8 -3
View File
@@ -29,7 +29,7 @@ ColumnLayout {
buttonIcon: "folder-open"
buttonTooltip: I18n.tr("settings.screen-recorder.general.output-folder.tooltip")
onInputEditingFinished: Settings.data.screenRecorder.directory = text
onButtonClicked: folderPicker.open()
onButtonClicked: folderPicker.openFilePicker()
}
// Show Cursor
@@ -229,9 +229,14 @@ ColumnLayout {
NFilePicker {
id: folderPicker
pickerType: "folder"
selectFiles: false
selectFolders: true
title: I18n.tr("settings.screen-recorder.general.select-output-folder")
initialPath: Settings.data.screenRecorder.directory || Quickshell.env("HOME") + "/Videos"
onAccepted: paths => Settings.data.screenRecorder.directory = paths[0]
onAccepted: paths => {
if (paths.length > 0) {
Settings.data.screenRecorder.directory = paths[0] // Use first selected file
}
}
}
}
+16 -4
View File
@@ -340,15 +340,27 @@ ColumnLayout {
NFilePicker {
id: mainFolderPicker
pickerType: "folder"
selectFiles: false
selectFolders: true
title: I18n.tr("settings.wallpaper.settings.select-folder")
onAccepted: paths => Settings.data.wallpaper.directory = paths[0]
initialPath: Settings.data.wallpaper.directory || Quickshell.env("HOME") + "/Pictures"
onAccepted: paths => {
if (paths.length > 0) {
Settings.data.wallpaper.directory = paths[0]
}
}
}
NFilePicker {
id: monitorFolderPicker
pickerType: "folder"
selectFiles: false
selectFolders: true
title: I18n.tr("settings.wallpaper.settings.select-monitor-folder")
onAccepted: paths => WallpaperService.setMonitorDirectory(specificFolderMonitorName, paths[0])
initialPath: WallpaperService.getMonitorDirectory(specificFolderMonitorName) || Quickshell.env("HOME") + "/Pictures"
onAccepted: paths => {
if (paths.length > 0) {
WallpaperService.setMonitorDirectory(specificFolderMonitorName, paths[0])
}
}
}
}
+1
View File
@@ -63,6 +63,7 @@ Singleton {
}
}
// TODO Translate
readonly property ListModel fillModeModel: ListModel {
// Centers image without resizing
// Pads with fillColor if image is smaller than screen
+788 -174
View File
@@ -1,206 +1,820 @@
import QtCore
import QtQuick
import QtQuick.Dialogs
import QtQuick.Layouts
import QtQuick.Controls
import Qt.labs.folderlistmodel
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
import qs.Widgets
import "../Helpers/FuzzySort.js" as FuzzySort
Item {
Popup {
id: root
// Public API Properties
property string initialPath: ""
// Properties
property string title: "File Picker"
property string initialPath: Quickshell.env("HOME") || "/home"
property bool selectFiles: true
property bool selectFolders: true
property var nameFilters: ["*"]
property bool showDirs: true
property bool showHiddenFiles: false
property real scaling: 1.0
property var selectedPaths: []
property string selectedPath: ""
property bool multipleSelection: false
property string pickerType: "file" // "file" or "folder"
property var nameFilters: ["All files (*)"] // e.g., ["Image files (*.png *.jpg)", "Text files (*.txt)"]
property string title: pickerType === "folder" ? I18n.tr("widgets.file-picker.select-folder") : I18n.tr("widgets.file-picker.select-file")
property string acceptLabel: I18n.tr("placeholders.select")
property string rejectLabel: I18n.tr("placeholders.cancel")
property string currentPath: initialPath
property bool shouldResetSelection: false
// State properties
property bool isOpen: false
// Signals for external connections
// Signals
signal accepted(var paths)
signal rejected
signal pathSelected(string path)
signal pathsSelected(var paths)
signal beforeOpen
signal afterClose
signal cancelled
// Public functions
function open() {
beforeOpen()
if (PanelService.openedPanel !== null) {
PanelService.openedPanel.isMasked = true
}
for (var i = 0; i < PanelService.openedPopups.length; i++) {
PanelService.openedPopups[i].isMasked = true
}
isOpen = true
// Small delay to ensure panel changes happen first
Qt.callLater(function () {
if (pickerType === "folder") {
folderDialog.open()
} else {
fileDialog.open()
}
})
onOpened: {
PanelService.willOpenPopup(root)
}
function close() {
if (pickerType === "folder") {
folderDialog.close()
} else {
fileDialog.close()
}
handleClose()
onClosed: {
PanelService.willClosePopup(root)
}
function handleClose() {
isOpen = false
if (PanelService.openedPanel !== null) {
PanelService.openedPanel.isMasked = false
}
for (var i = 0; i < PanelService.openedPopups.length; i++) {
PanelService.openedPopups[i].isMasked = false
}
afterClose()
function openFilePicker() {
if (!root.currentPath)
root.currentPath = root.initialPath
shouldResetSelection = true
open()
}
function reset() {
selectedPaths = []
selectedPath = ""
function getFileIcon(fileName) {
const ext = fileName.split('.').pop().toLowerCase()
const iconMap = {
"txt": 'filepicker-file-text',
"md": 'filepicker-file-text',
"log": 'filepicker-file-text',
"jpg": 'filepicker-photo',
"jpeg": 'filepicker-photo',
"png": 'filepicker-photo',
"gif": 'filepicker-photo',
"bmp": 'filepicker-photo',
"svg": 'filepicker-photo',
"mp4": 'filepicker-video',
"avi": 'filepicker-video',
"mkv": 'filepicker-video',
"mov": 'filepicker-video',
"mp3": 'filepicker-music',
"wav": 'filepicker-music',
"flac": 'filepicker-music',
"ogg": 'filepicker-music',
"zip": 'filepicker-archive',
"tar": 'filepicker-archive',
"gz": 'filepicker-archive',
"rar": 'filepicker-archive',
"7z": 'filepicker-archive',
"pdf": 'filepicker-text',
"doc": 'filepicker-text',
"docx": 'filepicker-text',
"xls": 'filepicker-table',
"xlsx": 'filepicker-table',
"ppt": 'filepicker-presentation',
"pptx": 'filepicker-presentation',
"html": 'filepicker-code',
"htm": 'filepicker-code',
"css": 'filepicker-code',
"js": 'filepicker-code',
"json": 'filepicker-code',
"xml": 'filepicker-code',
"exe": 'filepicker-settings',
"app": 'filepicker-settings',
"deb": 'filepicker-settings',
"rpm": 'filepicker-settings'
}
return iconMap[ext] || 'filepicker-file'
}
// Helper function to set file extensions easily
function setFileExtensions(extensions) {
if (!extensions || extensions.length === 0) {
nameFilters = ["All files (*)"]
function formatFileSize(bytes) {
if (bytes === 0)
return "0 B"
const k = 1024, sizes = ["B", "KB", "MB", "GB", "TB"]
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]
}
function confirmSelection() {
if (filePickerPanel.currentSelection.length === 0)
return
}
var filters = []
for (var i = 0; i < extensions.length; i++) {
var ext = extensions[i]
if (typeof ext === "string") {
// Simple extension like "png"
filters.push(ext.toUpperCase() + " files (*." + ext + ")")
} else if (typeof ext === "object" && ext.label && ext.extensions) {
// Complex filter like {label: "Images", extensions: ["png", "jpg", "jpeg"]}
var filterStr = ext.label + " ("
for (var j = 0; j < ext.extensions.length; j++) {
filterStr += "*." + ext.extensions[j]
if (j < ext.extensions.length - 1)
filterStr += " "
root.selectedPaths = filePickerPanel.currentSelection
root.accepted(filePickerPanel.currentSelection)
root.close()
}
function updateFilteredModel() {
filteredModel.clear()
const searchText = filePickerPanel.filterText.toLowerCase()
for (var i = 0; i < folderModel.count; i++) {
const fileName = folderModel.get(i, "fileName")
const filePath = folderModel.get(i, "filePath")
const fileIsDir = folderModel.get(i, "fileIsDir")
const fileSize = folderModel.get(i, "fileSize")
if (root.selectFolders && !root.selectFiles && !fileIsDir)
continue
if (searchText === "" || fileName.toLowerCase().includes(searchText)) {
filteredModel.append({
"fileName": fileName,
"filePath": filePath,
"fileIsDir": fileIsDir,
"fileSize": fileSize
})
}
}
}
width: 900 * scaling
height: 700 * scaling
modal: true
closePolicy: Popup.CloseOnEscape
anchors.centerIn: Overlay.overlay
background: Rectangle {
color: Color.mSurfaceVariant
radius: Style.radiusL * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
}
Rectangle {
id: filePickerPanel
anchors.fill: parent
anchors.margins: Style.marginL * scaling
color: Color.transparent
property string filterText: ""
property var currentSelection: []
property bool viewMode: true // true = grid, false = list
property string searchText: ""
property bool showSearchBar: false
focus: true
Keys.onPressed: event => {
if (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_F) {
filePickerPanel.showSearchBar = !filePickerPanel.showSearchBar
if (filePickerPanel.showSearchBar)
Qt.callLater(() => searchInput.forceActiveFocus())
event.accepted = true
} else if (event.key === Qt.Key_Escape && filePickerPanel.showSearchBar) {
filePickerPanel.showSearchBar = false
filePickerPanel.searchText = ""
filePickerPanel.filterText = ""
root.updateFilteredModel()
event.accepted = true
}
}
ColumnLayout {
anchors.fill: parent
spacing: Style.marginM * scaling
// Header
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NIcon {
icon: "filepicker-folder"
color: Color.mPrimary
font.pointSize: Style.fontSizeXXL * scaling
}
NText {
text: root.title
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "filepicker-refresh"
tooltipText: "Refresh"
onClicked: folderModel.refresh()
}
NIconButton {
icon: "filepicker-close"
tooltipText: "Close"
onClicked: {
root.cancelled()
root.close()
}
}
}
NDivider {
Layout.fillWidth: true
}
// Navigation toolbar
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 45 * scaling
color: Color.mSurfaceVariant
radius: Style.radiusS * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
spacing: Style.marginS * scaling
NIconButton {
icon: "filepicker-arrow-up"
tooltipText: "Up"
baseSize: Style.baseWidgetSize * 0.8
enabled: folderModel.folder.toString() !== "file:///"
onClicked: {
const parentPath = folderModel.parentFolder.toString().replace("file://", "")
folderModel.folder = "file://" + parentPath
root.currentPath = parentPath
}
}
NIconButton {
icon: "filepicker-home"
tooltipText: "Home"
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
const homePath = Quickshell.env("HOME") || "/home"
folderModel.folder = "file://" + homePath
root.currentPath = homePath
}
}
NIconButton {
icon: filePickerPanel.viewMode ? "filepicker-list" : "filepicker-layout-grid"
tooltipText: filePickerPanel.viewMode ? "List View" : "Grid View"
baseSize: Style.baseWidgetSize * 0.8
onClicked: filePickerPanel.viewMode = !filePickerPanel.viewMode
}
NIconButton {
icon: filePickerPanel.showSearchBar ? "filepicker-x" : "filepicker-search"
tooltipText: filePickerPanel.showSearchBar ? "Close Search" : "Search"
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
filePickerPanel.showSearchBar = !filePickerPanel.showSearchBar
if (!filePickerPanel.showSearchBar) {
filePickerPanel.searchText = ""
filePickerPanel.filterText = ""
root.updateFilteredModel()
}
}
}
NIconButton {
icon: root.showHiddenFiles ? "filepicker-eye-off" : "filepicker-eye"
tooltipText: root.showHiddenFiles ? "Hide Hidden Files" : "Show Hidden Files"
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
root.showHiddenFiles = !root.showHiddenFiles
root.updateFilteredModel()
}
}
NTextInput {
id: locationInput
text: root.currentPath
placeholderText: "Enter path..."
Layout.fillWidth: true
onEditingFinished: {
const newPath = text.trim()
if (newPath !== "" && newPath !== root.currentPath) {
folderModel.folder = "file://" + newPath
root.currentPath = newPath
} else {
text = root.currentPath
}
}
Connections {
target: root
function onCurrentPathChanged() {
if (!locationInput.activeFocus)
locationInput.text = root.currentPath
}
}
}
}
}
// Search bar
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 45 * scaling
color: Color.mSurfaceVariant
radius: Style.radiusS * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
visible: filePickerPanel.showSearchBar
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
spacing: Style.marginS * scaling
NIcon {
icon: "filepicker-search"
color: Color.mOnSurfaceVariant
font.pointSize: Style.fontSizeS * scaling
}
NTextInput {
id: searchInput
placeholderText: "Search files and folders..."
Layout.fillWidth: true
text: filePickerPanel.searchText
onTextChanged: {
filePickerPanel.searchText = text
filePickerPanel.filterText = text
root.updateFilteredModel()
}
Keys.onEscapePressed: {
filePickerPanel.showSearchBar = false
filePickerPanel.searchText = ""
filePickerPanel.filterText = ""
root.updateFilteredModel()
}
}
NIconButton {
icon: "filepicker-x"
tooltipText: "Clear"
baseSize: Style.baseWidgetSize * 0.6
visible: filePickerPanel.searchText.length > 0
onClicked: {
searchInput.text = ""
filePickerPanel.searchText = ""
filePickerPanel.filterText = ""
root.updateFilteredModel()
}
}
}
}
// File list area
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.mSurface
radius: Style.radiusM * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
clip: true
FolderListModel {
id: folderModel
folder: "file://" + root.currentPath
nameFilters: root.nameFilters
showDirs: root.showDirs
showHidden: root.showHiddenFiles
sortField: FolderListModel.Name
sortReversed: false
onFolderChanged: {
root.currentPath = folder.toString().replace("file://", "")
filePickerPanel.currentSelection = []
}
onStatusChanged: {
if (status === FolderListModel.Error) {
if (root.currentPath !== Quickshell.env("HOME")) {
folder = "file://" + Quickshell.env("HOME")
root.currentPath = Quickshell.env("HOME")
}
} else if (status === FolderListModel.Ready) {
root.updateFilteredModel()
}
}
}
ListModel {
id: filteredModel
}
// Common scroll bar component
Component {
id: scrollBarComponent
ScrollBar {
policy: ScrollBar.AsNeeded
contentItem: Rectangle {
implicitWidth: 6 * scaling
implicitHeight: 100
radius: Style.radiusM * scaling
color: parent.pressed ? Qt.alpha(Color.mTertiary, 0.8) : parent.hovered ? Qt.alpha(Color.mTertiary, 0.8) : Qt.alpha(Color.mTertiary, 0.8)
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
background: Rectangle {
implicitWidth: 6 * scaling
implicitHeight: 100
color: Color.transparent
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0.0
radius: (Style.radiusM * scaling) / 2
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
}
}
// Grid view
GridView {
id: gridView
anchors.fill: parent
anchors.margins: Style.marginM * scaling
model: filteredModel
visible: filePickerPanel.viewMode
clip: true
property int columns: Math.max(1, Math.floor(width / (120 * scaling)))
property int itemSize: Math.floor((width - leftMargin - rightMargin - (columns * Style.marginS * scaling)) / columns)
cellWidth: Math.floor((width - leftMargin - rightMargin) / columns)
cellHeight: Math.floor(itemSize * 0.8) + Style.marginXS * scaling + Style.fontSizeS * scaling + Style.marginM * scaling
leftMargin: Style.marginS * scaling
rightMargin: Style.marginS * scaling
topMargin: Style.marginS * scaling
bottomMargin: Style.marginS * scaling
ScrollBar.vertical: scrollBarComponent.createObject(gridView, {
"parent": gridView,
"x": gridView.mirrored ? 0 : gridView.width - width,
"y": 0,
"height": gridView.height
})
delegate: Rectangle {
id: gridItem
width: gridView.itemSize
height: gridView.cellHeight
color: Color.transparent
radius: Style.radiusM * scaling
property bool isSelected: filePickerPanel.currentSelection.includes(model.filePath)
Rectangle {
anchors.fill: parent
color: Color.transparent
radius: parent.radius
border.color: isSelected ? Color.mSecondary : Color.mSurface
border.width: Math.max(1, Style.borderL * scaling)
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
Rectangle {
anchors.fill: parent
color: (mouseArea.containsMouse && !isSelected) ? Color.mTertiary : Color.transparent
radius: parent.radius
border.color: (mouseArea.containsMouse && !isSelected) ? Color.mTertiary : Color.transparent
border.width: Math.max(1, Style.borderS * scaling)
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginXS * scaling
Rectangle {
id: iconContainer
Layout.fillWidth: true
Layout.preferredHeight: Math.round(gridView.itemSize * 0.67)
color: Color.transparent
property bool isImage: {
if (model.fileIsDir)
return false
const ext = model.fileName.split('.').pop().toLowerCase()
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'].includes(ext)
}
Image {
id: thumbnail
anchors.fill: parent
anchors.margins: Style.marginXS * scaling
source: iconContainer.isImage ? "file://" + model.filePath : ""
fillMode: Image.PreserveAspectFit
visible: iconContainer.isImage && status === Image.Ready
smooth: false
cache: true
asynchronous: true
sourceSize.width: 120 * scaling
sourceSize.height: 120 * scaling
onStatusChanged: {
if (status === Image.Error)
visible = false
}
Rectangle {
anchors.fill: parent
color: Color.mSurfaceVariant
radius: Style.radiusS * scaling
visible: thumbnail.status === Image.Loading
NIcon {
icon: "filepicker-photo"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
anchors.centerIn: parent
}
}
}
NIcon {
icon: model.fileIsDir ? "filepicker-folder" : root.getFileIcon(model.fileName)
font.pointSize: Style.fontSizeXXL * 2 * scaling
color: {
if (isSelected)
return Color.mSecondary
else if (mouseArea.containsMouse)
return model.fileIsDir ? Color.mOnTertiary : Color.mOnTertiary
else
return model.fileIsDir ? Color.mPrimary : Color.mOnSurfaceVariant
}
anchors.centerIn: parent
visible: !iconContainer.isImage || thumbnail.status !== Image.Ready
}
Rectangle {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Style.marginS * scaling
width: 24 * scaling
height: 24 * scaling
radius: width / 2
color: Color.mSecondary
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
visible: isSelected
NIcon {
icon: "filepicker-check"
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSecondary
anchors.centerIn: parent
}
}
}
NText {
text: model.fileName
color: {
if (isSelected)
return Color.mSecondary
else if (mouseArea.containsMouse)
return Color.mOnTertiary
else
return Color.mOnSurface
}
font.pointSize: Style.fontSizeS * scaling
font.weight: isSelected ? Style.fontWeightBold : Style.fontWeightRegular
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
elide: Text.ElideRight
maximumLineCount: 2
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (model.fileIsDir) {
if (root.selectFolders && !root.selectFiles) {
filePickerPanel.currentSelection = [model.filePath]
} else {
folderModel.folder = "file://" + model.filePath
root.currentPath = model.filePath
}
} else {
if (root.selectFiles)
filePickerPanel.currentSelection = [model.filePath]
}
}
}
onDoubleClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (model.fileIsDir) {
if (root.selectFolders && !root.selectFiles) {
filePickerPanel.currentSelection = [model.filePath]
root.confirmSelection()
} else {
folderModel.folder = "file://" + model.filePath
root.currentPath = model.filePath
}
} else {
if (root.selectFiles) {
filePickerPanel.currentSelection = [model.filePath]
root.confirmSelection()
}
}
}
}
}
}
}
// List view
NListView {
id: listView
anchors.fill: parent
anchors.margins: Style.marginS * scaling
model: filteredModel
visible: !filePickerPanel.viewMode
clip: true
delegate: Rectangle {
id: listItem
width: listView.width
height: 40 * scaling
color: {
if (filePickerPanel.currentSelection.includes(model.filePath))
return Color.mSecondary
if (mouseArea.containsMouse)
return Color.mTertiary
return Color.transparent
}
radius: Style.radiusS * scaling
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: Style.marginM * scaling
anchors.rightMargin: Style.marginM * scaling
spacing: Style.marginM * scaling
NIcon {
icon: model.fileIsDir ? "filepicker-folder" : root.getFileIcon(model.fileName)
font.pointSize: Style.fontSizeL * scaling
color: model.fileIsDir ? (filePickerPanel.currentSelection.includes(model.filePath) ? Color.mOnSecondary : Color.mPrimary) : Color.mOnSurfaceVariant
}
NText {
text: model.fileName
color: filePickerPanel.currentSelection.includes(model.filePath) ? Color.mOnSecondary : Color.mOnSurface
font.pointSize: Style.fontSizeM * scaling
font.weight: filePickerPanel.currentSelection.includes(model.filePath) ? Style.fontWeightBold : Style.fontWeightRegular
Layout.fillWidth: true
elide: Text.ElideRight
}
NText {
text: model.fileIsDir ? "" : root.formatFileSize(model.fileSize)
color: filePickerPanel.currentSelection.includes(model.filePath) ? Color.mOnSecondary : Color.mOnSurfaceVariant
font.pointSize: Style.fontSizeS * scaling
visible: !model.fileIsDir
Layout.preferredWidth: implicitWidth
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (model.fileIsDir) {
if (root.selectFolders && !root.selectFiles) {
filePickerPanel.currentSelection = [model.filePath]
} else {
folderModel.folder = "file://" + model.filePath
root.currentPath = model.filePath
}
} else {
if (root.selectFiles)
filePickerPanel.currentSelection = [model.filePath]
}
}
}
onDoubleClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (model.fileIsDir) {
if (root.selectFolders && !root.selectFiles) {
filePickerPanel.currentSelection = [model.filePath]
root.confirmSelection()
} else {
folderModel.folder = "file://" + model.filePath
root.currentPath = model.filePath
}
} else {
if (root.selectFiles) {
filePickerPanel.currentSelection = [model.filePath]
root.confirmSelection()
}
}
}
}
}
}
}
}
// Footer
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: {
if (filePickerPanel.searchText.length > 0) {
return "Searching for: \"" + filePickerPanel.searchText + "\" (" + filteredModel.count + " matches)"
} else if (filePickerPanel.currentSelection.length > 0) {
return filePickerPanel.currentSelection.length + " item(s) selected"
} else {
return filteredModel.count + " items"
}
}
color: filePickerPanel.searchText.length > 0 ? Color.mPrimary : Color.mOnSurfaceVariant
font.pointSize: Style.fontSizeS * scaling
Layout.fillWidth: true
}
NButton {
text: "Cancel"
outlined: true
onClicked: {
root.cancelled()
root.close()
}
}
NButton {
text: {
if (root.selectFolders && !root.selectFiles)
return "Select Folder"
else if (root.selectFiles && !root.selectFolders)
return "Select File"
else
return "Select"
}
icon: "filepicker-check"
enabled: filePickerPanel.currentSelection.length > 0
onClicked: root.confirmSelection()
}
filterStr += ")"
filters.push(filterStr)
}
}
if (filters.length > 0) {
filters.push("All files (*)")
nameFilters = filters
}
}
// Helper function to convert URL to local path
function urlToPath(url) {
var path = url.toString()
// Remove file:// prefix (works for both Windows and Unix)
path = path.replace(/^file:\/\/\//, "/") // Unix
path = path.replace(/^file:\/\//, "") // Windows
// Handle Windows drive letters
if (Qt.platform.os === "windows") {
path = path.replace(/^\/([A-Z]:)/, "$1")
}
return path
}
// Get default folder with proper fallback
function getDefaultFolder() {
if (root.initialPath) {
return "file:///" + root.initialPath.replace(/^\//, "")
}
// Fallback to home directory
try {
return StandardPaths.writableLocation(StandardPaths.HomeLocation)
} catch (e) {
// Final fallback if StandardPaths fails
return "file:///" + (Qt.platform.os === "windows" ? "C:/Users" : "/home")
}
}
// FileDialog for file selection (Qt 6.x)
FileDialog {
id: fileDialog
title: root.title
currentFolder: getDefaultFolder()
fileMode: root.multipleSelection ? FileDialog.OpenFiles : FileDialog.OpenFile
nameFilters: root.nameFilters
acceptLabel: root.acceptLabel
rejectLabel: root.rejectLabel
modality: Qt.WindowModal
onAccepted: {
if (fileMode === FileDialog.OpenFiles) {
var paths = []
for (var i = 0; i < fileDialog.selectedFiles.length; i++) {
paths.push(urlToPath(fileDialog.selectedFiles[i]))
Connections {
target: root
function onShouldResetSelectionChanged() {
if (root.shouldResetSelection) {
filePickerPanel.currentSelection = []
root.shouldResetSelection = false
}
root.selectedPaths = paths
root.selectedPath = paths.length > 0 ? paths[0] : ""
root.pathsSelected(paths)
root.accepted(paths)
} else {
var singlePath = urlToPath(fileDialog.selectedFile)
root.selectedPath = singlePath
root.selectedPaths = [singlePath]
root.pathSelected(singlePath)
root.accepted([singlePath])
}
root.handleClose()
}
onRejected: {
root.rejected()
root.handleClose()
}
}
// FolderDialog for folder selection (Qt 6.x)
FolderDialog {
id: folderDialog
title: root.title
currentFolder: getDefaultFolder()
acceptLabel: root.acceptLabel
rejectLabel: root.rejectLabel
modality: Qt.WindowModal
onAccepted: {
var folderPath = urlToPath(folderDialog.selectedFolder)
root.selectedPath = folderPath
root.selectedPaths = [folderPath]
root.pathSelected(folderPath)
root.accepted([folderPath])
root.handleClose()
}
onRejected: {
root.rejected()
root.handleClose()
Component.onCompleted: {
if (!root.currentPath)
root.currentPath = root.initialPath
folderModel.folder = "file://" + root.currentPath
}
}
}