diff --git a/Sources/Adwaita/AnyView+.swift b/Sources/Adwaita/AnyView+.swift index 5909aa0..902dff5 100644 --- a/Sources/Adwaita/AnyView+.swift +++ b/Sources/Adwaita/AnyView+.swift @@ -127,6 +127,22 @@ extension AnyView { ) } + /// Add a shortcuts dialog to the parent window. + /// - Parameters: + /// - visible: Whether the dialog is presented. + /// - id: The dialog's id. + /// - Returns: The view. + public func shortcutsDialog( + visible: Binding, + id: String? = nil + ) -> ShortcutsDialog { + .init( + visible: visible, + child: self, + id: id ?? "" + ) + } + /// Create an importer file dialog. /// - Parameters: /// - open: The signal to open the dialog. diff --git a/Sources/Adwaita/View/Dialogs/PreferencesDialog.swift b/Sources/Adwaita/View/Dialogs/PreferencesDialog.swift index b116dab..82808dc 100644 --- a/Sources/Adwaita/View/Dialogs/PreferencesDialog.swift +++ b/Sources/Adwaita/View/Dialogs/PreferencesDialog.swift @@ -174,9 +174,11 @@ public struct PreferencesDialog: AdwaitaWidget { child.updateStorage(storage, data: data, updateProperties: updateProperties, type: type) } defer { - for (index, page) in pages.enumerated() { - if let preferences = storage.content["preferences-\(index)"] { - page.update(groups: preferences, data: data, updateProperties: updateProperties) + if visible { + for (index, page) in pages.enumerated() { + if let preferences = storage.content["preferences-\(index)"] { + page.update(groups: preferences, data: data, updateProperties: updateProperties) + } } } } diff --git a/Sources/Adwaita/View/Dialogs/ShortcutsDialog.swift b/Sources/Adwaita/View/Dialogs/ShortcutsDialog.swift new file mode 100644 index 0000000..b35ea30 --- /dev/null +++ b/Sources/Adwaita/View/Dialogs/ShortcutsDialog.swift @@ -0,0 +1,194 @@ +// +// ShortcutsDialog.swift +// Adwaita +// +// Created by david-swift on 04.11.25. +// + +import CAdw + +/// The shortcuts dialog widget. +public struct ShortcutsDialog: AdwaitaWidget { + + /// Whether the dialog is visible. + @Binding var visible: Bool + /// An identifier used if multiple dialogs are on one view. + var id: String + /// The shortcuts dialog sections. + var sections: [ShortcutsSection] = [] + /// 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: A unique identifier for dialogs on the view. + init( + visible: Binding, + child: AnyView, + id: String + ) { + self._visible = visible + self.child = child + self.id = id + } + + /// A shortcuts section. + public struct ShortcutsSection { + + /// The section's title. + var title: String? + /// The section's content. + var content: [ShortcutsItem] = [] + + /// Initialize the shortcuts section. + /// - Parameter title: The section's title. + init(_ title: String?) { + self.title = title + } + + /// Get the GTK shortcuts section as well as the section's view storages. + /// - Parameter data: The widget data. + /// - Returns: The shortcuts section pointer and the section's view storages. + func gtkShortcutsSection(data: WidgetData) -> (OpaquePointer?, [ViewStorage]) { + let section = adw_shortcuts_section_new(title) + let items = content.map { $0.gtkShortcutsItem(data: data) } + for item in items { + adw_shortcuts_section_add(section, item.opaquePointer) + } + return (section, items) + } + + /// Add a shortcuts item. + /// - Parameters: + /// - title: The item's title. + /// - accelerator: The shortcut acccelerator. + public func shortcutsItem( + _ title: String, + accelerator: String + ) -> Self { + var newSelf = self + newSelf.content.append( + .init( + title: title, + accelerator: accelerator + ) + ) + return newSelf + } + + } + + /// The shortcuts item. + struct ShortcutsItem { + + /// The item's title. + var title: String + /// The item's description. + var accelerator: String + + /// Get the GTK preferences group's storage. + /// - Parameter data: The widget data. + /// - Returns: The view storage. + func gtkShortcutsItem(data: WidgetData) -> ViewStorage { + .init(adw_shortcuts_item_new(title, accelerator)) + } + + } + + /// 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) + } + 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 sections.indices { + let container = storage.content["shortcuts-\(index)"]?.map { $0.opaquePointer } + container?.forEach { g_object_unref($0?.cast()) } + g_object_unref((storage.fields["shortcuts-\(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_shortcuts_dialog_new() + 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, section) in sections.map({ $0.gtkShortcutsSection(data: data) }).enumerated() { + storage.content["shortcuts-\(index)"] = section.1 + storage.fields["shortcuts-\(index)"] = section.0 + adw_shortcuts_dialog_add(pointer?.opaque(), section.0) + } + } + + /// Add a shortcuts section. + /// - Parameters: + /// - title: The section's title or `nil`. + /// - content: Modify the shortcuts items. + public func shortcutsSection( + _ title: String? = nil, + content: (ShortcutsSection) -> ShortcutsSection + ) -> Self { + modify { $0.sections.append(content(.init(title))) } + } +} diff --git a/Sources/Demo/Demo.swift b/Sources/Demo/Demo.swift index be70ae5..a4a7224 100644 --- a/Sources/Demo/Demo.swift +++ b/Sources/Demo/Demo.swift @@ -90,6 +90,7 @@ struct Demo: App { @State private var maximized = false @State private var about = false @State private var preferences = false + @State private var shortcuts = false @State private var title: WindowName = .demo @State private var closeAlert = false @State private var destroy = false @@ -184,6 +185,18 @@ struct Demo: App { destroy = true window.close() } + .shortcutsDialog(visible: $shortcuts) + .shortcutsSection("Windows") { section in + section + .shortcutsItem("New window", accelerator: "n".ctrl()) + .shortcutsItem("Close window", accelerator: "w".ctrl()) + } + .shortcutsSection("General") { section in + section + .shortcutsItem("Show preferences", accelerator: "comma".ctrl()) + .shortcutsItem("Show keyboard shortcuts", accelerator: "question".ctrl()) + } + .shortcutsSection { $0.shortcutsItem("Quit Demo", accelerator: "q".ctrl()) } } var menu: AnyView { @@ -199,6 +212,8 @@ struct Demo: App { MenuSection { MenuButton("Preferences") { preferences = true } .keyboardShortcut("comma".ctrl()) + MenuButton("Keyboard Shortcuts") { shortcuts = true } + .keyboardShortcut("question".ctrl()) MenuButton("About") { about = true } MenuButton("Quit", window: false) { app.quit() } .keyboardShortcut("q".ctrl())