diff --git a/Sources/Adwaita/AnyView+.swift b/Sources/Adwaita/AnyView+.swift index 04ed566..a60005f 100644 --- a/Sources/Adwaita/AnyView+.swift +++ b/Sources/Adwaita/AnyView+.swift @@ -86,6 +86,22 @@ extension AnyView { ) } + /// Add a preferences dialog to the parent window. + /// - Parameters: + /// - visible: Whether the dialog is presented. + /// - id: The dialog's id. + /// - Returns: The view. + public func preferencesDialog( + visible: Binding, + id: String? = nil + ) -> PreferencesDialog { + .init( + visible: visible, + child: self, + id: id ?? "" + ) + } + /// Create an importer file dialog. /// - Parameters: /// - open: The signal to open the dialog. diff --git a/Sources/Core/View/Dialogs/PreferencesDialog.swift b/Sources/Core/View/Dialogs/PreferencesDialog.swift new file mode 100644 index 0000000..25d58a5 --- /dev/null +++ b/Sources/Core/View/Dialogs/PreferencesDialog.swift @@ -0,0 +1,247 @@ +// +// PreferencesDialog.swift +// Adwaita +// +// Created by david-swift on 04.11.24. +// + +import CAdw + +/// The preferences dialog widget. +public struct PreferencesDialog: AdwaitaWidget { + + /// Whether the dialog is visible. + @Binding var visible: Bool + /// An identifier used if multiple dialogs are on one view. + var id: String + /// The settings dialog pages. + var pages: [PreferencesPage] = [] + /// The content. + var child: AnyView + + /// The ID for the dialog's storage. + let dialogID = "dialog" + /// The ID for the content's storage. + let contentID = "content" + + /// Initialize a dialog wrapper. + /// - Parameters: + /// - visible: Whether the dialog is visible. + /// - child: The wrapped view. + /// - id: An unique identifier for dialogs on the view. + public init( + visible: Binding, + child: AnyView, + id: String + ) { + self._visible = visible + self.child = child + self.id = id + } + + /// A preferences page. + public struct PreferencesPage { + + /// The page's title. + var title: String + /// The page's icon. + var icon: Icon + /// The page content. + var content: [PreferencesGroup] = [] + + /// Initialize the preferences page. + /// - Parameters: + /// - title: The page title. + /// - icon: The page's icon. + public init(_ title: String, icon: Icon) { + self.title = title + self.icon = icon + } + + /// Get the GTK preferences page as well as the group's view storages. + /// - Parameter data: The widget data. + /// - Returns: The preferences page pointer and the groups view storages. + func gtkPreferencesPage(data: WidgetData) -> (OpaquePointer?, [ViewStorage]) { + let page = Core.PreferencesPage() + .title(title) + .iconName(icon.string) + .storage(data: data.noModifiers, type: AdwaitaMainView.self) + let groups = content.map { $0.gtkPreferencesGroup(data: data) } + for group in groups { + adw_preferences_page_add(page.opaquePointer?.cast(), group.opaquePointer?.cast()) + } + return (page.opaquePointer, groups) + } + + /// Update the page. + /// - Parameters: + /// - groups: The groups' view storages. + /// - data: The widget data. + /// - updateProperties: Whether to update the properties. + func update(groups: [ViewStorage], data: WidgetData, updateProperties: Bool) { + for (index, storage) in groups.enumerated() { + content[safe: index]?.update(group: storage, data: data, updateProperties: updateProperties) + } + } + + /// Add a preferences group. + /// - Parameters: + /// - title: The group's title. + /// - description: The group's description. + /// - child: The view child. + public func group( + _ title: String, + description: String = "", + @ViewBuilder child: () -> Body + ) -> Self { + var newSelf = self + newSelf.content.append( + .init( + title: title, + description: description, + child: child() + ) + ) + return newSelf + } + + } + + /// The preferences group. + struct PreferencesGroup { + + /// The group's title. + var title: String + /// The group's description. + var description: String + /// The group's child view. + var child: Body + + /// Get the GTk preferences group. + /// - Parameter data: The widget data. + /// - Returns: The preferences group. + func group(data: WidgetData) -> Core.PreferencesGroup { + .init() + .title(title) + .description(description) + .child { child } + } + + /// Get the GTK preferences group's storage. + /// - Parameter data: The widget data. + /// - Returns: The view storage. + func gtkPreferencesGroup(data: WidgetData) -> ViewStorage { + group(data: data).container(data: data.noModifiers, type: AdwaitaMainView.self) + } + + /// Update the preferences group. + /// - Parameters: + /// - group: The view storage. + /// - data: The widget data. + /// - updateProperties: Whether to update the properties. + func update(group: ViewStorage, data: WidgetData, updateProperties: Bool) { + self.group(data: data) + .update(group, data: data, updateProperties: updateProperties, type: AdwaitaMainView.self) + } + + } + + /// The view storage. + /// - Parameters: + /// - modifiers: Modify views before being updated. + /// - type: The view render data type. + /// - Returns: The view storage. + public func container(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. + public func update( + _ storage: ViewStorage, + data: WidgetData, + updateProperties: Bool, + type: Data.Type + ) where Data: ViewRenderData { + if let storage = storage.content[.mainContent]?.first { + child.updateStorage(storage, data: data, updateProperties: updateProperties, type: type) + } + for (index, page) in pages.enumerated() { + if let preferences = storage.content["preferences-\(index)"] { + page.update(groups: preferences, data: data, updateProperties: updateProperties) + } + } + guard updateProperties else { + return + } + if visible { + if storage.content[dialogID + id]?.first == nil { + createDialog(storage: storage, data: data, type: type) + adw_dialog_present( + storage.content[dialogID + id]?.first?.opaquePointer?.cast(), + storage.opaquePointer?.cast() + ) + } + } else { + if storage.content[dialogID + id]?.first != nil { + let dialog = storage.content[dialogID + id]?.first?.opaquePointer + adw_dialog_close(dialog?.cast()) + g_object_unref(dialog?.cast()) + for index in pages.indices { + let container = storage.content["preferences-\(index)"]?.map { $0.opaquePointer } + container?.forEach { g_object_unref($0?.cast()) } + g_object_unref((storage.fields["preferences-\(index)"] as? OpaquePointer)?.cast()) + } + } + } + } + + /// Create a new instance of the dialog. + /// - Parameters: + /// - storage: The wrapped view's storage. + /// - modifiers: The view modifiers. + /// - type: The view render data type. + func createDialog( + storage: ViewStorage, + data: WidgetData, + type: Data.Type + ) where Data: ViewRenderData { + let pointer = adw_preferences_dialog_new() + adw_preferences_dialog_set_search_enabled(pointer?.cast(), 1) + let dialog = ViewStorage(pointer?.opaque()) + storage.content[dialogID + id] = [dialog] + dialog.connectSignal(name: "closed") { + storage.content[dialogID + id] = [] + storage.content[contentID + id] = [] + if visible { + visible = false + } + } + for (index, page) in pages.map({ $0.gtkPreferencesPage(data: data) }).enumerated() { + storage.content["preferences-\(index)"] = page.1 + storage.fields["preferences-\(index)"] = page.0 + adw_preferences_dialog_add(pointer?.cast(), page.0?.cast()) + } + } + + /// Add a preferences page. + /// - Parameters: + /// - title: The page title. + /// - icon: The page icon. + /// - content: Modify the preferences pages. + public func preferencesPage( + _ title: String, + icon: Icon, + content: (PreferencesPage) -> PreferencesPage + ) -> Self { + modify { $0.pages.append(content(.init(title, icon: icon))) } + } + +} diff --git a/Sources/Demo/Demo.swift b/Sources/Demo/Demo.swift index b60837d..c21b5d9 100644 --- a/Sources/Demo/Demo.swift +++ b/Sources/Demo/Demo.swift @@ -76,6 +76,7 @@ struct Demo: App { @State private var wide = true @State private var maximized = false @State private var about = false + @State private var preferences = false var window: AdwaitaWindow var app: AdwaitaApp! var pictureURL: URL? @@ -140,6 +141,22 @@ struct Demo: App { website: .init(string: "https://adwaita-swift.aparoksha.dev/"), issues: .init(string: "https://git.aparoksha.dev/aparoksha/adwaita-swift/issues") ) + .preferencesDialog(visible: $preferences) + .preferencesPage("Page 1", icon: .default(icon: .audioHeadset)) { page in + page + .group("General") { + SwitchRow() + .title("Hello") + .subtitle("World") + SwitchRow() + .title("Switch") + .subtitle("Row") + } + .group("Extra", description: "This is the group's description") { + ActionRow() + .title("Extra Action") + } + } } var menu: AnyView { @@ -153,6 +170,8 @@ struct Demo: App { } .keyboardShortcut("w".ctrl()) MenuSection { + MenuButton("Preferences") { preferences = true } + .keyboardShortcut("comma".ctrl()) MenuButton("About") { about = true } MenuButton("Quit", window: false) { app.quit() } .keyboardShortcut("q".ctrl())