david-swift 04c77831b5
Some checks are pending
Deploy Docs / publish (push) Waiting to run
SwiftLint / SwiftLint (push) Waiting to run
Remove Core library
2025-10-30 21:36:13 +01:00

254 lines
7.9 KiB
Swift

//
// FileDialog.swift
// Adwaita
//
// Created by david-swift on 12.08.24.
//
import CAdw
import Foundation
/// A structure representing a file dialog window.
struct FileDialog: AdwaitaWidget {
/// 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 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
/// Initialize the file dialog wrapper.
/// - Parameters:
/// - type: The dialog type.
/// - open: The signal.
/// - child: The wrapped view.
/// - result: Run when the import or export succeeds.
/// - cancel: Run when the import or export is not successful.
/// - initialFolder: The initial folder.
init(
type: DialogType,
`open`: Signal,
child: AnyView,
result: @escaping (URL) -> Void,
cancel: @escaping () -> Void,
initialFolder: URL? = nil,
) {
self.type = type
self.open = open
self.child = child
self.result = result
self.cancel = cancel
self.initialFolder = initialFolder
}
/// The different types of dialogs and their properties.
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:
/// - modifiers: Modify views before being updated.
/// - type: The view render data type.
/// - Returns: The view storage.
func container<Data>(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData {
let child = child.storage(data: data, type: type)
let storage = ViewStorage(child.opaquePointer, content: [.mainContent: [child]])
update(storage, data: data, updateProperties: true, type: type)
return storage
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - modifiers: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The view render data type.
func update<Data>(
_ storage: ViewStorage,
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
storage.fields["result"] = result
storage.fields["cancel"] = cancel
guard let mainStorage = storage.content[.mainContent]?.first else {
return
}
child.updateStorage(mainStorage, data: data, updateProperties: updateProperties, type: type)
if open.update, storage.fields["callbacks"] == nil {
var unref: [OpaquePointer?] = []
let pointer = gtk_file_dialog_new()
unref.append(pointer)
if let initialName = self.type.initialName {
gtk_file_dialog_set_initial_name(pointer, initialName)
}
if let extensions = self.type.extensions {
let filter = gtk_file_filter_new()
for name in extensions {
gtk_file_filter_add_suffix(filter, name)
}
gtk_file_dialog_set_default_filter(pointer, filter)
unref.append(filter)
} else {
gtk_file_dialog_set_default_filter(pointer, nil)
}
if let initialFolder {
let file = g_file_new_for_path(initialFolder.absoluteString)
gtk_file_dialog_set_initial_folder(pointer, file)
unref.append(file)
}
let callbacks = AdwaitaFileDialog()
let unrefClosure = {
for ref in unref {
g_object_unref(ref?.cast())
}
storage.fields["callbacks"] = nil
}
callbacks.onResult = { (storage.fields["result"] as? (URL) -> Void)?($0); unrefClosure() }
callbacks.onCancel = { (storage.fields["cancel"] as? () -> Void)?(); unrefClosure() }
callbacks.reset = { storage.fields["callbacks"] = nil }
storage.fields["callbacks"] = callbacks
let ptr = UInt64(Int(bitPattern: pointer))
let window = UInt64(Int(bitPattern: gtk_widget_get_root(mainStorage.opaquePointer?.cast())))
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)
}
}
}
}
/// An Adwaita file dialog window callback.
class AdwaitaFileDialog {
/// A closure triggered on selecting a file in the dialog.
var onResult: (URL) -> Void = { _ in }
/// A closure triggered when the dialog is canceled.
var onCancel: () -> Void = { }
/// Reset the file dialog.
var reset: () -> Void = { }
/// Initialize the window callback.
init() { }
deinit { print("DEINIT fd") }
/// Run this when a file gets opened.
/// - Parameter path: The file path.
func onOpen(_ path: String) {
let url = URL(fileURLWithPath: path)
onResult(url)
reset()
}
/// Run this when a file gets saved.
/// - Parameter path: The file path.
func onSave(_ path: String) {
let url = URL(fileURLWithPath: path)
onResult(url)
reset()
}
/// Run this when the user cancels the action.
func onClose() {
onCancel()
reset()
}
}
/// Run when a file should be opened.
/// - Parameters:
/// - ptr: The pointer.
/// - file: The path to the file.
/// - userData: The file dialog data.
@_cdecl("filedialog_on_open_cb")
func filedialog_on_open_cb(
ptr: UnsafeMutableRawPointer,
file: UnsafePointer<CChar>?,
userData: UnsafeMutableRawPointer
) {
let dialog = Unmanaged<AdwaitaFileDialog>.fromOpaque(userData).takeUnretainedValue()
if let file {
dialog.onOpen(.init(cString: file))
} else {
dialog.onClose()
}
}
/// Run when a file should be saved.
/// - Parameters:
/// - ptr: The pointer.
/// - file: The path to the file.
/// - userData: The file dialog data.
@_cdecl("filedialog_on_save_cb")
func filedialog_on_save_cb(
ptr: UnsafeMutableRawPointer,
file: UnsafePointer<CChar>?,
userData: UnsafeMutableRawPointer
) {
let dialog = Unmanaged<AdwaitaFileDialog>.fromOpaque(userData).takeUnretainedValue()
if let file {
dialog.onSave(.init(cString: file))
} else {
dialog.onClose()
}
}