Make FileDialog a window

This commit is contained in:
david-swift 2023-12-09 13:08:11 +01:00
parent 2249ef2d6a
commit 083b352348
16 changed files with 455 additions and 115 deletions

View File

@ -10,6 +10,7 @@
- [Widget](protocols/Widget.md)
- [WindowScene](protocols/WindowScene.md)
- [WindowSceneGroup](protocols/WindowSceneGroup.md)
- [WindowType](protocols/WindowType.md)
## Structs
@ -20,6 +21,7 @@
- [Clamp](structs/Clamp.md)
- [ContentModifier](structs/ContentModifier.md)
- [EitherView](structs/EitherView.md)
- [FileDialog](structs/FileDialog.md)
- [HStack](structs/HStack.md)
- [HeaderBar](structs/HeaderBar.md)
- [InspectorWrapper](structs/InspectorWrapper.md)
@ -59,6 +61,8 @@
- [App](extensions/App.md)
- [Array](extensions/Array.md)
- [GTUIWindow](extensions/GTUIWindow.md)
- [Libadwaita.FileDialog](extensions/Libadwaita.FileDialog.md)
- [MenuItem](extensions/MenuItem.md)
- [MenuItemGroup](extensions/MenuItemGroup.md)
- [NativeWidgetPeer](extensions/NativeWidgetPeer.md)

View File

@ -19,21 +19,17 @@ Whether the reference to the window should disappear in the next update.
### `window`
The GTUI window.
The window.
### `view`
The content's storage.
### `fileDialog`
The file dialog for the window.
## Methods
### `init(id:window:view:)`
Initialize a window storage.
- Parameters:
- id: The window's identifier.
- window: The GTUI window.
- window: The window.
- view: The content's storage.

View File

@ -0,0 +1,9 @@
**EXTENSION**
# `GTUIWindow`
## Methods
### `setParentWindow(_:)`
Set the window's parent window.
- Parameter parent: The parent window.

View File

@ -0,0 +1,48 @@
**EXTENSION**
# `Libadwaita.FileDialog`
## Properties
### `importer`
An ID for the importer field.
### `folder`
An ID for the folder field.
### `result`
An ID for the result field.
### `cancel`
An ID for the cancel field.
### `isImporter`
Whether the file dialog is an importer.
### `folder`
The selected folder in the file dialog.
### `onResult`
A closure triggered on selecting a file in the dialog.
### `onCancel`
A closure triggered when the dialog is canceled.
## Methods
### `setParentWindow(_:)`
Set the window's parent window.
- Parameter parent: The parent window.
Currently not implemented.
### `show()`
Display the file dialog.

View File

@ -0,0 +1,20 @@
**PROTOCOL**
# `WindowType`
A window type.
## Properties
### `fields`
A dictionary for custom data.
## Methods
### `setParentWindow(_:)`
Set a parent window.
- Parameter parent: The parent window.
### `show()`
Show the window.

View File

@ -0,0 +1,90 @@
**STRUCT**
# `FileDialog`
A structure representing a file dialog window.
## Properties
### `id`
The window's identifier.
### `importer`
Whether the window is an importer.
### `open`
Whether an instance of the window type should be opened when the app is starting up.
### `parentID`
The identifier of the window's parent.
### `appShortcuts`
The keyboard shortcuts on the app level.
### `initialFolder`
The initial folder.
### `initialName`
The initial file name for the file exporter.
### `extensions`
The accepted extensions for the file importer.
### `folders`
Whether folders are accepted in the file importer.
### `result`
The closure to run when the import or export is successful.
### `cancel`
The closure to run when the import or export is not successful.
## Methods
### `init(importer:initialFolder:extensions:folders:onOpen:onClose:)`
Create an importer file dialog window.
- Parameters:
- importer: The window's identifier.
- 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.
### `init(exporter:initialFolder:initialName:onSave:onClose:)`
Create an exporter file dialog window.
- Parameters:
- exporter: The window's identifier.
- initialFolder: The URL to the folder open when being opened.
- initialName: The default file name.
- onSave: Run this when a path for exporting has been chosen.
- onClose: Run this when the user cancelled the action.
### `createWindow(app:)`
Get the storage for the window.
- Parameter app: The application.
- Returns: The storage.
### `update(_:app:)`
Update a window.
- Parameters:
- storage: The storage to update.
- app: The application.
### `update(window:)`
Update the window.
- Parameter window: The window.

View File

@ -9,6 +9,10 @@ A type that signalizes an action.
An action is signalized by toggling a boolean to `true` and back to `false`.
### `id`
A signal has a unique identifier.
### `update`
Whether the action has caused an update.

View File

@ -31,50 +31,6 @@ The keyboard shortcuts.
The keyboard shortcuts on the app level.
### `fileImporter`
The signal for the file importer.
### `fileExporter`
The signal for the file exporter.
### `initialImporterFolder`
The initial folder for the file importer.
### `initialExporterFolder`
The initial folder for the file exporter.
### `initialName`
The initial file name for the file exporter.
### `extensions`
The accepted extensions for the file importer.
### `folders`
Whether folders are accepted in the file importer.
### `importerResult`
The closure to run when the import is successful.
### `exporterResult`
The closure to run when the export is successful.
### `importerCancel`
The closure to run when the import is not successful.
### `exporterCancel`
The closure to run when the export is not successful.
### `defaultSize`
The default window size.
@ -91,6 +47,10 @@ Whether the window is resizable.
Whether the window is deletable.
### `signals`
The signals for the importers and exporters.
## Methods
### `init(id:open:content:)`
@ -168,11 +128,6 @@ Add a keyboard shortcut.
Update the keyboard shortcuts.
- Parameter window: The application window.
### `updateFileDialog(storage:)`
Open a file importer or exporter if a signal has been activated and update changes.
- Parameter storage: The window storage.
### `closeShortcut()`
Add the shortcut "<Ctrl>w" which closes the window.

View File

@ -5,11 +5,15 @@
// Created by david-swift on 30.11.23.
//
import Foundation
/// A type that signalizes an action.
public struct Signal {
/// An action is signalized by toggling a boolean to `true` and back to `false`.
@State var boolean = false
/// A signal has a unique identifier.
public let id: UUID = .init()
/// Whether the action has caused an update.
public var update: Bool { boolean }

View File

@ -0,0 +1,74 @@
//
// Libadwaita.FileDialog.swift
// Adwaita
//
// Created by david-swift on 09.12.23.
//
import Foundation
import Libadwaita
extension Libadwaita.FileDialog: WindowType {
/// An ID for the importer field.
static var importer: String { "importer" }
/// An ID for the folder field.
static var folder: String { "folder" }
/// An ID for the result field.
static var result: String { "result" }
/// An ID for the cancel field.
static var cancel: String { "cancel" }
/// Whether the file dialog is an importer.
var isImporter: Bool {
get {
fields[Self.importer] as? Bool ?? true
}
set {
fields[Self.importer] = newValue
}
}
/// The selected folder in the file dialog.
var folder: URL? {
get {
fields[Self.folder] as? URL
}
set {
fields[Self.folder] = newValue
}
}
/// A closure triggered on selecting a file in the dialog.
var onResult: ((URL) -> Void) {
get {
fields[Self.result] as? (URL) -> Void ?? { _ in }
}
set {
fields[Self.result] = newValue
}
}
/// A closure triggered when the dialog is canceled.
var onCancel: (() -> Void) {
get {
fields[Self.cancel] as? () -> Void ?? { }
}
set {
fields[Self.cancel] = newValue
}
}
/// Set the window's parent window.
/// - Parameter parent: The parent window.
///
/// Currently not implemented.
public func setParentWindow(_ parent: WindowType) { }
/// Display the file dialog.
public func show() {
if isImporter {
self.open(folder: folder, onResult, onClose: onCancel)
} else {
self.save(folder: folder, onResult, onClose: onCancel)
}
}
}

View File

@ -57,7 +57,7 @@ public class GTUIApp: Application {
let window = window.createWindow(app: self)
sceneStorage.append(window)
if let parent {
window.window.setParent(parent)
window.window.setParentWindow(parent)
window.window.fields[overwriteParentID] = true
}
setParentWindows()
@ -69,7 +69,7 @@ public class GTUIApp: Application {
func setParentWindows() {
for window in sceneStorage where !(window.window.fields[overwriteParentID] as? Bool ?? false) {
if let parent = sceneStorage.first(where: { $0.id == window.parentID }) {
window.window.setParent(parent.window)
window.window.setParentWindow(parent.window)
}
}
}

View File

@ -9,3 +9,15 @@ import Libadwaita
/// A GTUI window.
public typealias GTUIWindow = Libadwaita.Window
extension GTUIWindow: WindowType {
/// Set the window's parent window.
/// - Parameter parent: The parent window.
public func setParentWindow(_ parent: WindowType) {
if let window = parent as? GTUIWindow {
self.setParent(window)
}
}
}

View File

@ -16,23 +16,20 @@ public class WindowStorage {
public var parentID: String?
/// Whether the reference to the window should disappear in the next update.
public var destroy = false
/// The GTUI window.
public var window: GTUIWindow
/// The window.
public var window: WindowType
/// The content's storage.
public var view: ViewStorage?
/// The file dialog for the window.
public var fileDialog: FileDialog
/// Initialize a window storage.
/// - Parameters:
/// - id: The window's identifier.
/// - window: The GTUI window.
/// - window: The window.
/// - view: The content's storage.
public init(id: String, window: GTUIWindow, view: ViewStorage?) {
public init(id: String, window: WindowType, view: ViewStorage?) {
self.id = id
self.window = window
self.view = view
fileDialog = .init(window)
}
}

View File

@ -0,0 +1,21 @@
//
// GTUIWindowRepresentable.swift
// Adwaita
//
// Created by david-swift on 09.12.23.
//
import Libadwaita
/// A window type.
public protocol WindowType {
/// A dictionary for custom data.
var fields: [String: Any] { get set }
/// Set a parent window.
/// - Parameter parent: The parent window.
func setParentWindow(_ parent: WindowType)
/// Show the window.
func show()
}

View File

@ -0,0 +1,127 @@
//
// FileDialog.swift
// Adwaita
//
// Created by david-swift on 08.12.23.
//
// swiftlint:disable discouraged_optional_collection
import Foundation
import Libadwaita
/// A structure representing a file dialog window.
public struct FileDialog: WindowScene {
/// The window's identifier.
public var id: String
/// Whether the window is an importer.
public var importer = true
/// Whether an instance of the window type should be opened when the app is starting up.
public var open: Int { 0 }
/// The identifier of the window's parent.
public var parentID: String?
/// The keyboard shortcuts on the app level.
public var appShortcuts: [String: (GTUIApp) -> Void] = [:]
/// 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]?
/// Whether folders are accepted in the file importer.
var folders = false
/// 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)?
/// Create an importer file dialog window.
/// - Parameters:
/// - importer: The window's identifier.
/// - 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 init(
importer: String,
initialFolder: URL? = nil,
extensions: [String]? = nil,
folders: Bool = false,
onOpen: @escaping (URL) -> Void,
onClose: @escaping () -> Void
) {
self.id = importer
self.initialFolder = initialFolder
self.extensions = extensions
self.folders = folders
self.result = onOpen
self.cancel = onClose
}
/// Create an exporter file dialog window.
/// - Parameters:
/// - exporter: The window's identifier.
/// - initialFolder: The URL to the folder open when being opened.
/// - initialName: The default file name.
/// - onSave: Run this when a path for exporting has been chosen.
/// - onClose: Run this when the user cancelled the action.
public init(
exporter: String,
initialFolder: URL? = nil,
initialName: String? = nil,
onSave: @escaping (URL) -> Void,
onClose: @escaping () -> Void
) {
self.id = exporter
self.importer = false
self.initialFolder = initialFolder
self.initialName = initialName
self.result = onSave
self.cancel = onClose
}
/// Get the storage for the window.
/// - Parameter app: The application.
/// - Returns: The storage.
public func createWindow(app: GTUIApp) -> WindowStorage {
let window = Libadwaita.FileDialog(nil)
let windowStorage = WindowStorage(id: id, window: window, view: nil)
windowStorage.parentID = parentID
update(window: window)
return windowStorage
}
/// Update a window.
/// - Parameters:
/// - storage: The storage to update.
/// - app: The application.
public func update(_ storage: WindowStorage, app: GTUIApp) {
updateAppShortcuts(app: app)
if let window = storage.window as? Libadwaita.FileDialog {
update(window: window)
}
storage.destroy = true
}
/// Update the window.
/// - Parameter window: The window.
func update(window: Libadwaita.FileDialog) {
window.isImporter = importer
window.folder = initialFolder
if let initialName {
window.setInitialName(initialName)
}
window.setExtensions(extensions, folders: folders)
if let result {
window.onResult = result
}
if let cancel {
window.onCancel = cancel
}
}
}
// swiftlint:enable discouraged_optional_collection

View File

@ -27,28 +27,6 @@ public struct Window: WindowScene {
var shortcuts: [String: (GTUIApplicationWindow) -> Void] = [:]
/// The keyboard shortcuts on the app level.
public var appShortcuts: [String: (GTUIApp) -> Void] = [:]
/// The signal for the file importer.
var fileImporter: Signal = .init()
/// The signal for the file exporter.
var fileExporter: Signal = .init()
/// The initial folder for the file importer.
var initialImporterFolder: URL?
/// The initial folder for the file exporter.
var initialExporterFolder: URL?
/// The initial file name for the file exporter.
var initialName: String?
/// The accepted extensions for the file importer.
var extensions: [String]?
/// Whether folders are accepted in the file importer.
var folders = false
/// The closure to run when the import is successful.
var importerResult: ((URL) -> Void)?
/// The closure to run when the export is successful.
var exporterResult: ((URL) -> Void)?
/// The closure to run when the import is not successful.
var importerCancel: (() -> Void)?
/// The closure to run when the export is not successful.
var exporterCancel: (() -> Void)?
/// The default window size.
var defaultSize: (Int, Int)?
/// The window's title.
@ -57,6 +35,8 @@ public struct Window: WindowScene {
var resizable = true
/// Whether the window is deletable.
var deletable = true
/// The signals for the importers and exporters.
var signals: [Signal] = []
/// Create a window type with a certain identifier and user interface.
/// - Parameters:
@ -81,7 +61,6 @@ public struct Window: WindowScene {
return false
}
windowStorage.parentID = parentID
updateFileDialog(storage: windowStorage)
return windowStorage
}
@ -119,7 +98,11 @@ public struct Window: WindowScene {
updateShortcuts(window: window)
updateAppShortcuts(app: app)
}
updateFileDialog(storage: storage)
for signal in signals where signal.update {
Task {
app.showWindow(signal.id.uuidString)
}
}
}
/// Set some general propreties of the window.
@ -159,15 +142,20 @@ public struct Window: WindowScene {
folders: Bool = false,
onOpen: @escaping (URL) -> Void,
onClose: @escaping () -> Void
) -> Self {
) -> Scene {
var newSelf = self
newSelf.fileImporter = signal
newSelf.initialImporterFolder = initialFolder
newSelf.extensions = extensions
newSelf.folders = folders
newSelf.importerResult = onOpen
newSelf.importerCancel = onClose
newSelf.signals.append(signal)
return newSelf
.overlay {
FileDialog(
importer: signal.id.uuidString,
initialFolder: initialFolder,
extensions: extensions,
folders: folders,
onOpen: onOpen,
onClose: onClose
)
}
}
/// Add an exporter file dialog to the window.
@ -183,14 +171,19 @@ public struct Window: WindowScene {
initialName: String? = nil,
onSave: @escaping (URL) -> Void,
onClose: @escaping () -> Void
) -> Self {
) -> Scene {
var newSelf = self
newSelf.fileExporter = signal
newSelf.initialExporterFolder = initialFolder
newSelf.initialName = initialName
newSelf.exporterResult = onSave
newSelf.exporterCancel = onClose
newSelf.signals.append(signal)
return newSelf
.overlay {
FileDialog(
exporter: signal.id.uuidString,
initialFolder: initialFolder,
initialName: initialName,
onSave: onSave,
onClose: onClose
)
}
}
/// Add a keyboard shortcut.
@ -212,20 +205,6 @@ public struct Window: WindowScene {
}
}
/// Open a file importer or exporter if a signal has been activated and update changes.
/// - Parameter storage: The window storage.
func updateFileDialog(storage: WindowStorage) {
storage.fileDialog.setExtensions(extensions, folders: folders)
if let initialName {
storage.fileDialog.setInitialName(initialName)
}
if fileImporter.update, let importerResult, let importerCancel {
storage.fileDialog.open(folder: initialImporterFolder, importerResult, onClose: importerCancel)
} else if fileExporter.update, let exporterResult, let exporterCancel {
storage.fileDialog.save(folder: initialExporterFolder, exporterResult, onClose: exporterCancel)
}
}
/// Add the shortcut "<Ctrl>w" which closes the window.
/// - Returns: The window.
public func closeShortcut() -> Self {