From 083b35234824ce818cabf654d28a77c68b59b867 Mon Sep 17 00:00:00 2001 From: david-swift Date: Sat, 9 Dec 2023 13:08:11 +0100 Subject: [PATCH] Make FileDialog a window --- Documentation/Reference/README.md | 4 + .../Reference/classes/WindowStorage.md | 8 +- .../Reference/extensions/GTUIWindow.md | 9 ++ .../extensions/Libadwaita.FileDialog.md | 48 +++++++ .../Reference/protocols/WindowType.md | 20 +++ Documentation/Reference/structs/FileDialog.md | 90 +++++++++++++ Documentation/Reference/structs/Signal.md | 4 + Documentation/Reference/structs/Window.md | 53 +------- Sources/Adwaita/Model/Data Flow/Signal.swift | 4 + .../Extensions/Libadwaita.FileDialog.swift | 74 ++++++++++ .../Model/User Interface/App/GTUIApp.swift | 4 +- .../User Interface/Window/GTUIWindow.swift | 12 ++ .../User Interface/Window/WindowStorage.swift | 11 +- .../User Interface/Window/WindowType.swift | 21 +++ Sources/Adwaita/Window/FileDialog.swift | 127 ++++++++++++++++++ Sources/Adwaita/Window/Window.swift | 81 +++++------ 16 files changed, 455 insertions(+), 115 deletions(-) create mode 100644 Documentation/Reference/extensions/GTUIWindow.md create mode 100644 Documentation/Reference/extensions/Libadwaita.FileDialog.md create mode 100644 Documentation/Reference/protocols/WindowType.md create mode 100644 Documentation/Reference/structs/FileDialog.md create mode 100644 Sources/Adwaita/Model/Extensions/Libadwaita.FileDialog.swift create mode 100644 Sources/Adwaita/Model/User Interface/Window/WindowType.swift create mode 100644 Sources/Adwaita/Window/FileDialog.swift diff --git a/Documentation/Reference/README.md b/Documentation/Reference/README.md index 1626991..0f5fdd7 100644 --- a/Documentation/Reference/README.md +++ b/Documentation/Reference/README.md @@ -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) diff --git a/Documentation/Reference/classes/WindowStorage.md b/Documentation/Reference/classes/WindowStorage.md index 3772300..6513645 100644 --- a/Documentation/Reference/classes/WindowStorage.md +++ b/Documentation/Reference/classes/WindowStorage.md @@ -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. diff --git a/Documentation/Reference/extensions/GTUIWindow.md b/Documentation/Reference/extensions/GTUIWindow.md new file mode 100644 index 0000000..f834fd1 --- /dev/null +++ b/Documentation/Reference/extensions/GTUIWindow.md @@ -0,0 +1,9 @@ +**EXTENSION** + +# `GTUIWindow` + +## Methods +### `setParentWindow(_:)` + +Set the window's parent window. +- Parameter parent: The parent window. diff --git a/Documentation/Reference/extensions/Libadwaita.FileDialog.md b/Documentation/Reference/extensions/Libadwaita.FileDialog.md new file mode 100644 index 0000000..cb3d422 --- /dev/null +++ b/Documentation/Reference/extensions/Libadwaita.FileDialog.md @@ -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. diff --git a/Documentation/Reference/protocols/WindowType.md b/Documentation/Reference/protocols/WindowType.md new file mode 100644 index 0000000..68c7df6 --- /dev/null +++ b/Documentation/Reference/protocols/WindowType.md @@ -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. diff --git a/Documentation/Reference/structs/FileDialog.md b/Documentation/Reference/structs/FileDialog.md new file mode 100644 index 0000000..97bc9b8 --- /dev/null +++ b/Documentation/Reference/structs/FileDialog.md @@ -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. diff --git a/Documentation/Reference/structs/Signal.md b/Documentation/Reference/structs/Signal.md index cb4a10d..c7e8aa3 100644 --- a/Documentation/Reference/structs/Signal.md +++ b/Documentation/Reference/structs/Signal.md @@ -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. diff --git a/Documentation/Reference/structs/Window.md b/Documentation/Reference/structs/Window.md index 0418e95..5054be3 100644 --- a/Documentation/Reference/structs/Window.md +++ b/Documentation/Reference/structs/Window.md @@ -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 "w" which closes the window. diff --git a/Sources/Adwaita/Model/Data Flow/Signal.swift b/Sources/Adwaita/Model/Data Flow/Signal.swift index ebcc2da..920d8c2 100644 --- a/Sources/Adwaita/Model/Data Flow/Signal.swift +++ b/Sources/Adwaita/Model/Data Flow/Signal.swift @@ -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 } diff --git a/Sources/Adwaita/Model/Extensions/Libadwaita.FileDialog.swift b/Sources/Adwaita/Model/Extensions/Libadwaita.FileDialog.swift new file mode 100644 index 0000000..ac8c008 --- /dev/null +++ b/Sources/Adwaita/Model/Extensions/Libadwaita.FileDialog.swift @@ -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) + } + } + +} diff --git a/Sources/Adwaita/Model/User Interface/App/GTUIApp.swift b/Sources/Adwaita/Model/User Interface/App/GTUIApp.swift index fc4ce66..a179d37 100644 --- a/Sources/Adwaita/Model/User Interface/App/GTUIApp.swift +++ b/Sources/Adwaita/Model/User Interface/App/GTUIApp.swift @@ -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) } } } diff --git a/Sources/Adwaita/Model/User Interface/Window/GTUIWindow.swift b/Sources/Adwaita/Model/User Interface/Window/GTUIWindow.swift index 2867e46..4fe9e92 100644 --- a/Sources/Adwaita/Model/User Interface/Window/GTUIWindow.swift +++ b/Sources/Adwaita/Model/User Interface/Window/GTUIWindow.swift @@ -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) + } + } + +} diff --git a/Sources/Adwaita/Model/User Interface/Window/WindowStorage.swift b/Sources/Adwaita/Model/User Interface/Window/WindowStorage.swift index 548bd5b..07567c3 100644 --- a/Sources/Adwaita/Model/User Interface/Window/WindowStorage.swift +++ b/Sources/Adwaita/Model/User Interface/Window/WindowStorage.swift @@ -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) } } diff --git a/Sources/Adwaita/Model/User Interface/Window/WindowType.swift b/Sources/Adwaita/Model/User Interface/Window/WindowType.swift new file mode 100644 index 0000000..283c154 --- /dev/null +++ b/Sources/Adwaita/Model/User Interface/Window/WindowType.swift @@ -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() + +} diff --git a/Sources/Adwaita/Window/FileDialog.swift b/Sources/Adwaita/Window/FileDialog.swift new file mode 100644 index 0000000..d088f1a --- /dev/null +++ b/Sources/Adwaita/Window/FileDialog.swift @@ -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 diff --git a/Sources/Adwaita/Window/Window.swift b/Sources/Adwaita/Window/Window.swift index 956a5d7..a9f0987 100644 --- a/Sources/Adwaita/Window/Window.swift +++ b/Sources/Adwaita/Window/Window.swift @@ -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 "w" which closes the window. /// - Returns: The window. public func closeShortcut() -> Self {