From c8ce2cc2fe4979e9905cc4ae7d13fa7b6c3a16a1 Mon Sep 17 00:00:00 2001 From: david-swift Date: Fri, 17 Oct 2025 12:47:39 +0200 Subject: [PATCH] Add support for folder importers --- Sources/Adwaita/AnyView+.swift | 34 ++++++--- Sources/CAdw/shim.h | 20 ++++++ Sources/Core/View/Dialogs/FileDialog.swift | 82 ++++++++++++++++------ 3 files changed, 107 insertions(+), 29 deletions(-) diff --git a/Sources/Adwaita/AnyView+.swift b/Sources/Adwaita/AnyView+.swift index c455690..db0f0c6 100644 --- a/Sources/Adwaita/AnyView+.swift +++ b/Sources/Adwaita/AnyView+.swift @@ -133,7 +133,6 @@ extension AnyView { /// - open: The signal to open the dialog. /// - initialFolder: The URL to the folder open when being opened. /// - extensions: The accepted file extensions. - /// - folders: Whether folders are accepted. /// - onOpen: Run this when a file for importing has been chosen. /// - onClose: Run this when the user cancelled the action. public func fileImporter( @@ -144,14 +143,34 @@ extension AnyView { onClose: @escaping () -> Void ) -> AnyView { FileDialog( - importer: true, + type: .importer(folder: false, extensions: extensions), open: open, child: self, result: onOpen, cancel: onClose, - initialFolder: initialFolder, - initialName: nil, - extensions: extensions + initialFolder: initialFolder + ) + } + + /// Create an importer file dialog for folders. + /// - Parameters: + /// - open: The signal to open the dialog. + /// - initialFolder: The URL to the folder open when being opened. + /// - onOpen: Run this when a file for importing has been chosen. + /// - onClose: Run this when the user cancelled the action. + public func folderImporter( + open: Signal, + initialFolder: URL? = nil, + onOpen: @escaping (URL) -> Void, + onClose: @escaping () -> Void + ) -> AnyView { + FileDialog( + type: .importer(folder: true, extensions: nil), + open: open, + child: self, + result: onOpen, + cancel: onClose, + initialFolder: initialFolder ) } @@ -170,13 +189,12 @@ extension AnyView { onClose: @escaping () -> Void ) -> AnyView { FileDialog( - importer: false, + type: .exporter(initialName: initialName), open: open, child: self, result: onSave, cancel: onClose, - initialFolder: initialFolder, - initialName: initialName + initialFolder: initialFolder ) } diff --git a/Sources/CAdw/shim.h b/Sources/CAdw/shim.h index 8f356b2..80f626b 100644 --- a/Sources/CAdw/shim.h +++ b/Sources/CAdw/shim.h @@ -44,6 +44,26 @@ gtui_filedialog_open (uint64_t dialog, uint64_t data, uint64_t window) gtk_file_dialog_open (dialog, window, NULL, G_CALLBACK (gtui_filedialog_open_finish), (void *)data); } +static void +gtui_filedialog_open_folder_finish (uint64_t dialog, uint64_t result, uint64_t data) +{ + GFile *file = gtk_file_dialog_select_folder_finish (dialog, result, NULL); + if (file != NULL) { + const char *path = g_file_peek_path (file); + g_object_unref (file); + filedialog_on_open_cb (dialog, path, data); + } else { + filedialog_on_open_cb (dialog, NULL, data); + } +} + +static void +gtui_filedialog_open_folder (uint64_t dialog, uint64_t data, uint64_t window) +{ + swift_retain (data); + gtk_file_dialog_select_folder (dialog, window, NULL, G_CALLBACK (gtui_filedialog_open_folder_finish), (void *)data); +} + static void gtui_alertdialog_cb (uint64_t dialog, uint64_t result, uint64_t data) { diff --git a/Sources/Core/View/Dialogs/FileDialog.swift b/Sources/Core/View/Dialogs/FileDialog.swift index 0a39936..dc8836d 100644 --- a/Sources/Core/View/Dialogs/FileDialog.swift +++ b/Sources/Core/View/Dialogs/FileDialog.swift @@ -11,54 +11,92 @@ import Foundation /// A structure representing a file dialog window. public struct FileDialog: AdwaitaWidget { - /// Whether the dialog is an importer. - var importer: Bool + /// The dialog type. + var type: DialogType /// Whether the dialog should open. var open: Signal /// The dialog's child. var child: AnyView /// The initial folder. var initialFolder: URL? - /// The initial file name for the file exporter. - var initialName: String? - /// The accepted extensions for the file importer. - var extensions: [String]? /// The closure to run when the import or export is successful. var result: (URL) -> Void /// The closure to run when the import or export is not successful. var cancel: () -> Void - // swiftlint:disable function_default_parameter_at_end /// Initialize the file dialog wrapper. /// - Parameters: - /// - importer: Whether it is an importer. + /// - type: The dialog type. /// - open: The signal. /// - child: The wrapped view. - /// - initialFolder: The initial URL. - /// - initialName: The initial name. - /// - extensions: The file extensions. /// - result: Run when the import or export succeeds. /// - cancel: Run when the import or export is not successful. + /// - initialFolder: The initial folder. public init( - importer: Bool = true, + type: DialogType, `open`: Signal, child: AnyView, result: @escaping (URL) -> Void, cancel: @escaping () -> Void, initialFolder: URL? = nil, - initialName: String? = nil, - extensions: [String]? = nil ) { - self.importer = importer + self.type = type self.open = open self.child = child self.result = result self.cancel = cancel self.initialFolder = initialFolder - self.initialName = initialName - self.extensions = extensions } - // swiftlint:enable function_default_parameter_at_end + + /// The different types of dialogs and their properties. + public enum DialogType { + + /// An importer dialog. + case importer(folder: Bool, extensions: [String]?) + /// An exporter dialog. + case exporter(initialName: String?) + + /// Whether the dialog is an importer. + var isImporter: Bool { + switch self { + case .importer: + true + default: + false + } + } + + /// The supported extensions. + var extensions: [String]? { + switch self { + case let .importer(folder: _, extensions: extensions): + extensions + default: + nil + } + } + + /// Whether to import folders. + var folder: Bool { + switch self { + case let .importer(folder: folder, extensions: _): + folder + default: + false + } + } + + /// The initial name. + var initialName: String? { + switch self { + case let .exporter(initialName: initialName): + initialName + default: + nil + } + } + + } /// The view storage. /// - Parameters: @@ -94,10 +132,10 @@ public struct FileDialog: AdwaitaWidget { var unref: [OpaquePointer?] = [] let pointer = gtk_file_dialog_new() unref.append(pointer) - if let initialName { + if let initialName = self.type.initialName { gtk_file_dialog_set_initial_name(pointer, initialName) } - if let extensions { + if let extensions = self.type.extensions { let filter = gtk_file_filter_new() for name in extensions { gtk_file_filter_add_suffix(filter, name) @@ -125,7 +163,9 @@ public struct FileDialog: AdwaitaWidget { storage.fields["callbacks"] = callbacks let ptr = UInt64(Int(bitPattern: pointer)) let window = UInt64(Int(bitPattern: gtk_widget_get_root(mainStorage.opaquePointer?.cast()))) - if importer { + if self.type.isImporter && self.type.folder { + gtui_filedialog_open_folder(ptr, unsafeBitCast(callbacks, to: UInt64.self), window) + } else if self.type.isImporter { gtui_filedialog_open(ptr, unsafeBitCast(callbacks, to: UInt64.self), window) } else { gtui_filedialog_save(ptr, unsafeBitCast(callbacks, to: UInt64.self), window)