forked from aparoksha/adwaita-swift
Add support for alert dialogs
This commit is contained in:
parent
8cba3242f5
commit
d63dec3c6b
@ -91,11 +91,15 @@ struct AboutDialog: Widget {
|
|||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
|
|
||||||
/// Add a dialog to the parent window.
|
/// Add an about dialog to the parent window.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - visible: Whether the dialog is presented.
|
/// - visible: Whether the dialog is presented.
|
||||||
/// - title: The dialog's title.
|
/// - app: The app's name.
|
||||||
/// - content: The dialog's content.
|
/// - developer: The developer's name.
|
||||||
|
/// - version: The version string.
|
||||||
|
/// - icon: The app icon.
|
||||||
|
/// - website: The app's website.
|
||||||
|
/// - issues: Website for reporting issues.
|
||||||
public func aboutDialog(
|
public func aboutDialog(
|
||||||
visible: Binding<Bool>,
|
visible: Binding<Bool>,
|
||||||
app: String? = nil,
|
app: String? = nil,
|
||||||
|
|||||||
255
Sources/Adwaita/View/Dialogs/AlertDialog.swift
Normal file
255
Sources/Adwaita/View/Dialogs/AlertDialog.swift
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
//
|
||||||
|
// AlertDialog.swift
|
||||||
|
// Adwaita
|
||||||
|
//
|
||||||
|
// Created by david-swift on 05.04.24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CAdw
|
||||||
|
import LevenshteinTransformations
|
||||||
|
|
||||||
|
/// The message dialog widget.
|
||||||
|
public struct AlertDialog: Widget {
|
||||||
|
|
||||||
|
/// The ID for the dialog's storage.
|
||||||
|
static let dialogID = "alert-dialog"
|
||||||
|
/// The ID for the dialog's responses' storage.
|
||||||
|
static let responsesID = "responses"
|
||||||
|
/// The ID for the visibility binding.
|
||||||
|
static let visibleID = "visible"
|
||||||
|
/// The ID for the callbacks.
|
||||||
|
static let callbacks = "callbacks"
|
||||||
|
|
||||||
|
/// Whether the dialog is visible.
|
||||||
|
@Binding var visible: Bool
|
||||||
|
/// An identifier used if multiple dialogs are on one view.
|
||||||
|
var id: String
|
||||||
|
/// The dialog's title.
|
||||||
|
var heading: String
|
||||||
|
/// The body text.
|
||||||
|
var body: String
|
||||||
|
/// The available responses.
|
||||||
|
var responses: [Response] = []
|
||||||
|
/// The child view.
|
||||||
|
var child: View
|
||||||
|
|
||||||
|
/// Information about a response.
|
||||||
|
struct Response: Identifiable {
|
||||||
|
|
||||||
|
/// The title.
|
||||||
|
var title: String
|
||||||
|
/// The identifier.
|
||||||
|
var id: String { title }
|
||||||
|
/// The appearance.
|
||||||
|
var appearance: ResponseAppearance
|
||||||
|
/// The function for the keyboard shortcut, or no shortcut.
|
||||||
|
var role: ResponseRole?
|
||||||
|
/// The callback.
|
||||||
|
var action: () -> Void
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The appearance of the response.
|
||||||
|
public enum ResponseAppearance {
|
||||||
|
|
||||||
|
/// The regular appearance.
|
||||||
|
case `default`
|
||||||
|
/// The suggested appearance.
|
||||||
|
case suggested
|
||||||
|
/// The destructive appearance.
|
||||||
|
case destructive
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The role of the response, determining a keyboard shortcut.
|
||||||
|
public enum ResponseRole {
|
||||||
|
|
||||||
|
/// The close role.
|
||||||
|
case close
|
||||||
|
/// The default role.
|
||||||
|
case `default`
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the container of the child.
|
||||||
|
/// - Parameter modifiers: Modify views before being updated.
|
||||||
|
/// - Returns: The view storage.
|
||||||
|
public func container(modifiers: [(View) -> View]) -> ViewStorage {
|
||||||
|
let storage = child.storage(modifiers: modifiers)
|
||||||
|
storage.fields[Self.visibleID + id] = _visible
|
||||||
|
update(storage, modifiers: modifiers, updateProperties: true)
|
||||||
|
return storage
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the view storage of the child, dialog, and dialog content.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - storage: The view storage.
|
||||||
|
/// - modifiers: Modify views before being updated.
|
||||||
|
/// - updateProperties: Whether to update properties.
|
||||||
|
public func update(_ storage: ViewStorage, modifiers: [(View) -> View], updateProperties: Bool) {
|
||||||
|
child.widget(modifiers: modifiers).update(storage, modifiers: modifiers, updateProperties: updateProperties)
|
||||||
|
guard updateProperties else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if visible {
|
||||||
|
var present = false
|
||||||
|
if storage.content[Self.dialogID + id]?.first == nil {
|
||||||
|
createDialog(storage: storage, modifiers: modifiers)
|
||||||
|
present = true
|
||||||
|
}
|
||||||
|
let pointer = storage.content[Self.dialogID + id]?.first?.pointer
|
||||||
|
adw_alert_dialog_set_heading(pointer?.cast(), heading)
|
||||||
|
adw_alert_dialog_set_body(pointer?.cast(), body)
|
||||||
|
let old = storage.fields[Self.responsesID + id] as? [Response] ?? []
|
||||||
|
old.identifiableTransform(
|
||||||
|
to: responses,
|
||||||
|
functions: .init { index, element in
|
||||||
|
adw_alert_dialog_remove_response(pointer?.cast(), responseID(old[safe: index]?.id))
|
||||||
|
adw_alert_dialog_add_response(pointer?.cast(), responseID(element.id), element.title)
|
||||||
|
} delete: { index in
|
||||||
|
adw_alert_dialog_remove_response(pointer?.cast(), responseID(old[safe: index]?.id))
|
||||||
|
} insert: { _, element in
|
||||||
|
adw_alert_dialog_add_response(pointer?.cast(), responseID(element.id), element.title)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
storage.fields[Self.responsesID + id] = responses
|
||||||
|
var handlers: [String: () -> Void] = [:]
|
||||||
|
for response in responses {
|
||||||
|
handlers[responseID(response.id) ?? ""] = response.action
|
||||||
|
}
|
||||||
|
storage.fields[Self.callbacks + id] = handlers
|
||||||
|
responsesCosmetics(pointer: pointer)
|
||||||
|
if present {
|
||||||
|
gtui_alertdialog_choose(
|
||||||
|
.init(Int(bitPattern: pointer)),
|
||||||
|
unsafeBitCast(storage, to: UInt64.self),
|
||||||
|
.init(Int(bitPattern: storage.pointer))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if storage.content[Self.dialogID + id]?.first != nil {
|
||||||
|
adw_dialog_close(storage.content[Self.dialogID + id]?.first?.pointer?.cast())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Style the responses and add shortcuts if required.
|
||||||
|
/// - Parameter pointer: The pointer.
|
||||||
|
func responsesCosmetics(pointer: OpaquePointer?) {
|
||||||
|
for element in responses {
|
||||||
|
switch element.appearance {
|
||||||
|
case .default:
|
||||||
|
adw_alert_dialog_set_response_appearance(
|
||||||
|
pointer?.cast(),
|
||||||
|
responseID(element.id),
|
||||||
|
ADW_RESPONSE_DEFAULT
|
||||||
|
)
|
||||||
|
case .suggested:
|
||||||
|
adw_alert_dialog_set_response_appearance(
|
||||||
|
pointer?.cast(),
|
||||||
|
responseID(element.id),
|
||||||
|
ADW_RESPONSE_SUGGESTED
|
||||||
|
)
|
||||||
|
case .destructive:
|
||||||
|
adw_alert_dialog_set_response_appearance(
|
||||||
|
pointer?.cast(),
|
||||||
|
responseID(element.id),
|
||||||
|
ADW_RESPONSE_DESTRUCTIVE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let closeResponse = responses.first(where: { $0.role == .close }) ?? responses.first {
|
||||||
|
adw_alert_dialog_set_close_response(pointer?.cast(), responseID(closeResponse.id))
|
||||||
|
}
|
||||||
|
if let defaultResponse = responses.first(where: { $0.role == .default }) {
|
||||||
|
adw_alert_dialog_set_default_response(pointer?.cast(), responseID(defaultResponse.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new instance of the dialog.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - storage: The wrapped view's storage.
|
||||||
|
/// - modifiers: The view modifiers.
|
||||||
|
func createDialog(storage: ViewStorage, modifiers: [(View) -> View]) {
|
||||||
|
let pointer = adw_alert_dialog_new(nil, nil)
|
||||||
|
let dialog = ViewStorage(pointer?.opaque())
|
||||||
|
storage.content[Self.dialogID + id] = [dialog]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the identifier of a response which is combined with the dialog's id.
|
||||||
|
/// - Parameter id: The response identifier.
|
||||||
|
/// - Returns: The new identifier.
|
||||||
|
func responseID(_ id: String?) -> String? {
|
||||||
|
if let id {
|
||||||
|
return self.id + "...." + id
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a response to the alert dialog.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - title: The response.
|
||||||
|
/// - appearance: The response's appearance.
|
||||||
|
/// - role: The response's shortcut, if any.
|
||||||
|
/// - action: The
|
||||||
|
public func response(
|
||||||
|
_ title: String,
|
||||||
|
appearance: ResponseAppearance = .default,
|
||||||
|
role: ResponseRole? = nil,
|
||||||
|
action: @escaping () -> Void
|
||||||
|
) -> Self {
|
||||||
|
var newSelf = self
|
||||||
|
newSelf.responses.append(.init(title: title, appearance: appearance, role: role, action: action))
|
||||||
|
return newSelf
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
|
||||||
|
/// Add an alert dialog to the parent window.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - visible: Whether the dialog is presented.
|
||||||
|
/// - heading: The heading.
|
||||||
|
/// - body: The body text.
|
||||||
|
public func alertDialog(
|
||||||
|
visible: Binding<Bool>,
|
||||||
|
heading: String,
|
||||||
|
body: String = "",
|
||||||
|
id: String? = nil
|
||||||
|
) -> AlertDialog {
|
||||||
|
.init(
|
||||||
|
visible: visible,
|
||||||
|
id: id ?? "no-id",
|
||||||
|
heading: heading,
|
||||||
|
body: body,
|
||||||
|
child: self
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run when an alert dialog closes.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - ptr: The pointer.
|
||||||
|
/// - answer: The identifier of the answer.
|
||||||
|
/// - userData: The alert dialog data.
|
||||||
|
@_cdecl("alertdialog_on_close_cb")
|
||||||
|
func alertdialog_on_close_cb(
|
||||||
|
ptr: UnsafeMutableRawPointer,
|
||||||
|
answer: UnsafePointer<CChar>?,
|
||||||
|
userData: UnsafeMutableRawPointer
|
||||||
|
) {
|
||||||
|
let storage = Unmanaged<ViewStorage>.fromOpaque(userData).takeUnretainedValue()
|
||||||
|
var id = ""
|
||||||
|
if let answer {
|
||||||
|
let answer = String(cString: answer)
|
||||||
|
id = .init(answer.split(separator: "....").first ?? "")
|
||||||
|
(storage.fields[AlertDialog.callbacks + id] as? [String: () -> Void])?[answer]?()
|
||||||
|
}
|
||||||
|
storage.content[AlertDialog.dialogID + id] = []
|
||||||
|
storage.fields[AlertDialog.responsesID + id] = []
|
||||||
|
if let visible = storage.fields[AlertDialog.visibleID + id] as? Binding<Bool>, visible.wrappedValue {
|
||||||
|
visible.wrappedValue = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,6 +5,8 @@ static void
|
|||||||
filedialog_on_open_cb (void *, void *, void *);
|
filedialog_on_open_cb (void *, void *, void *);
|
||||||
static void
|
static void
|
||||||
filedialog_on_save_cb (void *, void *, void *);
|
filedialog_on_save_cb (void *, void *, void *);
|
||||||
|
static void
|
||||||
|
alertdialog_on_close_cb (void *, void *, void *);
|
||||||
|
|
||||||
static void
|
static void
|
||||||
gtui_filedialog_save_finish (uint64_t dialog, uint64_t result, uint64_t data)
|
gtui_filedialog_save_finish (uint64_t dialog, uint64_t result, uint64_t data)
|
||||||
@ -35,3 +37,17 @@ gtui_filedialog_open (uint64_t dialog, uint64_t data, uint64_t window)
|
|||||||
swift_retain (data);
|
swift_retain (data);
|
||||||
gtk_file_dialog_open (dialog, window, NULL, G_CALLBACK (gtui_filedialog_open_finish), (void *)data);
|
gtk_file_dialog_open (dialog, window, NULL, G_CALLBACK (gtui_filedialog_open_finish), (void *)data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
gtui_alertdialog_cb (uint64_t dialog, uint64_t result, uint64_t data)
|
||||||
|
{
|
||||||
|
const char *response = adw_alert_dialog_choose_finish (dialog, result);
|
||||||
|
alertdialog_on_close_cb (dialog, response, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
gtui_alertdialog_choose (uint64_t dialog, uint64_t data, uint64_t parent)
|
||||||
|
{
|
||||||
|
adw_alert_dialog_choose (dialog, parent, NULL, gtui_alertdialog_cb, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
37
Tests/AlertDialogDemo.swift
Normal file
37
Tests/AlertDialogDemo.swift
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
//
|
||||||
|
// AlertDialogDemo.swift
|
||||||
|
// Adwaita
|
||||||
|
//
|
||||||
|
// Created by david-swift on 05.04.24.
|
||||||
|
//
|
||||||
|
|
||||||
|
// swiftlint:disable missing_docs
|
||||||
|
|
||||||
|
import Adwaita
|
||||||
|
|
||||||
|
struct AlertDialogDemo: View {
|
||||||
|
|
||||||
|
@State private var dialog = false
|
||||||
|
let padding = 20
|
||||||
|
|
||||||
|
var view: Body {
|
||||||
|
VStack {
|
||||||
|
Button("Show Dialog") {
|
||||||
|
dialog = true
|
||||||
|
}
|
||||||
|
.style("pill")
|
||||||
|
.frame(maxSize: 100)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.alertDialog(visible: $dialog, heading: "Alert Dialog", body: "This is an alert dialog")
|
||||||
|
.response("Cancel", role: .close) {
|
||||||
|
print("Cancel")
|
||||||
|
}
|
||||||
|
.response("Done", appearance: .suggested, role: .default) {
|
||||||
|
print("Done")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// swiftlint:enable missing_docs
|
||||||
@ -18,6 +18,7 @@ enum Page: String, Identifiable, CaseIterable, Codable {
|
|||||||
case transition
|
case transition
|
||||||
case dice
|
case dice
|
||||||
case dialog
|
case dialog
|
||||||
|
case alertDialog
|
||||||
case toast
|
case toast
|
||||||
case list
|
case list
|
||||||
case carousel
|
case carousel
|
||||||
@ -39,6 +40,8 @@ enum Page: String, Identifiable, CaseIterable, Codable {
|
|||||||
return "Flow Box"
|
return "Flow Box"
|
||||||
case .navigationView:
|
case .navigationView:
|
||||||
return "Navigation View"
|
return "Navigation View"
|
||||||
|
case .alertDialog:
|
||||||
|
return "Alert Dialog"
|
||||||
default:
|
default:
|
||||||
return rawValue.capitalized
|
return rawValue.capitalized
|
||||||
}
|
}
|
||||||
@ -69,6 +72,8 @@ enum Page: String, Identifiable, CaseIterable, Codable {
|
|||||||
return "Roll the dice"
|
return "Roll the dice"
|
||||||
case .dialog:
|
case .dialog:
|
||||||
return "A window on top of another window"
|
return "A window on top of another window"
|
||||||
|
case .alertDialog:
|
||||||
|
return "A dialog presenting a message or question"
|
||||||
case .toast:
|
case .toast:
|
||||||
return "Show a notification inside of your app"
|
return "Show a notification inside of your app"
|
||||||
case .list:
|
case .list:
|
||||||
@ -106,6 +111,8 @@ enum Page: String, Identifiable, CaseIterable, Codable {
|
|||||||
DiceDemo()
|
DiceDemo()
|
||||||
case .dialog:
|
case .dialog:
|
||||||
DialogDemo()
|
DialogDemo()
|
||||||
|
case .alertDialog:
|
||||||
|
AlertDialogDemo()
|
||||||
case .toast:
|
case .toast:
|
||||||
ToastDemo(toast: toast)
|
ToastDemo(toast: toast)
|
||||||
case .list:
|
case .list:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user