Skip to content

Commit 052cd68

Browse files
committed
Implement .alert view modifier (AppKit, Gtk 4, Gtk 3)
1 parent 95aaf07 commit 052cd68

File tree

21 files changed

+1185
-32
lines changed

21 files changed

+1185
-32
lines changed

Sources/AppKitBackend/AppKitBackend.swift

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ extension App {
77

88
public final class AppKitBackend: AppBackend {
99
public typealias Window = NSCustomWindow
10-
1110
public typealias Widget = NSView
12-
1311
public typealias Menu = NSMenu
12+
public typealias Alert = NSAlert
1413

1514
public let defaultTableRowContentHeight = 20
1615
public let defaultTableCellVerticalPadding = 4
@@ -868,6 +867,47 @@ public final class AppKitBackend: AppBackend {
868867
)
869868
handleClose()
870869
}
870+
871+
public func createAlert() -> Alert {
872+
NSAlert()
873+
}
874+
875+
public func updateAlert(
876+
_ alert: Alert,
877+
title: String,
878+
actionLabels: [String],
879+
environment: Environment
880+
) {
881+
alert.messageText = title
882+
for label in actionLabels {
883+
alert.addButton(withTitle: label)
884+
}
885+
}
886+
887+
public func showAlert(
888+
_ alert: Alert,
889+
window: Window,
890+
responseHandler handleResponse: @escaping (Int) -> Void
891+
) {
892+
alert.beginSheetModal(for: window) { response in
893+
guard response != .stop, response != .continue else {
894+
return
895+
}
896+
897+
guard response != .abort, response != .cancel else {
898+
print("warning: Got abort or cancel modal response, unexpected and unhandled")
899+
return
900+
}
901+
902+
let firstButton = NSApplication.ModalResponse.alertFirstButtonReturn.rawValue
903+
let action = response.rawValue - firstButton
904+
handleResponse(action)
905+
}
906+
}
907+
908+
public func dismissAlert(_ alert: Alert, window: Window) {
909+
window.endSheet(alert.window)
910+
}
871911
}
872912

873913
final class NSCustomMenuItem: NSMenuItem {

Sources/Gtk/Widgets/Dialog.swift

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import CGtk
2+
3+
/// Dialogs are a convenient way to prompt the user for a small amount
4+
/// of input.
5+
///
6+
/// ![An example GtkDialog](dialog.png)
7+
///
8+
/// Typical uses are to display a message, ask a question, or anything else
9+
/// that does not require extensive effort on the user’s part.
10+
///
11+
/// The main area of a `GtkDialog` is called the "content area", and is yours
12+
/// to populate with widgets such a `GtkLabel` or `GtkEntry`, to present
13+
/// your information, questions, or tasks to the user.
14+
///
15+
/// In addition, dialogs allow you to add "action widgets". Most commonly,
16+
/// action widgets are buttons. Depending on the platform, action widgets may
17+
/// be presented in the header bar at the top of the window, or at the bottom
18+
/// of the window. To add action widgets, create your `GtkDialog` using
19+
/// [ctor@Gtk.Dialog.new_with_buttons], or use
20+
/// [method@Gtk.Dialog.add_button], [method@Gtk.Dialog.add_buttons],
21+
/// or [method@Gtk.Dialog.add_action_widget].
22+
///
23+
/// `GtkDialogs` uses some heuristics to decide whether to add a close
24+
/// button to the window decorations. If any of the action buttons use
25+
/// the response ID %GTK_RESPONSE_CLOSE or %GTK_RESPONSE_CANCEL, the
26+
/// close button is omitted.
27+
///
28+
/// Clicking a button that was added as an action widget will emit the
29+
/// [signal@Gtk.Dialog::response] signal with a response ID that you specified.
30+
/// GTK will never assign a meaning to positive response IDs; these are
31+
/// entirely user-defined. But for convenience, you can use the response
32+
/// IDs in the [enum@Gtk.ResponseType] enumeration (these all have values
33+
/// less than zero). If a dialog receives a delete event, the
34+
/// [signal@Gtk.Dialog::response] signal will be emitted with the
35+
/// %GTK_RESPONSE_DELETE_EVENT response ID.
36+
///
37+
/// Dialogs are created with a call to [ctor@Gtk.Dialog.new] or
38+
/// [ctor@Gtk.Dialog.new_with_buttons]. The latter is recommended; it allows
39+
/// you to set the dialog title, some convenient flags, and add buttons.
40+
///
41+
/// A “modal” dialog (that is, one which freezes the rest of the application
42+
/// from user input), can be created by calling [method@Gtk.Window.set_modal]
43+
/// on the dialog. When using [ctor@Gtk.Dialog.new_with_buttons], you can also
44+
/// pass the %GTK_DIALOG_MODAL flag to make a dialog modal.
45+
///
46+
/// For the simple dialog in the following example, a [class@Gtk.MessageDialog]
47+
/// would save some effort. But you’d need to create the dialog contents manually
48+
/// if you had more than a simple message in the dialog.
49+
///
50+
/// An example for simple `GtkDialog` usage:
51+
///
52+
/// ```c
53+
/// // Function to open a dialog box with a message
54+
/// void
55+
/// quick_message (GtkWindow *parent, char *message)
56+
/// {
57+
/// GtkWidget *dialog, *label, *content_area;
58+
/// GtkDialogFlags flags;
59+
///
60+
/// // Create the widgets
61+
/// flags = GTK_DIALOG_DESTROY_WITH_PARENT;
62+
/// dialog = gtk_dialog_new_with_buttons ("Message",
63+
/// parent,
64+
/// flags,
65+
/// _("_OK"),
66+
/// GTK_RESPONSE_NONE,
67+
/// NULL);
68+
/// content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
69+
/// label = gtk_label_new (message);
70+
///
71+
/// // Ensure that the dialog box is destroyed when the user responds
72+
///
73+
/// g_signal_connect_swapped (dialog,
74+
/// "response",
75+
/// G_CALLBACK (gtk_window_destroy),
76+
/// dialog);
77+
///
78+
/// // Add the label, and show everything we’ve added
79+
///
80+
/// gtk_box_append (GTK_BOX (content_area), label);
81+
/// gtk_widget_show (dialog);
82+
/// }
83+
/// ```
84+
///
85+
/// # GtkDialog as GtkBuildable
86+
///
87+
/// The `GtkDialog` implementation of the `GtkBuildable` interface exposes the
88+
/// @content_area as an internal child with the name “content_area”.
89+
///
90+
/// `GtkDialog` supports a custom `<action-widgets>` element, which can contain
91+
/// multiple `<action-widget>` elements. The “response” attribute specifies a
92+
/// numeric response, and the content of the element is the id of widget
93+
/// (which should be a child of the dialogs @action_area). To mark a response
94+
/// as default, set the “default” attribute of the `<action-widget>` element
95+
/// to true.
96+
///
97+
/// `GtkDialog` supports adding action widgets by specifying “action” as
98+
/// the “type” attribute of a `<child>` element. The widget will be added
99+
/// either to the action area or the headerbar of the dialog, depending
100+
/// on the “use-header-bar” property. The response id has to be associated
101+
/// with the action widget using the `<action-widgets>` element.
102+
///
103+
/// An example of a `GtkDialog` UI definition fragment:
104+
///
105+
/// ```xml
106+
/// <object class="GtkDialog" id="dialog1"><child type="action"><object class="GtkButton" id="button_cancel"/></child><child type="action"><object class="GtkButton" id="button_ok"></object></child><action-widgets><action-widget response="cancel">button_cancel</action-widget><action-widget response="ok" default="true">button_ok</action-widget></action-widgets></object>
107+
/// ```
108+
///
109+
/// # Accessibility
110+
///
111+
/// `GtkDialog` uses the %GTK_ACCESSIBLE_ROLE_DIALOG role.
112+
open class Dialog: Window {
113+
/// Creates a new dialog box.
114+
///
115+
/// Widgets should not be packed into the `GtkWindow`
116+
/// directly, but into the @content_area and @action_area,
117+
/// as described above.
118+
override public init() {
119+
super.init()
120+
widgetPointer = gtk_dialog_new()
121+
}
122+
123+
func registerSignalHandlers() {
124+
removeSignals()
125+
126+
super.didMoveToParent()
127+
128+
addSignal(name: "close") { [weak self] () in
129+
guard let self = self else { return }
130+
self.close?(self)
131+
}
132+
133+
let handler1:
134+
@convention(c) (UnsafeMutableRawPointer, Int, UnsafeMutableRawPointer) -> Void =
135+
{ _, value1, data in
136+
SignalBox1<Int>.run(data, value1)
137+
}
138+
139+
addSignal(name: "response", handler: gCallback(handler1)) { [weak self] (responseId: Int) in
140+
guard let self = self else { return }
141+
self.response?(self, responseId)
142+
}
143+
144+
let handler2:
145+
@convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void =
146+
{ _, value1, data in
147+
SignalBox1<OpaquePointer>.run(data, value1)
148+
}
149+
150+
addSignal(name: "notify::use-header-bar", handler: gCallback(handler2)) {
151+
[weak self] (_: OpaquePointer) in
152+
guard let self = self else { return }
153+
self.notifyUseHeaderBar?(self)
154+
}
155+
}
156+
157+
public func addButton(label: String, responseId: Int) {
158+
gtk_dialog_add_button(
159+
castedPointer(),
160+
label,
161+
Int32(responseId)
162+
)
163+
}
164+
165+
/// Show the dialog and set up signal handlers.
166+
override public func show() {
167+
registerSignalHandlers()
168+
super.show()
169+
}
170+
171+
/// Emitted when the user uses a keybinding to close the dialog.
172+
///
173+
/// This is a [keybinding signal](class.SignalAction.html).
174+
///
175+
/// The default binding for this signal is the Escape key.
176+
public var close: ((Dialog) -> Void)?
177+
178+
/// Emitted when an action widget is clicked.
179+
///
180+
/// The signal is also emitted when the dialog receives a
181+
/// delete event, and when [method@Gtk.Dialog.response] is called.
182+
/// On a delete event, the response ID is %GTK_RESPONSE_DELETE_EVENT.
183+
/// Otherwise, it depends on which action widget was clicked.
184+
public var response: ((Dialog, Int) -> Void)?
185+
186+
public var notifyUseHeaderBar: ((Dialog) -> Void)?
187+
}

0 commit comments

Comments
 (0)