// // Window.swift // Adwaita // // Created by david-swift on 14.09.23. // // swiftlint:disable discouraged_optional_collection import Foundation import Libadwaita /// A structure representing an application window type. /// /// Note that multiple instances of a window can be opened at the same time. public struct Window: WindowScene { /// The window's identifier. public var id: String /// The window's content. var content: (GTUIApplicationWindow) -> Body /// Whether an instance of the window type should be opened when the app is starting up. public var `open`: Int /// The identifier of the window's parent. public var parentID: String? /// The keyboard shortcuts. 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)? /// Create a window type with a certain identifier and user interface. /// - Parameters: /// - id: The identifier. /// - open: The number of instances of the window type when the app is starting. /// - content: The window's content. public init(id: String, `open`: Int = 1, @ViewBuilder content: @escaping (GTUIApplicationWindow) -> Body) { self.content = content self.id = id self.open = open } /// Get the storage for the window. /// - Parameter app: The application. /// - Returns: The storage. public func createWindow(app: GTUIApp) -> WindowStorage { let window = createGTUIWindow(app: app) let storage = getViewStorage(window: window) let windowStorage = WindowStorage(id: id, window: window, view: storage) window.observeHide { windowStorage.destroy = true return false } windowStorage.parentID = parentID updateFileDialog(storage: windowStorage) return windowStorage } /// Get the window. /// - Parameter app: The application. /// - Returns: The window. func createGTUIWindow(app: GTUIApp) -> GTUIApplicationWindow { let window = GTUIApplicationWindow(app: app) updateAppShortcuts(app: app) window.show() return window } /// Get the storage of the content view. /// - Parameter window: The window. /// - Returns: The storage of the content of the window. func getViewStorage(window: GTUIApplicationWindow) -> ViewStorage { let content = content(window) let storage = content.widget(modifiers: []).container(modifiers: []) window.setChild(storage.view) updateShortcuts(window: window) return storage } /// Update a window storage's content. /// - Parameter storage: The storage to update. public func update(_ storage: WindowStorage, app: GTUIApp) { if let window = storage.window as? GTUIApplicationWindow { let content = content(window) content.widget(modifiers: []).updateStorage(storage.view, modifiers: []) updateShortcuts(window: window) updateAppShortcuts(app: app) } updateFileDialog(storage: storage) } /// Add windows that overlay the last instance of this window if presented. /// - Parameter windows: The windows. /// - Returns: The new windows and this window. public func overlay(@SceneBuilder windows: () -> [WindowSceneGroup]) -> [WindowScene] { windows().windows().map { window in var newWindow = window newWindow.parentID = id return newWindow } + [self] } /// Add an importer file dialog to the window. /// - Parameters: /// - signal: The signal for opening 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( _ signal: Signal, initialFolder: URL? = nil, extensions: [String]? = nil, folders: Bool = false, onOpen: @escaping (URL) -> Void, onClose: @escaping () -> Void ) -> Self { var newSelf = self newSelf.fileImporter = signal newSelf.initialImporterFolder = initialFolder newSelf.extensions = extensions newSelf.folders = folders newSelf.importerResult = onOpen newSelf.importerCancel = onClose return newSelf } /// Add an exporter file dialog to the window. /// - Parameters: /// - signal: The signal for opening the dialog. /// - 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 func fileExporter( _ signal: Signal, initialFolder: URL? = nil, initialName: String? = nil, onSave: @escaping (URL) -> Void, onClose: @escaping () -> Void ) -> Self { var newSelf = self newSelf.fileExporter = signal newSelf.initialExporterFolder = initialFolder newSelf.initialName = initialName newSelf.exporterResult = onSave newSelf.exporterCancel = onClose return newSelf } /// Add a keyboard shortcut. /// - Parameters: /// - shortcut: The keyboard shortcut. /// - action: The closure to execute when the keyboard shortcut is pressed. /// - Returns: The window. public func keyboardShortcut(_ shortcut: String, action: @escaping (GTUIApplicationWindow) -> Void) -> Self { var newSelf = self newSelf.shortcuts[shortcut] = action return newSelf } /// Update the keyboard shortcuts. /// - Parameter window: The application window. func updateShortcuts(window: GTUIApplicationWindow) { for shortcut in shortcuts { window.addKeyboardShortcut(shortcut.key, id: shortcut.key) { shortcut.value(window) } } } /// 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 { keyboardShortcut("w".ctrl()) { $0.close() } } } // swiftlint:enable discouraged_optional_collection