Add NInputButton widget and FileManagerService integration

NInputButton.qml: new input+button widget
FileManagerService.qml: singleton service for file/folder dialogs
NFileManager.qml: create first iteration of filemanager
WallpaperTab.qml: integrate NInputButton
ScreenRecorderTab.qml: integrate NInputButton
GeneralTab.qml: integrate NInputButton
This commit is contained in:
Ly-sec
2025-09-21 13:06:57 +02:00
parent 2e0214ddb8
commit 86d891cfa8
6 changed files with 1086 additions and 10 deletions
+18 -3
View File
@@ -30,15 +30,30 @@ ColumnLayout {
Layout.alignment: Qt.AlignTop
}
NTextInput {
Layout.fillWidth: true
NInputButton {
label: `${Quickshell.env("USER") || "user"}'s profile picture`
description: "Your profile picture that appears throughout the interface."
text: Settings.data.general.avatarImage
placeholderText: "/home/user/.face"
onEditingFinished: {
buttonIcon: "photo"
buttonTooltip: "Browse for avatar image"
onInputEditingFinished: {
Settings.data.general.avatarImage = text
}
onButtonClicked: {
FileManagerService.open({
"title": "Select Avatar Image",
"initialPath": Settings.data.general.avatarImage || Quickshell.env("HOME"),
"selectFiles": true,
"scaling": scaling,
"onSelected": function (path) {
Settings.data.general.avatarImage = path
text = path
},
"parent": root
})
}
}
}
@@ -20,16 +20,30 @@ ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NTextInput {
NInputButton {
label: "Output folder"
description: "Folder where screen recordings will be saved."
placeholderText: "/home/xxx/Videos"
text: Settings.data.screenRecorder.directory
onEditingFinished: {
buttonIcon: "folder-open"
buttonTooltip: "Browse for output folder"
onInputEditingFinished: {
Settings.data.screenRecorder.directory = text
}
Layout.maximumWidth: 420 * scaling
onButtonClicked: {
FileManagerService.open({
"title": "Select Output Folder",
"initialPath": Settings.data.screenRecorder.directory || Quickshell.env("HOME") + "/Videos",
"selectFiles": false,
"scaling": scaling,
"onSelected": function (path) {
Settings.data.screenRecorder.directory = path
text = path
},
"parent": root
})
}
}
ColumnLayout {
+49 -3
View File
@@ -29,14 +29,21 @@ ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NTextInput {
NInputButton {
id: wallpaperPathInput
label: "Wallpaper folder"
description: "Path to your main wallpaper folder."
text: Settings.data.wallpaper.directory
onEditingFinished: {
buttonIcon: "folder-open"
buttonTooltip: "Browse for wallpaper folder"
Layout.fillWidth: true
onInputEditingFinished: {
Settings.data.wallpaper.directory = text
}
Layout.maximumWidth: 420 * scaling
onButtonClicked: {
openFileManager()
}
}
// Monitor-specific directories
@@ -73,11 +80,22 @@ ColumnLayout {
Layout.preferredWidth: 90 * scaling
}
NTextInput {
id: monitorPathInput
Layout.fillWidth: true
text: WallpaperService.getMonitorDirectory(modelData.name)
onEditingFinished: WallpaperService.setMonitorDirectory(modelData.name, text)
Layout.maximumWidth: 420 * scaling
}
NIconButton {
icon: "folder-open"
tooltipText: "Browse for wallpaper folder"
baseSize: Style.baseWidgetSize * 0.8
Layout.alignment: Qt.AlignBottom
onClicked: {
openMonitorFileManager(modelData.name)
}
}
}
}
}
@@ -324,4 +342,32 @@ ColumnLayout {
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// File manager functions
function openFileManager() {
FileManagerService.open({
"title": "Select Wallpaper Folder",
"initialPath": Settings.data.wallpaper.directory || Quickshell.env("HOME"),
"selectFiles": false,
"scaling": scaling,
"onSelected": function (path) {
Settings.data.wallpaper.directory = path
wallpaperPathInput.text = path
},
"parent": root
})
}
function openMonitorFileManager(monitorName) {
FileManagerService.open({
"title": "Select Monitor Wallpaper Folder",
"initialPath": WallpaperService.getMonitorDirectory(monitorName),
"selectFiles": false,
"scaling": scaling,
"onSelected": function (path) {
WallpaperService.setMonitorDirectory(monitorName, path)
},
"parent": root
})
}
}
+36
View File
@@ -0,0 +1,36 @@
pragma Singleton
import QtQuick
import Quickshell
QtObject {
id: root
// Function to open a file manager dialog
function open(options) {
var component = Qt.createComponent(Qt.resolvedUrl(Quickshell.shellDir + "/Widgets/NFileManager.qml"))
if (component.status === Component.Ready) {
var dialog = component.createObject(options.parent || Overlay.overlay, {
"title": options.title || "Select File/Folder",
"initialPath": options.initialPath || Quickshell.env("HOME"),
"selectFiles": options.selectFiles || false,
"selectFolders": !options.selectFiles || false,
"scaling": options.scaling || 1.0
})
if (dialog) {
if (options.onSelected) {
if (options.selectFiles) {
dialog.fileSelected.connect(options.onSelected)
} else {
dialog.folderSelected.connect(options.onSelected)
}
}
dialog.open()
return dialog
}
} else {
console.error("Component error:", component.errorString())
}
return null
}
}
+882
View File
@@ -0,0 +1,882 @@
import QtQuick
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
Popup {
id: root
// Public properties
property string title: "File Manager"
property string initialPath: Quickshell.env("HOME") || "/home"
property bool selectFiles: true
property bool selectFolders: true
property var nameFilters: ["*"] // Default to show all files
property bool showDirs: true
property real scaling: 1.0
// Selected files/folders
property var selectedPaths: []
property string currentPath: initialPath
property bool shouldResetSelection: false
// Signals
signal fileSelected(string path)
signal filesSelected(var paths)
signal folderSelected(string path)
signal cancelled
// Override the open function to ensure proper initialization
function openFileManager() {
// Ensure we have a valid path
if (!root.currentPath || root.currentPath === "") {
root.currentPath = root.initialPath
}
// Signal that selection should be reset
shouldResetSelection = true
open()
}
// Helper functions
function getFileIcon(fileName) {
var extension = fileName.split('.').pop().toLowerCase()
switch (extension) {
case 'txt':
case 'md':
case 'log':
return 'file-text'
case 'jpg':
case 'jpeg':
case 'png':
case 'gif':
case 'bmp':
case 'svg':
return 'photo'
case 'mp4':
case 'avi':
case 'mkv':
case 'mov':
return 'video'
case 'mp3':
case 'wav':
case 'flac':
case 'ogg':
return 'music'
case 'zip':
case 'tar':
case 'gz':
case 'rar':
case '7z':
return 'archive'
case 'pdf':
return 'file-text'
case 'doc':
case 'docx':
return 'file-text'
case 'xls':
case 'xlsx':
return 'table'
case 'ppt':
case 'pptx':
return 'presentation'
case 'html':
case 'htm':
case 'css':
case 'js':
case 'json':
case 'xml':
return 'code'
case 'exe':
case 'app':
case 'deb':
case 'rpm':
return 'settings'
default:
return 'file'
}
}
function formatFileSize(bytes) {
if (bytes === 0)
return "0 B"
var k = 1024
var sizes = ["B", "KB", "MB", "GB", "TB"]
var i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]
}
function confirmSelection() {
if (fileManagerPanel.currentSelection.length === 0) {
return
}
root.selectedPaths = fileManagerPanel.currentSelection
if (fileManagerPanel.currentSelection.length === 1) {
var path = fileManagerPanel.currentSelection[0]
if (root.selectFiles && !root.selectFolders) {
root.fileSelected(path)
} else if (root.selectFolders && !root.selectFiles) {
root.folderSelected(path)
} else {
// Both files and folders allowed
var isDir = folderModel.get(folderModel.indexOf(path), "fileIsDir")
if (isDir) {
root.folderSelected(path)
} else {
root.fileSelected(path)
}
}
} else {
root.filesSelected(fileManagerPanel.currentSelection)
}
root.close()
}
// Function to update the filtered model
function updateFilteredModel() {
filteredModel.clear()
var searchText = fileManagerPanel.filterText.toLowerCase()
for (var i = 0; i < folderModel.count; i++) {
var fileName = folderModel.get(i, "fileName")
var filePath = folderModel.get(i, "filePath")
var fileIsDir = folderModel.get(i, "fileIsDir")
var fileSize = folderModel.get(i, "fileSize")
// If no search text or file name contains search text
if (searchText === "" || fileName.toLowerCase().includes(searchText)) {
filteredModel.append({
"fileName": fileName,
"filePath": filePath,
"fileIsDir": fileIsDir,
"fileSize": fileSize
})
}
}
}
// Function to intelligently truncate filenames
function truncateFileName(fileName, maxLength) {
if (fileName.length <= maxLength) {
return fileName
}
// For files, try to preserve the extension
var lastDot = fileName.lastIndexOf('.')
if (lastDot > 0 && lastDot < fileName.length - 1) {
var name = fileName.substring(0, lastDot)
var ext = fileName.substring(lastDot)
var availableForName = maxLength - ext.length - 3 // 3 for "..."
if (availableForName > 0) {
return name.substring(0, availableForName) + "..." + ext
}
}
// Fallback: just truncate from the end
return fileName.substring(0, maxLength - 3) + "..."
}
// Popup properties
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: fileManagerPanel
anchors.fill: parent
anchors.margins: Style.marginL * scaling
color: Color.transparent
property string filterText: ""
property var currentSelection: []
property bool isNavigating: false
property bool viewMode: true // true = grid, false = list
property string searchText: ""
property bool isSearching: false
property bool showSearchBar: false
// Keyboard shortcuts
Keys.onPressed: event => {
if (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_F) {
fileManagerPanel.showSearchBar = !fileManagerPanel.showSearchBar
if (fileManagerPanel.showSearchBar) {
// Focus the search input when opening
Qt.callLater(() => {
searchInput.forceActiveFocus()
})
}
event.accepted = true
} else if (event.key === Qt.Key_Escape && fileManagerPanel.showSearchBar) {
// Close search bar on Escape
fileManagerPanel.showSearchBar = false
fileManagerPanel.searchText = ""
fileManagerPanel.isSearching = false
fileManagerPanel.filterText = ""
root.updateFilteredModel()
event.accepted = true
}
}
// Focus the file manager to receive key events
focus: true
ColumnLayout {
anchors.fill: parent
spacing: Style.marginM * scaling
// Header row (like SettingsPanel)
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
// Main icon
NIcon {
icon: "folder"
color: Color.mPrimary
font.pointSize: Style.fontSizeXXL * scaling
}
// Main title
NText {
text: root.title
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
// Action buttons
NIconButton {
icon: "refresh"
tooltipText: "Refresh"
Layout.alignment: Qt.AlignVCenter
onClicked: {
folderModel.refresh()
}
}
NIconButton {
icon: "close"
tooltipText: "Close"
Layout.alignment: Qt.AlignVCenter
onClicked: {
root.cancelled()
root.close()
}
}
}
// Divider
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.margins: Style.marginS * scaling
spacing: Style.marginS * scaling
// Navigation buttons
NIconButton {
icon: "arrow-left"
tooltipText: "Back"
baseSize: Style.baseWidgetSize * 0.8
enabled: folderModel.folder.toString() !== "file://" + root.initialPath
onClicked: {
var parentPath = folderModel.parentFolder.toString().replace("file://", "")
if (parentPath !== folderModel.folder.toString().replace("file://", "")) {
folderModel.folder = "file://" + parentPath
root.currentPath = parentPath
}
}
}
NIconButton {
icon: "arrow-up"
tooltipText: "Up"
baseSize: Style.baseWidgetSize * 0.8
enabled: folderModel.folder.toString() !== "file:///"
onClicked: {
var parentPath = folderModel.parentFolder.toString().replace("file://", "")
folderModel.folder = "file://" + parentPath
root.currentPath = parentPath
}
}
NIconButton {
icon: "home"
tooltipText: "Home"
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
var homePath = Quickshell.env("HOME") || "/home"
folderModel.folder = "file://" + homePath
root.currentPath = homePath
}
}
// View mode toggle
NIconButton {
icon: fileManagerPanel.viewMode ? "layout-grid" : "list"
tooltipText: fileManagerPanel.viewMode ? "List View" : "Grid View"
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
fileManagerPanel.viewMode = !fileManagerPanel.viewMode
}
}
// Search toggle
NIconButton {
icon: fileManagerPanel.showSearchBar ? "x" : "search"
tooltipText: fileManagerPanel.showSearchBar ? "Close Search" : "Search"
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
fileManagerPanel.showSearchBar = !fileManagerPanel.showSearchBar
if (!fileManagerPanel.showSearchBar) {
// Clear search when closing
fileManagerPanel.searchText = ""
fileManagerPanel.isSearching = false
fileManagerPanel.filterText = ""
root.updateFilteredModel()
}
}
}
// Location input
NTextInput {
id: locationInput
text: root.currentPath
placeholderText: "Enter path..."
Layout.fillWidth: true
onEditingFinished: {
var newPath = text.trim()
if (newPath !== "" && newPath !== root.currentPath) {
// Navigate to the path
folderModel.folder = "file://" + newPath
root.currentPath = newPath
} else {
// Reset to current path if invalid or same
text = root.currentPath
}
}
// Update text when currentPath changes from navigation (but not when user is typing)
Connections {
target: root
function onCurrentPathChanged() {
if (!locationInput.activeFocus) {
locationInput.text = root.currentPath
}
}
}
}
}
}
// Search bar (appears when search is toggled)
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: fileManagerPanel.showSearchBar
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Style.marginS * scaling
spacing: Style.marginS * scaling
NIcon {
icon: "search"
color: Color.mOnSurfaceVariant
font.pointSize: Style.fontSizeS * scaling
}
NTextInput {
id: searchInput
placeholderText: "Search files and folders..."
Layout.fillWidth: true
text: fileManagerPanel.searchText
onTextChanged: {
fileManagerPanel.searchText = text
fileManagerPanel.isSearching = text.length > 0
fileManagerPanel.filterText = text
root.updateFilteredModel()
}
Keys.onEscapePressed: {
fileManagerPanel.showSearchBar = false
fileManagerPanel.searchText = ""
fileManagerPanel.isSearching = false
fileManagerPanel.filterText = ""
root.updateFilteredModel()
}
}
NIconButton {
icon: "x"
tooltipText: "Clear"
baseSize: Style.baseWidgetSize * 0.6
visible: fileManagerPanel.searchText.length > 0
onClicked: {
searchInput.text = ""
fileManagerPanel.searchText = ""
fileManagerPanel.isSearching = false
fileManagerPanel.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: true
sortField: FolderListModel.Name
sortReversed: false
onFolderChanged: {
root.currentPath = folder.toString().replace("file://", "")
fileManagerPanel.currentSelection = []
}
onStatusChanged: {
if (status === FolderListModel.Error) {
console.log("FolderListModel error for path:", root.currentPath)
// Fallback to home directory if there's an error
if (root.currentPath !== Quickshell.env("HOME")) {
folder = "file://" + Quickshell.env("HOME")
root.currentPath = Quickshell.env("HOME")
}
} else if (status === FolderListModel.Ready) {
root.updateFilteredModel()
}
}
}
// Filtered model for search functionality
ListModel {
id: filteredModel
}
// Grid view
GridView {
id: gridView
anchors.fill: parent
anchors.margins: Style.marginM * scaling
model: filteredModel
visible: fileManagerPanel.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: ScrollBar {
parent: gridView
x: gridView.mirrored ? 0 : gridView.width - width
y: 0
height: gridView.height
policy: ScrollBar.AsNeeded
contentItem: Rectangle {
implicitWidth: 6 * scaling
implicitHeight: 100
radius: 3 * scaling
color: parent.pressed ? Color.mTertiary : parent.hovered ? Color.mSecondary : Color.mTertiary
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
}
delegate: Rectangle {
id: gridItem
width: gridView.itemSize
height: gridView.cellHeight
color: Color.transparent
radius: Style.radiusM * scaling
property string fileName: model.fileName
property string filePath: model.filePath
property bool isDirectory: model.fileIsDir
property bool isSelected: fileManagerPanel.currentSelection.includes(filePath)
// Selection background (covers entire item)
Rectangle {
anchors.fill: parent
color: isSelected ? Qt.alpha(Color.mSecondary, 0.15) : Color.transparent
radius: parent.radius
border.color: isSelected ? Color.mSecondary : Color.mSurface
border.width: Math.max(1, Style.borderL * 1.5 * scaling)
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
// Hover overlay
Rectangle {
anchors.fill: parent
color: Color.mSurface
opacity: (mouseArea.containsMouse && !isSelected) ? 0.1 : 0
radius: parent.radius
Behavior on opacity {
NumberAnimation {
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
NIcon {
icon: isDirectory ? "folder" : root.getFileIcon(fileName)
font.pointSize: Style.fontSizeXXL * scaling
color: isDirectory ? Color.mPrimary : Color.mOnSurfaceVariant
anchors.centerIn: parent
}
// Selection indicator (like WallpaperSelector)
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: "check"
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSecondary
anchors.centerIn: parent
}
}
}
NText {
text: fileName
color: isSelected ? Color.mPrimary : 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 (isDirectory) {
if (root.selectFolders && !root.selectFiles) {
fileManagerPanel.currentSelection = [filePath]
} else {
folderModel.folder = "file://" + filePath
root.currentPath = filePath
}
} else {
if (root.selectFiles) {
fileManagerPanel.currentSelection = [filePath]
}
}
}
}
onDoubleClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (isDirectory) {
if (root.selectFolders && !root.selectFiles) {
fileManagerPanel.currentSelection = [filePath]
root.confirmSelection()
} else {
folderModel.folder = "file://" + filePath
root.currentPath = filePath
}
} else {
if (root.selectFiles) {
fileManagerPanel.currentSelection = [filePath]
root.confirmSelection()
}
}
}
}
}
}
}
// List view
ListView {
id: listView
anchors.fill: parent
anchors.margins: Style.marginS * scaling
model: filteredModel
visible: !fileManagerPanel.viewMode
clip: true
ScrollBar.vertical: ScrollBar {
parent: listView
x: listView.mirrored ? 0 : listView.width - width
y: 0
height: listView.height
policy: ScrollBar.AsNeeded
contentItem: Rectangle {
implicitWidth: 6 * scaling
implicitHeight: 100
radius: 3 * scaling
color: parent.pressed ? Color.mTertiary : parent.hovered ? Color.mSecondary : Color.mTertiary
opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
}
delegate: Rectangle {
id: listItem
width: listView.width
height: 40 * scaling
color: {
if (fileManagerPanel.currentSelection.includes(filePath)) {
return Color.mSecondary
}
if (mouseArea.containsMouse) {
return Qt.alpha(Color.mOnSurface, 0.1)
}
return Color.transparent
}
radius: Style.radiusS * scaling
property string fileName: model.fileName
property string filePath: model.filePath
property bool isDirectory: model.fileIsDir
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: isDirectory ? "folder" : root.getFileIcon(fileName)
font.pointSize: Style.fontSizeL * scaling
color: isDirectory ? Color.mPrimary : Color.mOnSurfaceVariant
}
NText {
text: fileName
color: fileManagerPanel.currentSelection.includes(filePath) ? Color.mOnSecondary : Color.mOnSurface
font.pointSize: Style.fontSizeM * scaling
font.weight: fileManagerPanel.currentSelection.includes(filePath) ? Style.fontWeightBold : Style.fontWeightRegular
Layout.fillWidth: true
elide: Text.ElideRight
}
NText {
text: isDirectory ? "" : root.formatFileSize(model.fileSize)
color: fileManagerPanel.currentSelection.includes(filePath) ? Color.mOnSecondary : Color.mOnSurfaceVariant
font.pointSize: Style.fontSizeS * scaling
visible: !isDirectory
Layout.preferredWidth: implicitWidth
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (isDirectory) {
if (root.selectFolders && !root.selectFiles) {
fileManagerPanel.currentSelection = [filePath]
} else {
folderModel.folder = "file://" + filePath
root.currentPath = filePath
}
} else {
if (root.selectFiles) {
fileManagerPanel.currentSelection = [filePath]
}
}
}
}
onDoubleClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (isDirectory) {
if (root.selectFolders && !root.selectFiles) {
fileManagerPanel.currentSelection = [filePath]
root.confirmSelection()
} else {
folderModel.folder = "file://" + filePath
root.currentPath = filePath
}
} else {
if (root.selectFiles) {
fileManagerPanel.currentSelection = [filePath]
root.confirmSelection()
}
}
}
}
}
}
}
}
// Status and actions
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
// Status text
NText {
text: {
if (fileManagerPanel.isSearching) {
return "Searching for: \"" + fileManagerPanel.searchText + "\" (" + filteredModel.count + " matches)"
} else if (fileManagerPanel.currentSelection.length > 0) {
return fileManagerPanel.currentSelection.length + " item(s) selected"
} else {
return filteredModel.count + " items"
}
}
color: fileManagerPanel.isSearching ? Color.mPrimary : Color.mOnSurfaceVariant
font.pointSize: Style.fontSizeS * scaling
Layout.fillWidth: true
}
// Action buttons
NButton {
text: "Cancel"
outlined: true
onClicked: {
root.cancelled()
root.close()
}
}
NButton {
text: "Select"
icon: "check"
enabled: fileManagerPanel.currentSelection.length > 0
onClicked: root.confirmSelection()
}
}
}
// Watch for selection reset flag
Connections {
target: root
function onShouldResetSelectionChanged() {
if (root.shouldResetSelection) {
fileManagerPanel.currentSelection = []
root.shouldResetSelection = false
}
}
}
Component.onCompleted: {
// Ensure we have a valid path
if (!root.currentPath || root.currentPath === "") {
root.currentPath = root.initialPath
}
folderModel.folder = "file://" + root.currentPath
}
}
}
+83
View File
@@ -0,0 +1,83 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
Layout.fillWidth: true
property alias text: input.text
property alias placeholderText: input.placeholderText
property string label: ""
property string description: ""
property alias buttonIcon: button.icon
property alias buttonTooltip: button.tooltipText
property alias buttonEnabled: button.enabled
property real maximumWidth: 0
signal buttonClicked
signal inputTextChanged(string text)
signal inputEditingFinished
spacing: Style.marginS * scaling
// Label and description
NLabel {
label: root.label
description: root.description
visible: root.label !== "" || root.description !== ""
Layout.fillWidth: true
}
// Input field with button
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
// Input field container
Rectangle {
id: inputContainer
Layout.fillWidth: true
Layout.maximumWidth: root.maximumWidth > 0 ? root.maximumWidth : -1
implicitHeight: Style.baseWidgetSize * 1.1 * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: input.activeFocus ? Color.mSecondary : Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
TextField {
id: input
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Style.marginM * scaling
color: Color.mOnSurface
font.pointSize: Style.fontSizeS * scaling
font.family: Settings.data.ui.fontDefault
selectByMouse: true
background: Item {} // Remove default background since we have our own Rectangle
onTextChanged: root.inputTextChanged(text)
onEditingFinished: root.inputEditingFinished()
}
}
// Button
NIconButton {
id: button
baseSize: Style.baseWidgetSize
onClicked: root.buttonClicked()
}
}
}