From 86d891cfa864d5cc7874194cc4913d905a487683 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 21 Sep 2025 13:06:57 +0200 Subject: [PATCH 1/7] 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 --- Modules/SettingsPanel/Tabs/GeneralTab.qml | 21 +- .../SettingsPanel/Tabs/ScreenRecorderTab.qml | 22 +- Modules/SettingsPanel/Tabs/WallpaperTab.qml | 52 +- Services/FileManagerService.qml | 36 + Widgets/NFileManager.qml | 882 ++++++++++++++++++ Widgets/NInputButton.qml | 83 ++ 6 files changed, 1086 insertions(+), 10 deletions(-) create mode 100644 Services/FileManagerService.qml create mode 100644 Widgets/NFileManager.qml create mode 100644 Widgets/NInputButton.qml diff --git a/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml index 2eec600f..775dc831 100644 --- a/Modules/SettingsPanel/Tabs/GeneralTab.qml +++ b/Modules/SettingsPanel/Tabs/GeneralTab.qml @@ -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 + }) + } } } diff --git a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml index 64408d63..a8c9eae5 100644 --- a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml +++ b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml @@ -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 { diff --git a/Modules/SettingsPanel/Tabs/WallpaperTab.qml b/Modules/SettingsPanel/Tabs/WallpaperTab.qml index fbece072..54ad5394 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperTab.qml @@ -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 + }) + } } diff --git a/Services/FileManagerService.qml b/Services/FileManagerService.qml new file mode 100644 index 00000000..742e4c1c --- /dev/null +++ b/Services/FileManagerService.qml @@ -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 + } +} diff --git a/Widgets/NFileManager.qml b/Widgets/NFileManager.qml new file mode 100644 index 00000000..38bfaf12 --- /dev/null +++ b/Widgets/NFileManager.qml @@ -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 + } + } +} diff --git a/Widgets/NInputButton.qml b/Widgets/NInputButton.qml new file mode 100644 index 00000000..ed1bafd0 --- /dev/null +++ b/Widgets/NInputButton.qml @@ -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() + } + } +} From 596500472168c85adafd163e8e04d7c8f53c98ae Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 21 Sep 2025 13:18:52 +0200 Subject: [PATCH 2/7] NFileManager: fix file path, add image thumbnails --- Services/FileManagerService.qml | 13 +++++++++- Widgets/NFileManager.qml | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/Services/FileManagerService.qml b/Services/FileManagerService.qml index 742e4c1c..a6288e7a 100644 --- a/Services/FileManagerService.qml +++ b/Services/FileManagerService.qml @@ -10,9 +10,20 @@ QtObject { function open(options) { var component = Qt.createComponent(Qt.resolvedUrl(Quickshell.shellDir + "/Widgets/NFileManager.qml")) if (component.status === Component.Ready) { + // Extract directory from file path if it's a file + var initialPath = options.initialPath || Quickshell.env("HOME") + if (options.selectFiles && initialPath !== Quickshell.env("HOME")) { + // If selecting files and path is not home, extract directory + var pathParts = initialPath.split('/') + if (pathParts.length > 1) { + pathParts.pop() // Remove filename + initialPath = pathParts.join('/') || '/' + } + } + var dialog = component.createObject(options.parent || Overlay.overlay, { "title": options.title || "Select File/Folder", - "initialPath": options.initialPath || Quickshell.env("HOME"), + "initialPath": initialPath, "selectFiles": options.selectFiles || false, "selectFolders": !options.selectFiles || false, "scaling": options.scaling || 1.0 diff --git a/Widgets/NFileManager.qml b/Widgets/NFileManager.qml index 38bfaf12..cd5805b8 100644 --- a/Widgets/NFileManager.qml +++ b/Widgets/NFileManager.qml @@ -600,11 +600,57 @@ Popup { Layout.preferredHeight: Math.round(gridView.itemSize * 0.67) color: Color.transparent + // Check if file is an image + property bool isImage: { + if (isDirectory) + return false + var ext = fileName.split('.').pop().toLowerCase() + return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'].includes(ext) + } + + // Show thumbnail for images, icon for others + Image { + id: thumbnail + anchors.fill: parent + anchors.margins: Style.marginXS * scaling + source: iconContainer.isImage ? "file://" + filePath : "" + fillMode: Image.PreserveAspectFit + visible: iconContainer.isImage && status === Image.Ready + smooth: false // Disable smooth for faster rendering + cache: true + asynchronous: true // Load images asynchronously + sourceSize.width: 120 * scaling // Limit image size for faster loading + sourceSize.height: 120 * scaling + + // Fallback to icon if image fails to load or takes too long + onStatusChanged: { + if (status === Image.Error) { + visible = false + } + } + + // Show loading indicator while image loads + Rectangle { + anchors.fill: parent + color: Color.mSurfaceVariant + radius: Style.radiusS * scaling + visible: thumbnail.status === Image.Loading + + NIcon { + icon: "photo" + font.pointSize: Style.fontSizeL * scaling + color: Color.mOnSurfaceVariant + anchors.centerIn: parent + } + } + } + NIcon { icon: isDirectory ? "folder" : root.getFileIcon(fileName) font.pointSize: Style.fontSizeXXL * scaling color: isDirectory ? Color.mPrimary : Color.mOnSurfaceVariant anchors.centerIn: parent + visible: !iconContainer.isImage || thumbnail.status !== Image.Ready } // Selection indicator (like WallpaperSelector) From dfe3aed46e357ab99164ea067d5e908fe2f69a71 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 21 Sep 2025 19:39:52 +0200 Subject: [PATCH 3/7] NFilePicker: fix some layout/color issues --- Widgets/NFileManager.qml | 65 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/Widgets/NFileManager.qml b/Widgets/NFileManager.qml index cd5805b8..811314e5 100644 --- a/Widgets/NFileManager.qml +++ b/Widgets/NFileManager.qml @@ -151,6 +151,11 @@ Popup { var fileIsDir = folderModel.get(i, "fileIsDir") var fileSize = folderModel.get(i, "fileSize") + // In folder selection mode, only show directories + if (root.selectFolders && !root.selectFiles && !fileIsDir) { + continue + } + // If no search text or file name contains search text if (searchText === "" || fileName.toLowerCase().includes(searchText)) { filteredModel.append({ @@ -537,8 +542,8 @@ Popup { contentItem: Rectangle { implicitWidth: 6 * scaling implicitHeight: 100 - radius: 3 * scaling - color: parent.pressed ? Color.mTertiary : parent.hovered ? Color.mSecondary : Color.mTertiary + 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 { @@ -546,6 +551,26 @@ Popup { 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 + } + } } } @@ -753,8 +778,8 @@ Popup { contentItem: Rectangle { implicitWidth: 6 * scaling implicitHeight: 100 - radius: 3 * scaling - color: parent.pressed ? Color.mTertiary : parent.hovered ? Color.mSecondary : Color.mTertiary + 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 { @@ -762,6 +787,26 @@ Popup { 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 + } + } } } @@ -799,7 +844,7 @@ Popup { NIcon { icon: isDirectory ? "folder" : root.getFileIcon(fileName) font.pointSize: Style.fontSizeL * scaling - color: isDirectory ? Color.mPrimary : Color.mOnSurfaceVariant + color: isDirectory ? (fileManagerPanel.currentSelection.includes(filePath) ? Color.mOnSecondary : Color.mPrimary) : Color.mOnSurfaceVariant } NText { @@ -898,7 +943,15 @@ Popup { } NButton { - text: "Select" + text: { + if (root.selectFolders && !root.selectFiles) { + return "Select Folder" + } else if (root.selectFiles && !root.selectFolders) { + return "Select File" + } else { + return "Select" + } + } icon: "check" enabled: fileManagerPanel.currentSelection.length > 0 onClicked: root.confirmSelection() From 3bbf26a18ec5056f2c9d1ddbb25ea2bbdd00f604 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 21 Sep 2025 19:44:04 +0200 Subject: [PATCH 4/7] NFilePicker: renamed NFileManager to NFilePicker, update grid hover --- Modules/SettingsPanel/Tabs/GeneralTab.qml | 22 +-- .../SettingsPanel/Tabs/ScreenRecorderTab.qml | 22 +-- Modules/SettingsPanel/Tabs/WallpaperTab.qml | 42 +++--- ...nagerService.qml => FilePickerService.qml} | 2 +- Widgets/{NFileManager.qml => NFilePicker.qml} | 130 +++++++++--------- 5 files changed, 110 insertions(+), 108 deletions(-) rename Services/{FileManagerService.qml => FilePickerService.qml} (97%) rename Widgets/{NFileManager.qml => NFilePicker.qml} (87%) diff --git a/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml index 775dc831..8032f303 100644 --- a/Modules/SettingsPanel/Tabs/GeneralTab.qml +++ b/Modules/SettingsPanel/Tabs/GeneralTab.qml @@ -42,17 +42,17 @@ ColumnLayout { 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 - }) + FilePickerService.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 + }) } } } diff --git a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml index a8c9eae5..00b87261 100644 --- a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml +++ b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml @@ -32,17 +32,17 @@ ColumnLayout { Settings.data.screenRecorder.directory = text } 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 - }) + FilePickerService.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 + }) } } diff --git a/Modules/SettingsPanel/Tabs/WallpaperTab.qml b/Modules/SettingsPanel/Tabs/WallpaperTab.qml index 54ad5394..b4367c8a 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperTab.qml @@ -345,29 +345,29 @@ ColumnLayout { // 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 - }) + FilePickerService.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 - }) + FilePickerService.open({ + "title": "Select Monitor Wallpaper Folder", + "initialPath": WallpaperService.getMonitorDirectory(monitorName), + "selectFiles": false, + "scaling": scaling, + "onSelected": function (path) { + WallpaperService.setMonitorDirectory(monitorName, path) + }, + "parent": root + }) } } diff --git a/Services/FileManagerService.qml b/Services/FilePickerService.qml similarity index 97% rename from Services/FileManagerService.qml rename to Services/FilePickerService.qml index a6288e7a..94ff4ede 100644 --- a/Services/FileManagerService.qml +++ b/Services/FilePickerService.qml @@ -8,7 +8,7 @@ QtObject { // Function to open a file manager dialog function open(options) { - var component = Qt.createComponent(Qt.resolvedUrl(Quickshell.shellDir + "/Widgets/NFileManager.qml")) + var component = Qt.createComponent(Qt.resolvedUrl(Quickshell.shellDir + "/Widgets/NFilePicker.qml")) if (component.status === Component.Ready) { // Extract directory from file path if it's a file var initialPath = options.initialPath || Quickshell.env("HOME") diff --git a/Widgets/NFileManager.qml b/Widgets/NFilePicker.qml similarity index 87% rename from Widgets/NFileManager.qml rename to Widgets/NFilePicker.qml index 811314e5..3bddd62b 100644 --- a/Widgets/NFileManager.qml +++ b/Widgets/NFilePicker.qml @@ -13,7 +13,7 @@ Popup { id: root // Public properties - property string title: "File Manager" + property string title: "File Picker" property string initialPath: Quickshell.env("HOME") || "/home" property bool selectFiles: true property bool selectFolders: true @@ -112,14 +112,14 @@ Popup { } function confirmSelection() { - if (fileManagerPanel.currentSelection.length === 0) { + if (filePickerPanel.currentSelection.length === 0) { return } - root.selectedPaths = fileManagerPanel.currentSelection + root.selectedPaths = filePickerPanel.currentSelection - if (fileManagerPanel.currentSelection.length === 1) { - var path = fileManagerPanel.currentSelection[0] + if (filePickerPanel.currentSelection.length === 1) { + var path = filePickerPanel.currentSelection[0] if (root.selectFiles && !root.selectFolders) { root.fileSelected(path) } else if (root.selectFolders && !root.selectFiles) { @@ -134,7 +134,7 @@ Popup { } } } else { - root.filesSelected(fileManagerPanel.currentSelection) + root.filesSelected(filePickerPanel.currentSelection) } root.close() @@ -143,7 +143,7 @@ Popup { // Function to update the filtered model function updateFilteredModel() { filteredModel.clear() - var searchText = fileManagerPanel.filterText.toLowerCase() + var searchText = filePickerPanel.filterText.toLowerCase() for (var i = 0; i < folderModel.count; i++) { var fileName = folderModel.get(i, "fileName") @@ -205,7 +205,7 @@ Popup { } Rectangle { - id: fileManagerPanel + id: filePickerPanel anchors.fill: parent anchors.margins: Style.marginL * scaling color: Color.transparent @@ -221,20 +221,20 @@ Popup { // Keyboard shortcuts Keys.onPressed: event => { if (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_F) { - fileManagerPanel.showSearchBar = !fileManagerPanel.showSearchBar - if (fileManagerPanel.showSearchBar) { + filePickerPanel.showSearchBar = !filePickerPanel.showSearchBar + if (filePickerPanel.showSearchBar) { // Focus the search input when opening Qt.callLater(() => { searchInput.forceActiveFocus() }) } event.accepted = true - } else if (event.key === Qt.Key_Escape && fileManagerPanel.showSearchBar) { + } else if (event.key === Qt.Key_Escape && filePickerPanel.showSearchBar) { // Close search bar on Escape - fileManagerPanel.showSearchBar = false - fileManagerPanel.searchText = "" - fileManagerPanel.isSearching = false - fileManagerPanel.filterText = "" + filePickerPanel.showSearchBar = false + filePickerPanel.searchText = "" + filePickerPanel.isSearching = false + filePickerPanel.filterText = "" root.updateFilteredModel() event.accepted = true } @@ -351,26 +351,26 @@ Popup { // View mode toggle NIconButton { - icon: fileManagerPanel.viewMode ? "layout-grid" : "list" - tooltipText: fileManagerPanel.viewMode ? "List View" : "Grid View" + icon: filePickerPanel.viewMode ? "layout-grid" : "list" + tooltipText: filePickerPanel.viewMode ? "List View" : "Grid View" baseSize: Style.baseWidgetSize * 0.8 onClicked: { - fileManagerPanel.viewMode = !fileManagerPanel.viewMode + filePickerPanel.viewMode = !filePickerPanel.viewMode } } // Search toggle NIconButton { - icon: fileManagerPanel.showSearchBar ? "x" : "search" - tooltipText: fileManagerPanel.showSearchBar ? "Close Search" : "Search" + icon: filePickerPanel.showSearchBar ? "x" : "search" + tooltipText: filePickerPanel.showSearchBar ? "Close Search" : "Search" baseSize: Style.baseWidgetSize * 0.8 onClicked: { - fileManagerPanel.showSearchBar = !fileManagerPanel.showSearchBar - if (!fileManagerPanel.showSearchBar) { + filePickerPanel.showSearchBar = !filePickerPanel.showSearchBar + if (!filePickerPanel.showSearchBar) { // Clear search when closing - fileManagerPanel.searchText = "" - fileManagerPanel.isSearching = false - fileManagerPanel.filterText = "" + filePickerPanel.searchText = "" + filePickerPanel.isSearching = false + filePickerPanel.filterText = "" root.updateFilteredModel() } } @@ -416,7 +416,7 @@ Popup { radius: Style.radiusS * scaling border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) - visible: fileManagerPanel.showSearchBar + visible: filePickerPanel.showSearchBar RowLayout { anchors.left: parent.left @@ -435,20 +435,20 @@ Popup { id: searchInput placeholderText: "Search files and folders..." Layout.fillWidth: true - text: fileManagerPanel.searchText + text: filePickerPanel.searchText onTextChanged: { - fileManagerPanel.searchText = text - fileManagerPanel.isSearching = text.length > 0 - fileManagerPanel.filterText = text + filePickerPanel.searchText = text + filePickerPanel.isSearching = text.length > 0 + filePickerPanel.filterText = text root.updateFilteredModel() } Keys.onEscapePressed: { - fileManagerPanel.showSearchBar = false - fileManagerPanel.searchText = "" - fileManagerPanel.isSearching = false - fileManagerPanel.filterText = "" + filePickerPanel.showSearchBar = false + filePickerPanel.searchText = "" + filePickerPanel.isSearching = false + filePickerPanel.filterText = "" root.updateFilteredModel() } } @@ -457,12 +457,12 @@ Popup { icon: "x" tooltipText: "Clear" baseSize: Style.baseWidgetSize * 0.6 - visible: fileManagerPanel.searchText.length > 0 + visible: filePickerPanel.searchText.length > 0 onClicked: { searchInput.text = "" - fileManagerPanel.searchText = "" - fileManagerPanel.isSearching = false - fileManagerPanel.filterText = "" + filePickerPanel.searchText = "" + filePickerPanel.isSearching = false + filePickerPanel.filterText = "" root.updateFilteredModel() } } @@ -490,7 +490,7 @@ Popup { onFolderChanged: { root.currentPath = folder.toString().replace("file://", "") - fileManagerPanel.currentSelection = [] + filePickerPanel.currentSelection = [] } onStatusChanged: { @@ -518,7 +518,7 @@ Popup { anchors.fill: parent anchors.margins: Style.marginM * scaling model: filteredModel - visible: fileManagerPanel.viewMode + visible: filePickerPanel.viewMode clip: true property int columns: Math.max(1, Math.floor(width / (120 * scaling))) @@ -584,7 +584,7 @@ Popup { property string fileName: model.fileName property string filePath: model.filePath property bool isDirectory: model.fileIsDir - property bool isSelected: fileManagerPanel.currentSelection.includes(filePath) + property bool isSelected: filePickerPanel.currentSelection.includes(filePath) // Selection background (covers entire item) Rectangle { @@ -604,9 +604,11 @@ Popup { // Hover overlay Rectangle { anchors.fill: parent - color: Color.mSurface - opacity: (mouseArea.containsMouse && !isSelected) ? 0.1 : 0 + color: Qt.alpha(Color.mPrimary, 0.1) + opacity: (mouseArea.containsMouse && !isSelected) ? 1.0 : 0 radius: parent.radius + border.color: Qt.alpha(Color.mPrimary, 0.3) + border.width: Math.max(1, Style.borderS * scaling) Behavior on opacity { NumberAnimation { duration: Style.animationFast @@ -724,14 +726,14 @@ Popup { if (mouse.button === Qt.LeftButton) { if (isDirectory) { if (root.selectFolders && !root.selectFiles) { - fileManagerPanel.currentSelection = [filePath] + filePickerPanel.currentSelection = [filePath] } else { folderModel.folder = "file://" + filePath root.currentPath = filePath } } else { if (root.selectFiles) { - fileManagerPanel.currentSelection = [filePath] + filePickerPanel.currentSelection = [filePath] } } } @@ -741,7 +743,7 @@ Popup { if (mouse.button === Qt.LeftButton) { if (isDirectory) { if (root.selectFolders && !root.selectFiles) { - fileManagerPanel.currentSelection = [filePath] + filePickerPanel.currentSelection = [filePath] root.confirmSelection() } else { folderModel.folder = "file://" + filePath @@ -749,7 +751,7 @@ Popup { } } else { if (root.selectFiles) { - fileManagerPanel.currentSelection = [filePath] + filePickerPanel.currentSelection = [filePath] root.confirmSelection() } } @@ -765,7 +767,7 @@ Popup { anchors.fill: parent anchors.margins: Style.marginS * scaling model: filteredModel - visible: !fileManagerPanel.viewMode + visible: !filePickerPanel.viewMode clip: true ScrollBar.vertical: ScrollBar { @@ -815,7 +817,7 @@ Popup { width: listView.width height: 40 * scaling color: { - if (fileManagerPanel.currentSelection.includes(filePath)) { + if (filePickerPanel.currentSelection.includes(filePath)) { return Color.mSecondary } if (mouseArea.containsMouse) { @@ -844,21 +846,21 @@ Popup { NIcon { icon: isDirectory ? "folder" : root.getFileIcon(fileName) font.pointSize: Style.fontSizeL * scaling - color: isDirectory ? (fileManagerPanel.currentSelection.includes(filePath) ? Color.mOnSecondary : Color.mPrimary) : Color.mOnSurfaceVariant + color: isDirectory ? (filePickerPanel.currentSelection.includes(filePath) ? Color.mOnSecondary : Color.mPrimary) : Color.mOnSurfaceVariant } NText { text: fileName - color: fileManagerPanel.currentSelection.includes(filePath) ? Color.mOnSecondary : Color.mOnSurface + color: filePickerPanel.currentSelection.includes(filePath) ? Color.mOnSecondary : Color.mOnSurface font.pointSize: Style.fontSizeM * scaling - font.weight: fileManagerPanel.currentSelection.includes(filePath) ? Style.fontWeightBold : Style.fontWeightRegular + font.weight: filePickerPanel.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 + color: filePickerPanel.currentSelection.includes(filePath) ? Color.mOnSecondary : Color.mOnSurfaceVariant font.pointSize: Style.fontSizeS * scaling visible: !isDirectory Layout.preferredWidth: implicitWidth @@ -875,14 +877,14 @@ Popup { if (mouse.button === Qt.LeftButton) { if (isDirectory) { if (root.selectFolders && !root.selectFiles) { - fileManagerPanel.currentSelection = [filePath] + filePickerPanel.currentSelection = [filePath] } else { folderModel.folder = "file://" + filePath root.currentPath = filePath } } else { if (root.selectFiles) { - fileManagerPanel.currentSelection = [filePath] + filePickerPanel.currentSelection = [filePath] } } } @@ -892,7 +894,7 @@ Popup { if (mouse.button === Qt.LeftButton) { if (isDirectory) { if (root.selectFolders && !root.selectFiles) { - fileManagerPanel.currentSelection = [filePath] + filePickerPanel.currentSelection = [filePath] root.confirmSelection() } else { folderModel.folder = "file://" + filePath @@ -900,7 +902,7 @@ Popup { } } else { if (root.selectFiles) { - fileManagerPanel.currentSelection = [filePath] + filePickerPanel.currentSelection = [filePath] root.confirmSelection() } } @@ -919,15 +921,15 @@ Popup { // 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" + if (filePickerPanel.isSearching) { + 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: fileManagerPanel.isSearching ? Color.mPrimary : Color.mOnSurfaceVariant + color: filePickerPanel.isSearching ? Color.mPrimary : Color.mOnSurfaceVariant font.pointSize: Style.fontSizeS * scaling Layout.fillWidth: true } @@ -953,7 +955,7 @@ Popup { } } icon: "check" - enabled: fileManagerPanel.currentSelection.length > 0 + enabled: filePickerPanel.currentSelection.length > 0 onClicked: root.confirmSelection() } } @@ -964,7 +966,7 @@ Popup { target: root function onShouldResetSelectionChanged() { if (root.shouldResetSelection) { - fileManagerPanel.currentSelection = [] + filePickerPanel.currentSelection = [] root.shouldResetSelection = false } } From 4dcc9609d69ceae016d6618af6bbc9d88deb2163 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 21 Sep 2025 20:40:28 +0200 Subject: [PATCH 5/7] Add icons to TablerIcons, edit sizing of icons in FilePicker etc --- Commons/IconsSets/TablerIcons.qml | 21 ++++++++ Widgets/NFilePicker.qml | 90 +++++++++++++++++++------------ 2 files changed, 76 insertions(+), 35 deletions(-) diff --git a/Commons/IconsSets/TablerIcons.qml b/Commons/IconsSets/TablerIcons.qml index d4eab0fd..f726983a 100644 --- a/Commons/IconsSets/TablerIcons.qml +++ b/Commons/IconsSets/TablerIcons.qml @@ -119,6 +119,27 @@ Singleton { "bt-device-watch": "device-watch", "bt-device-speaker": "device-speaker", "bt-device-tv": "device-tv", + "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", "noctalia": "noctalia" } diff --git a/Widgets/NFilePicker.qml b/Widgets/NFilePicker.qml index 3bddd62b..5a9f19fe 100644 --- a/Widgets/NFilePicker.qml +++ b/Widgets/NFilePicker.qml @@ -50,30 +50,30 @@ Popup { case 'txt': case 'md': case 'log': - return 'file-text' + return 'filepicker-file-text' case 'jpg': case 'jpeg': case 'png': case 'gif': case 'bmp': case 'svg': - return 'photo' + return 'filepicker-photo' case 'mp4': case 'avi': case 'mkv': case 'mov': - return 'video' + return 'filepicker-video' case 'mp3': case 'wav': case 'flac': case 'ogg': - return 'music' + return 'filepicker-music' case 'zip': case 'tar': case 'gz': case 'rar': case '7z': - return 'archive' + return 'filepicker-archive' case 'pdf': return 'file-text' case 'doc': @@ -81,24 +81,24 @@ Popup { return 'file-text' case 'xls': case 'xlsx': - return 'table' + return 'filepicker-table' case 'ppt': case 'pptx': - return 'presentation' + return 'filepicker-presentation' case 'html': case 'htm': case 'css': case 'js': case 'json': case 'xml': - return 'code' + return 'filepicker-code' case 'exe': case 'app': case 'deb': case 'rpm': - return 'settings' + return 'filepicker-settings' default: - return 'file' + return 'filepicker-file' } } @@ -254,7 +254,7 @@ Popup { // Main icon NIcon { - icon: "folder" + icon: "filepicker-folder" color: Color.mPrimary font.pointSize: Style.fontSizeXXL * scaling } @@ -271,7 +271,7 @@ Popup { // Action buttons NIconButton { - icon: "refresh" + icon: "filepicker-refresh" tooltipText: "Refresh" Layout.alignment: Qt.AlignVCenter onClicked: { @@ -280,7 +280,7 @@ Popup { } NIconButton { - icon: "close" + icon: "filepicker-close" tooltipText: "Close" Layout.alignment: Qt.AlignVCenter onClicked: { @@ -313,7 +313,7 @@ Popup { // Navigation buttons NIconButton { - icon: "arrow-left" + icon: "filepicker-arrow-left" tooltipText: "Back" baseSize: Style.baseWidgetSize * 0.8 enabled: folderModel.folder.toString() !== "file://" + root.initialPath @@ -327,7 +327,7 @@ Popup { } NIconButton { - icon: "arrow-up" + icon: "filepicker-arrow-up" tooltipText: "Up" baseSize: Style.baseWidgetSize * 0.8 enabled: folderModel.folder.toString() !== "file:///" @@ -339,7 +339,7 @@ Popup { } NIconButton { - icon: "home" + icon: "filepicker-home" tooltipText: "Home" baseSize: Style.baseWidgetSize * 0.8 onClicked: { @@ -351,7 +351,7 @@ Popup { // View mode toggle NIconButton { - icon: filePickerPanel.viewMode ? "layout-grid" : "list" + icon: filePickerPanel.viewMode ? "filepicker-layout-grid" : "filepicker-list" tooltipText: filePickerPanel.viewMode ? "List View" : "Grid View" baseSize: Style.baseWidgetSize * 0.8 onClicked: { @@ -361,7 +361,7 @@ Popup { // Search toggle NIconButton { - icon: filePickerPanel.showSearchBar ? "x" : "search" + icon: filePickerPanel.showSearchBar ? "filepicker-x" : "filepicker-search" tooltipText: filePickerPanel.showSearchBar ? "Close Search" : "Search" baseSize: Style.baseWidgetSize * 0.8 onClicked: { @@ -426,7 +426,7 @@ Popup { spacing: Style.marginS * scaling NIcon { - icon: "search" + icon: "filepicker-search" color: Color.mOnSurfaceVariant font.pointSize: Style.fontSizeS * scaling } @@ -454,7 +454,7 @@ Popup { } NIconButton { - icon: "x" + icon: "filepicker-x" tooltipText: "Clear" baseSize: Style.baseWidgetSize * 0.6 visible: filePickerPanel.searchText.length > 0 @@ -589,10 +589,10 @@ Popup { // Selection background (covers entire item) Rectangle { anchors.fill: parent - color: isSelected ? Qt.alpha(Color.mSecondary, 0.15) : Color.transparent + color: Color.transparent radius: parent.radius border.color: isSelected ? Color.mSecondary : Color.mSurface - border.width: Math.max(1, Style.borderL * 1.5 * scaling) + border.width: Math.max(1, Style.borderL * scaling) Behavior on color { ColorAnimation { @@ -604,13 +604,17 @@ Popup { // Hover overlay Rectangle { anchors.fill: parent - color: Qt.alpha(Color.mPrimary, 0.1) - opacity: (mouseArea.containsMouse && !isSelected) ? 1.0 : 0 + color: (mouseArea.containsMouse && !isSelected) ? Color.mTertiary : Color.transparent radius: parent.radius - border.color: Qt.alpha(Color.mPrimary, 0.3) + border.color: (mouseArea.containsMouse && !isSelected) ? Color.mTertiary : Color.transparent border.width: Math.max(1, Style.borderS * scaling) - Behavior on opacity { - NumberAnimation { + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + Behavior on border.color { + ColorAnimation { duration: Style.animationFast } } @@ -664,7 +668,7 @@ Popup { visible: thumbnail.status === Image.Loading NIcon { - icon: "photo" + icon: "filepicker-photo" font.pointSize: Style.fontSizeL * scaling color: Color.mOnSurfaceVariant anchors.centerIn: parent @@ -673,9 +677,17 @@ Popup { } NIcon { - icon: isDirectory ? "folder" : root.getFileIcon(fileName) - font.pointSize: Style.fontSizeXXL * scaling - color: isDirectory ? Color.mPrimary : Color.mOnSurfaceVariant + icon: isDirectory ? "filepicker-folder" : root.getFileIcon(fileName) + font.pointSize: Style.fontSizeXXL * 2 * scaling + color: { + if (isSelected) { + return Color.mSecondary + } else if (mouseArea.containsMouse) { + return isDirectory ? Color.mOnTertiary : Color.mOnTertiary + } else { + return isDirectory ? Color.mPrimary : Color.mOnSurfaceVariant + } + } anchors.centerIn: parent visible: !iconContainer.isImage || thumbnail.status !== Image.Ready } @@ -694,7 +706,7 @@ Popup { visible: isSelected NIcon { - icon: "check" + icon: "filepicker-check" font.pointSize: Style.fontSizeS * scaling font.weight: Style.fontWeightBold color: Color.mOnSecondary @@ -705,7 +717,15 @@ Popup { NText { text: fileName - color: isSelected ? Color.mPrimary : Color.mOnSurface + 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 @@ -844,7 +864,7 @@ Popup { spacing: Style.marginM * scaling NIcon { - icon: isDirectory ? "folder" : root.getFileIcon(fileName) + icon: isDirectory ? "filepicker-folder" : root.getFileIcon(fileName) font.pointSize: Style.fontSizeL * scaling color: isDirectory ? (filePickerPanel.currentSelection.includes(filePath) ? Color.mOnSecondary : Color.mPrimary) : Color.mOnSurfaceVariant } @@ -954,7 +974,7 @@ Popup { return "Select" } } - icon: "check" + icon: "filepicker-check" enabled: filePickerPanel.currentSelection.length > 0 onClicked: root.confirmSelection() } From 385f4943aeac9c89f67d4ca50a7ad665e2989146 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 21 Sep 2025 20:52:47 +0200 Subject: [PATCH 6/7] NFilePicker: cleanup --- Commons/IconsSets/TablerIcons.qml | 1 + Widgets/NFilePicker.qml | 547 ++++++++++-------------------- 2 files changed, 181 insertions(+), 367 deletions(-) diff --git a/Commons/IconsSets/TablerIcons.qml b/Commons/IconsSets/TablerIcons.qml index f726983a..adb79ce6 100644 --- a/Commons/IconsSets/TablerIcons.qml +++ b/Commons/IconsSets/TablerIcons.qml @@ -140,6 +140,7 @@ Singleton { "filepicker-code": "code", "filepicker-settings": "settings", "filepicker-file": "file", + "filepicker-text": "file-text", "noctalia": "noctalia" } diff --git a/Widgets/NFilePicker.qml b/Widgets/NFilePicker.qml index 5a9f19fe..25c7f13c 100644 --- a/Widgets/NFilePicker.qml +++ b/Widgets/NFilePicker.qml @@ -12,16 +12,14 @@ import "../Helpers/FuzzySort.js" as FuzzySort Popup { id: root - // Public properties + // Properties property string title: "File Picker" property string initialPath: Quickshell.env("HOME") || "/home" property bool selectFiles: true property bool selectFolders: true - property var nameFilters: ["*"] // Default to show all files + property var nameFilters: ["*"] property bool showDirs: true property real scaling: 1.0 - - // Selected files/folders property var selectedPaths: [] property string currentPath: initialPath property bool shouldResetSelection: false @@ -32,131 +30,100 @@ Popup { 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 === "") { + if (!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 'filepicker-file-text' - case 'jpg': - case 'jpeg': - case 'png': - case 'gif': - case 'bmp': - case 'svg': - return 'filepicker-photo' - case 'mp4': - case 'avi': - case 'mkv': - case 'mov': - return 'filepicker-video' - case 'mp3': - case 'wav': - case 'flac': - case 'ogg': - return 'filepicker-music' - case 'zip': - case 'tar': - case 'gz': - case 'rar': - case '7z': - return 'filepicker-archive' - case 'pdf': - return 'file-text' - case 'doc': - case 'docx': - return 'file-text' - case 'xls': - case 'xlsx': - return 'filepicker-table' - case 'ppt': - case 'pptx': - return 'filepicker-presentation' - case 'html': - case 'htm': - case 'css': - case 'js': - case 'json': - case 'xml': - return 'filepicker-code' - case 'exe': - case 'app': - case 'deb': - case 'rpm': - return 'filepicker-settings' - default: - return 'filepicker-file' + 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' } 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)) + 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) { + if (filePickerPanel.currentSelection.length === 0) return - } root.selectedPaths = filePickerPanel.currentSelection + const path = filePickerPanel.currentSelection[0] if (filePickerPanel.currentSelection.length === 1) { - var path = filePickerPanel.currentSelection[0] - if (root.selectFiles && !root.selectFolders) { + const isDir = folderModel.get(folderModel.indexOf(path), "fileIsDir") + if (root.selectFiles && !root.selectFolders) root.fileSelected(path) - } else if (root.selectFolders && !root.selectFiles) { + 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 + isDir ? root.folderSelected(path) : root.fileSelected(path) } else { root.filesSelected(filePickerPanel.currentSelection) } - root.close() } - // Function to update the filtered model function updateFilteredModel() { filteredModel.clear() - var searchText = filePickerPanel.filterText.toLowerCase() + const searchText = filePickerPanel.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") + const fileName = folderModel.get(i, "fileName") + const filePath = folderModel.get(i, "filePath") + const fileIsDir = folderModel.get(i, "fileIsDir") + const fileSize = folderModel.get(i, "fileSize") - // In folder selection mode, only show directories - if (root.selectFolders && !root.selectFiles && !fileIsDir) { + if (root.selectFolders && !root.selectFiles && !fileIsDir) continue - } - - // If no search text or file name contains search text if (searchText === "" || fileName.toLowerCase().includes(searchText)) { filteredModel.append({ "fileName": fileName, @@ -168,29 +135,6 @@ Popup { } } - // 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 @@ -212,77 +156,56 @@ Popup { 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 + focus: true + Keys.onPressed: event => { if (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_F) { filePickerPanel.showSearchBar = !filePickerPanel.showSearchBar - if (filePickerPanel.showSearchBar) { - // Focus the search input when opening - Qt.callLater(() => { - searchInput.forceActiveFocus() - }) - } + if (filePickerPanel.showSearchBar) + Qt.callLater(() => searchInput.forceActiveFocus()) event.accepted = true } else if (event.key === Qt.Key_Escape && filePickerPanel.showSearchBar) { - // Close search bar on Escape filePickerPanel.showSearchBar = false filePickerPanel.searchText = "" - filePickerPanel.isSearching = false filePickerPanel.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) + // Header RowLayout { Layout.fillWidth: true spacing: Style.marginM * scaling - // Main icon NIcon { icon: "filepicker-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: "filepicker-refresh" tooltipText: "Refresh" - Layout.alignment: Qt.AlignVCenter - onClicked: { - folderModel.refresh() - } + onClicked: folderModel.refresh() } - NIconButton { icon: "filepicker-close" tooltipText: "Close" - Layout.alignment: Qt.AlignVCenter onClicked: { root.cancelled() root.close() @@ -290,7 +213,6 @@ Popup { } } - // Divider NDivider { Layout.fillWidth: true } @@ -305,34 +227,17 @@ Popup { border.width: Math.max(1, Style.borderS * scaling) RowLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter + anchors.fill: parent anchors.margins: Style.marginS * scaling spacing: Style.marginS * scaling - // Navigation buttons - NIconButton { - icon: "filepicker-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: "filepicker-arrow-up" tooltipText: "Up" baseSize: Style.baseWidgetSize * 0.8 enabled: folderModel.folder.toString() !== "file:///" onClicked: { - var parentPath = folderModel.parentFolder.toString().replace("file://", "") + const parentPath = folderModel.parentFolder.toString().replace("file://", "") folderModel.folder = "file://" + parentPath root.currentPath = parentPath } @@ -343,23 +248,19 @@ Popup { tooltipText: "Home" baseSize: Style.baseWidgetSize * 0.8 onClicked: { - var homePath = Quickshell.env("HOME") || "/home" + const homePath = Quickshell.env("HOME") || "/home" folderModel.folder = "file://" + homePath root.currentPath = homePath } } - // View mode toggle NIconButton { icon: filePickerPanel.viewMode ? "filepicker-layout-grid" : "filepicker-list" tooltipText: filePickerPanel.viewMode ? "List View" : "Grid View" baseSize: Style.baseWidgetSize * 0.8 - onClicked: { - filePickerPanel.viewMode = !filePickerPanel.viewMode - } + onClicked: filePickerPanel.viewMode = !filePickerPanel.viewMode } - // Search toggle NIconButton { icon: filePickerPanel.showSearchBar ? "filepicker-x" : "filepicker-search" tooltipText: filePickerPanel.showSearchBar ? "Close Search" : "Search" @@ -367,48 +268,39 @@ Popup { onClicked: { filePickerPanel.showSearchBar = !filePickerPanel.showSearchBar if (!filePickerPanel.showSearchBar) { - // Clear search when closing filePickerPanel.searchText = "" - filePickerPanel.isSearching = false filePickerPanel.filterText = "" root.updateFilteredModel() } } } - // Location input NTextInput { id: locationInput text: root.currentPath placeholderText: "Enter path..." Layout.fillWidth: true - onEditingFinished: { - var newPath = text.trim() + const 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) { + if (!locationInput.activeFocus) locationInput.text = root.currentPath - } } } } } } - // Search bar (appears when search is toggled) + // Search bar Rectangle { Layout.fillWidth: true Layout.preferredHeight: 45 * scaling @@ -419,9 +311,7 @@ Popup { visible: filePickerPanel.showSearchBar RowLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter + anchors.fill: parent anchors.margins: Style.marginS * scaling spacing: Style.marginS * scaling @@ -430,29 +320,23 @@ Popup { 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.isSearching = text.length > 0 filePickerPanel.filterText = text root.updateFilteredModel() } - Keys.onEscapePressed: { filePickerPanel.showSearchBar = false filePickerPanel.searchText = "" - filePickerPanel.isSearching = false filePickerPanel.filterText = "" root.updateFilteredModel() } } - NIconButton { icon: "filepicker-x" tooltipText: "Clear" @@ -461,7 +345,6 @@ Popup { onClicked: { searchInput.text = "" filePickerPanel.searchText = "" - filePickerPanel.isSearching = false filePickerPanel.filterText = "" root.updateFilteredModel() } @@ -487,16 +370,12 @@ Popup { showHidden: true sortField: FolderListModel.Name sortReversed: false - onFolderChanged: { root.currentPath = folder.toString().replace("file://", "") filePickerPanel.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") @@ -507,11 +386,47 @@ Popup { } } - // Filtered model for search functionality 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 @@ -532,47 +447,12 @@ Popup { 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: 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 - } - } - } - } + ScrollBar.vertical: scrollBarComponent.createObject(gridView, { + "parent": gridView, + "x": gridView.mirrored ? 0 : gridView.width - width, + "y": 0, + "height": gridView.height + }) delegate: Rectangle { id: gridItem @@ -581,19 +461,14 @@ Popup { 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: filePickerPanel.currentSelection.includes(filePath) + property bool isSelected: filePickerPanel.currentSelection.includes(model.filePath) - // Selection background (covers entire item) 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 @@ -601,7 +476,6 @@ Popup { } } - // Hover overlay Rectangle { anchors.fill: parent color: (mouseArea.containsMouse && !isSelected) ? Color.mTertiary : Color.transparent @@ -631,42 +505,35 @@ Popup { Layout.preferredHeight: Math.round(gridView.itemSize * 0.67) color: Color.transparent - // Check if file is an image property bool isImage: { - if (isDirectory) + if (model.fileIsDir) return false - var ext = fileName.split('.').pop().toLowerCase() + const ext = model.fileName.split('.').pop().toLowerCase() return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'].includes(ext) } - // Show thumbnail for images, icon for others Image { id: thumbnail anchors.fill: parent anchors.margins: Style.marginXS * scaling - source: iconContainer.isImage ? "file://" + filePath : "" + source: iconContainer.isImage ? "file://" + model.filePath : "" fillMode: Image.PreserveAspectFit visible: iconContainer.isImage && status === Image.Ready - smooth: false // Disable smooth for faster rendering + smooth: false cache: true - asynchronous: true // Load images asynchronously - sourceSize.width: 120 * scaling // Limit image size for faster loading + asynchronous: true + sourceSize.width: 120 * scaling sourceSize.height: 120 * scaling - - // Fallback to icon if image fails to load or takes too long onStatusChanged: { - if (status === Image.Error) { + if (status === Image.Error) visible = false - } } - // Show loading indicator while image loads 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 @@ -677,22 +544,20 @@ Popup { } NIcon { - icon: isDirectory ? "filepicker-folder" : root.getFileIcon(fileName) + icon: model.fileIsDir ? "filepicker-folder" : root.getFileIcon(model.fileName) font.pointSize: Style.fontSizeXXL * 2 * scaling color: { - if (isSelected) { + if (isSelected) return Color.mSecondary - } else if (mouseArea.containsMouse) { - return isDirectory ? Color.mOnTertiary : Color.mOnTertiary - } else { - return isDirectory ? Color.mPrimary : Color.mOnSurfaceVariant - } + 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 } - // Selection indicator (like WallpaperSelector) Rectangle { anchors.top: parent.top anchors.right: parent.right @@ -704,7 +569,6 @@ Popup { border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) visible: isSelected - NIcon { icon: "filepicker-check" font.pointSize: Style.fontSizeS * scaling @@ -716,15 +580,14 @@ Popup { } NText { - text: fileName + text: model.fileName color: { - if (isSelected) { + if (isSelected) return Color.mSecondary - } else if (mouseArea.containsMouse) { + else if (mouseArea.containsMouse) return Color.mOnTertiary - } else { + else return Color.mOnSurface - } } font.pointSize: Style.fontSizeS * scaling font.weight: isSelected ? Style.fontWeightBold : Style.fontWeightRegular @@ -744,34 +607,33 @@ Popup { onClicked: mouse => { if (mouse.button === Qt.LeftButton) { - if (isDirectory) { + if (model.fileIsDir) { if (root.selectFolders && !root.selectFiles) { - filePickerPanel.currentSelection = [filePath] + filePickerPanel.currentSelection = [model.filePath] } else { - folderModel.folder = "file://" + filePath - root.currentPath = filePath + folderModel.folder = "file://" + model.filePath + root.currentPath = model.filePath } } else { - if (root.selectFiles) { - filePickerPanel.currentSelection = [filePath] - } + if (root.selectFiles) + filePickerPanel.currentSelection = [model.filePath] } } } onDoubleClicked: mouse => { if (mouse.button === Qt.LeftButton) { - if (isDirectory) { + if (model.fileIsDir) { if (root.selectFolders && !root.selectFiles) { - filePickerPanel.currentSelection = [filePath] + filePickerPanel.currentSelection = [model.filePath] root.confirmSelection() } else { - folderModel.folder = "file://" + filePath - root.currentPath = filePath + folderModel.folder = "file://" + model.filePath + root.currentPath = model.filePath } } else { if (root.selectFiles) { - filePickerPanel.currentSelection = [filePath] + filePickerPanel.currentSelection = [model.filePath] root.confirmSelection() } } @@ -790,67 +652,25 @@ Popup { visible: !filePickerPanel.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: 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 - } - } - } - } + ScrollBar.vertical: scrollBarComponent.createObject(listView, { + "parent": listView, + "x": listView.mirrored ? 0 : listView.width - width, + "y": 0, + "height": listView.height + }) delegate: Rectangle { id: listItem width: listView.width height: 40 * scaling color: { - if (filePickerPanel.currentSelection.includes(filePath)) { + if (filePickerPanel.currentSelection.includes(model.filePath)) return Color.mSecondary - } - if (mouseArea.containsMouse) { + 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 @@ -864,25 +684,25 @@ Popup { spacing: Style.marginM * scaling NIcon { - icon: isDirectory ? "filepicker-folder" : root.getFileIcon(fileName) + icon: model.fileIsDir ? "filepicker-folder" : root.getFileIcon(model.fileName) font.pointSize: Style.fontSizeL * scaling - color: isDirectory ? (filePickerPanel.currentSelection.includes(filePath) ? Color.mOnSecondary : Color.mPrimary) : Color.mOnSurfaceVariant + color: model.fileIsDir ? (filePickerPanel.currentSelection.includes(model.filePath) ? Color.mOnSecondary : Color.mPrimary) : Color.mOnSurfaceVariant } NText { - text: fileName - color: filePickerPanel.currentSelection.includes(filePath) ? Color.mOnSecondary : Color.mOnSurface + text: model.fileName + color: filePickerPanel.currentSelection.includes(model.filePath) ? Color.mOnSecondary : Color.mOnSurface font.pointSize: Style.fontSizeM * scaling - font.weight: filePickerPanel.currentSelection.includes(filePath) ? Style.fontWeightBold : Style.fontWeightRegular + font.weight: filePickerPanel.currentSelection.includes(model.filePath) ? Style.fontWeightBold : Style.fontWeightRegular Layout.fillWidth: true elide: Text.ElideRight } NText { - text: isDirectory ? "" : root.formatFileSize(model.fileSize) - color: filePickerPanel.currentSelection.includes(filePath) ? Color.mOnSecondary : Color.mOnSurfaceVariant + text: model.fileIsDir ? "" : root.formatFileSize(model.fileSize) + color: filePickerPanel.currentSelection.includes(model.filePath) ? Color.mOnSecondary : Color.mOnSurfaceVariant font.pointSize: Style.fontSizeS * scaling - visible: !isDirectory + visible: !model.fileIsDir Layout.preferredWidth: implicitWidth } } @@ -895,34 +715,33 @@ Popup { onClicked: mouse => { if (mouse.button === Qt.LeftButton) { - if (isDirectory) { + if (model.fileIsDir) { if (root.selectFolders && !root.selectFiles) { - filePickerPanel.currentSelection = [filePath] + filePickerPanel.currentSelection = [model.filePath] } else { - folderModel.folder = "file://" + filePath - root.currentPath = filePath + folderModel.folder = "file://" + model.filePath + root.currentPath = model.filePath } } else { - if (root.selectFiles) { - filePickerPanel.currentSelection = [filePath] - } + if (root.selectFiles) + filePickerPanel.currentSelection = [model.filePath] } } } onDoubleClicked: mouse => { if (mouse.button === Qt.LeftButton) { - if (isDirectory) { + if (model.fileIsDir) { if (root.selectFolders && !root.selectFiles) { - filePickerPanel.currentSelection = [filePath] + filePickerPanel.currentSelection = [model.filePath] root.confirmSelection() } else { - folderModel.folder = "file://" + filePath - root.currentPath = filePath + folderModel.folder = "file://" + model.filePath + root.currentPath = model.filePath } } else { if (root.selectFiles) { - filePickerPanel.currentSelection = [filePath] + filePickerPanel.currentSelection = [model.filePath] root.confirmSelection() } } @@ -933,15 +752,14 @@ Popup { } } - // Status and actions + // Footer RowLayout { Layout.fillWidth: true spacing: Style.marginM * scaling - // Status text NText { text: { - if (filePickerPanel.isSearching) { + 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" @@ -949,12 +767,11 @@ Popup { return filteredModel.count + " items" } } - color: filePickerPanel.isSearching ? Color.mPrimary : Color.mOnSurfaceVariant + color: filePickerPanel.searchText.length > 0 ? Color.mPrimary : Color.mOnSurfaceVariant font.pointSize: Style.fontSizeS * scaling Layout.fillWidth: true } - // Action buttons NButton { text: "Cancel" outlined: true @@ -966,13 +783,12 @@ Popup { NButton { text: { - if (root.selectFolders && !root.selectFiles) { + if (root.selectFolders && !root.selectFiles) return "Select Folder" - } else if (root.selectFiles && !root.selectFolders) { + else if (root.selectFiles && !root.selectFolders) return "Select File" - } else { + else return "Select" - } } icon: "filepicker-check" enabled: filePickerPanel.currentSelection.length > 0 @@ -981,7 +797,6 @@ Popup { } } - // Watch for selection reset flag Connections { target: root function onShouldResetSelectionChanged() { @@ -993,10 +808,8 @@ Popup { } Component.onCompleted: { - // Ensure we have a valid path - if (!root.currentPath || root.currentPath === "") { + if (!root.currentPath) root.currentPath = root.initialPath - } folderModel.folder = "file://" + root.currentPath } } From 3684c87f8c49e1addf7c63fb9e3f34109c59a8e9 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 21 Sep 2025 21:32:57 +0200 Subject: [PATCH 7/7] WallpaperTab: fix width of NInputAction for individual wallpapers NFilePicker: reverse grid/listview button --- Modules/SettingsPanel/Tabs/WallpaperTab.qml | 30 ++++++++++----------- Widgets/NFilePicker.qml | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Modules/SettingsPanel/Tabs/WallpaperTab.qml b/Modules/SettingsPanel/Tabs/WallpaperTab.qml index b4367c8a..a8697990 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperTab.qml @@ -72,27 +72,27 @@ ColumnLayout { spacing: Style.marginM * scaling Repeater { model: Quickshell.screens || [] - delegate: RowLayout { + delegate: ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginS * scaling + NText { text: (modelData.name || "Unknown") color: Color.mPrimary font.weight: Style.fontWeightBold - 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 + font.pointSize: Style.fontSizeM * scaling } - NIconButton { - icon: "folder-open" - tooltipText: "Browse for wallpaper folder" - baseSize: Style.baseWidgetSize * 0.8 - Layout.alignment: Qt.AlignBottom - onClicked: { + NInputButton { + text: WallpaperService.getMonitorDirectory(modelData.name) + buttonIcon: "folder-open" + buttonTooltip: "Browse for wallpaper folder" + Layout.fillWidth: true + + onInputEditingFinished: { + WallpaperService.setMonitorDirectory(modelData.name, text) + } + onButtonClicked: { openMonitorFileManager(modelData.name) } } diff --git a/Widgets/NFilePicker.qml b/Widgets/NFilePicker.qml index 25c7f13c..39459404 100644 --- a/Widgets/NFilePicker.qml +++ b/Widgets/NFilePicker.qml @@ -255,7 +255,7 @@ Popup { } NIconButton { - icon: filePickerPanel.viewMode ? "filepicker-layout-grid" : "filepicker-list" + 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