Skip to content

Commit 8a39cac

Browse files
committed
ActionUIAppKitApplication: add alert API
actionUIAppRunAlert() allows clients to present a modal dialog with configurable title, message, alert style and buttons
1 parent bafc5e0 commit 8a39cac

3 files changed

Lines changed: 117 additions & 0 deletions

File tree

ActionUIAppKitApplication/ActionUIApp.swift

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,66 @@ private func actionStrdup(_ string: String) -> UnsafeMutablePointer<CChar> {
433433
return string.withCString { strdup($0)! }
434434
}
435435

436+
// MARK: - Alert dialog
437+
438+
/// Run a modal alert dialog and return the title of the button that was clicked.
439+
///
440+
/// Config JSON keys (all optional):
441+
/// title: String — bold heading text
442+
/// message: String — informative text below the title
443+
/// style: String — "informational" (default), "warning", or "critical"
444+
/// buttons: [String] — button titles, first is the default (rightmost),
445+
/// second is placed to its left, etc.
446+
/// Defaults to ["OK"] if omitted.
447+
///
448+
/// Returns the title of the clicked button as a C string, or nil on error.
449+
@_cdecl("actionUIAppRunAlert")
450+
public func actionUIAppRunAlert(
451+
_ configJSON: UnsafePointer<CChar>?
452+
) -> UnsafeMutablePointer<CChar>? {
453+
let jsonString = configJSON.map { String(cString: $0) }
454+
return runOnMainActorSync {
455+
var config: [String: Any] = [:]
456+
if let json = jsonString,
457+
let data = json.data(using: .utf8),
458+
let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
459+
config = parsed
460+
}
461+
462+
let alert = NSAlert()
463+
464+
if let title = config["title"] as? String {
465+
alert.messageText = title
466+
}
467+
if let message = config["message"] as? String {
468+
alert.informativeText = message
469+
}
470+
471+
switch config["style"] as? String {
472+
case "warning":
473+
alert.alertStyle = .warning
474+
case "critical":
475+
alert.alertStyle = .critical
476+
default:
477+
alert.alertStyle = .informational
478+
}
479+
480+
let buttons = config["buttons"] as? [String] ?? ["OK"]
481+
for title in buttons {
482+
alert.addButton(withTitle: title)
483+
}
484+
485+
let response = alert.runModal()
486+
487+
// NSAlert buttons are numbered .alertFirstButtonReturn, +1, +2, …
488+
let index = response.rawValue - NSApplication.ModalResponse.alertFirstButtonReturn.rawValue
489+
if index >= 0 && index < buttons.count {
490+
return actionStrdup(buttons[index])
491+
}
492+
return nil
493+
}
494+
}
495+
436496
// MARK: - File panels (NSOpenPanel / NSSavePanel)
437497

438498
/// Parse a JSON config dictionary and configure an `NSOpenPanel`.

ActionUIPython/actionui.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,51 @@ def _build_panel_config(**kwargs) -> Dict[str, Any]:
469469
config["allowsOtherFileTypes"] = True
470470
return config
471471

472+
# ------------------------------------------------------------------
473+
# Alert dialog
474+
# ------------------------------------------------------------------
475+
476+
def alert(self, *,
477+
title: Optional[str] = None,
478+
message: Optional[str] = None,
479+
style: str = "informational",
480+
buttons: Optional[List[str]] = None) -> Optional[str]:
481+
"""Run a modal alert dialog.
482+
483+
Args:
484+
title: Bold heading text.
485+
message: Informative text below the title.
486+
style: ``"informational"`` (default), ``"warning"``, or
487+
``"critical"``.
488+
buttons: List of button titles. The first is the default
489+
(rightmost) button. Defaults to ``["OK"]``.
490+
491+
Returns:
492+
The title of the clicked button, or ``None`` on error.
493+
494+
Example::
495+
496+
result = app.alert(
497+
title="Replace Pipeline?",
498+
message="The current pipeline is not empty.",
499+
style="warning",
500+
buttons=["Replace", "Cancel"],
501+
)
502+
if result == "Replace":
503+
...
504+
"""
505+
config: Dict[str, Any] = {}
506+
if title is not None:
507+
config["title"] = title
508+
if message is not None:
509+
config["message"] = message
510+
if style != "informational":
511+
config["style"] = style
512+
if buttons is not None:
513+
config["buttons"] = buttons
514+
config_json = json.dumps(config) if config else None
515+
return _actionui.app_run_alert(config_json)
516+
472517
# ------------------------------------------------------------------
473518
# Menu bar
474519
# ------------------------------------------------------------------

ActionUIPython/actionui_native.m

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,16 @@ static void app_window_will_present_bridge(const char* windowUUID) {
352352
return py_result;
353353
}
354354

355+
static PyObject* py_app_run_alert(PyObject* self, PyObject* args) {
356+
const char* configJSON = NULL;
357+
if (PyArg_ParseTuple(args, "|z", &configJSON) == 0) return NULL;
358+
char* result = actionUIAppRunAlert(configJSON);
359+
if (result == NULL) Py_RETURN_NONE;
360+
PyObject* py_result = PyUnicode_FromString(result);
361+
actionUIFreeString(result);
362+
return py_result;
363+
}
364+
355365
// MARK: - Python API: Version
356366

357367
static PyObject* py_get_version(PyObject* self, PyObject* args) {
@@ -879,6 +889,8 @@ static void app_window_will_present_bridge(const char* windowUUID) {
879889
"app_run_open_panel([configJSON]) -> str|None — run NSOpenPanel; returns JSON array of paths or None."},
880890
{"app_run_save_panel", py_app_run_save_panel, METH_VARARGS,
881891
"app_run_save_panel([configJSON]) -> str|None — run NSSavePanel; returns path or None."},
892+
{"app_run_alert", py_app_run_alert, METH_VARARGS,
893+
"app_run_alert([configJSON]) -> str|None — run modal NSAlert; returns clicked button title or None."},
882894

883895
{NULL, NULL, 0, NULL}
884896
};

0 commit comments

Comments
 (0)