Skip to content

Commit 0f2d23d

Browse files
committed
Update modal impls to work outside of scenes (falling back to app modal)
Alerts and file dialogs previously required a window to get attached to, but now I've updated the AppBackend API to make the window optional, and updated the backends to fallback to either making them whole-app modals, or selecting a specific 'main window' to attach the top-level modals to.
1 parent 2644ee1 commit 0f2d23d

File tree

8 files changed

+105
-76
lines changed

8 files changed

+105
-76
lines changed

Sources/AppKitBackend/AppKitBackend.swift

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -890,10 +890,10 @@ public final class AppKitBackend: AppBackend {
890890

891891
public func showAlert(
892892
_ alert: Alert,
893-
window: Window,
893+
window: Window?,
894894
responseHandler handleResponse: @escaping (Int) -> Void
895895
) {
896-
alert.beginSheetModal(for: window) { response in
896+
let completionHandler: (NSApplication.ModalResponse) -> Void = { response in
897897
guard response != .stop, response != .continue else {
898898
return
899899
}
@@ -907,16 +907,30 @@ public final class AppKitBackend: AppBackend {
907907
let action = response.rawValue - firstButton
908908
handleResponse(action)
909909
}
910+
911+
if let window {
912+
alert.beginSheetModal(
913+
for: window,
914+
completionHandler: completionHandler
915+
)
916+
} else {
917+
let response = alert.runModal()
918+
completionHandler(response)
919+
}
910920
}
911921

912-
public func dismissAlert(_ alert: Alert, window: Window) {
913-
window.endSheet(alert.window)
922+
public func dismissAlert(_ alert: Alert, window: Window?) {
923+
if let window {
924+
window.endSheet(alert.window)
925+
} else {
926+
NSApplication.shared.stopModal()
927+
}
914928
}
915929

916930
public func showOpenDialog(
917931
fileDialogOptions: FileDialogOptions,
918932
openDialogOptions: OpenDialogOptions,
919-
window: Window,
933+
window: Window?,
920934
resultHandler handleResult: @escaping (DialogResult<[URL]>) -> Void
921935
) {
922936
let panel = NSOpenPanel()
@@ -932,7 +946,7 @@ public final class AppKitBackend: AppBackend {
932946
panel.canChooseFiles = openDialogOptions.allowSelectingFiles
933947
panel.canChooseDirectories = openDialogOptions.allowSelectingDirectories
934948

935-
panel.beginSheetModal(for: window) { response in
949+
let handleResponse: (NSApplication.ModalResponse) -> Void = { response in
936950
guard response != .continue else {
937951
return
938952
}
@@ -943,12 +957,19 @@ public final class AppKitBackend: AppBackend {
943957
handleResult(.cancelled)
944958
}
945959
}
960+
961+
if let window {
962+
panel.beginSheetModal(for: window, completionHandler: handleResponse)
963+
} else {
964+
let response = panel.runModal()
965+
handleResponse(response)
966+
}
946967
}
947968

948969
public func showSaveDialog(
949970
fileDialogOptions: FileDialogOptions,
950971
saveDialogOptions: SaveDialogOptions,
951-
window: Window,
972+
window: Window?,
952973
resultHandler handleResult: @escaping (DialogResult<URL>) -> Void
953974
) {
954975
let panel = NSSavePanel()
@@ -963,7 +984,7 @@ public final class AppKitBackend: AppBackend {
963984
panel.nameFieldLabel = saveDialogOptions.nameFieldLabel ?? panel.nameFieldLabel
964985
panel.nameFieldStringValue = saveDialogOptions.defaultFileName ?? ""
965986

966-
panel.beginSheetModal(for: window) { response in
987+
let handleResponse: (NSApplication.ModalResponse) -> Void = { response in
967988
guard response != .continue else {
968989
return
969990
}
@@ -974,6 +995,13 @@ public final class AppKitBackend: AppBackend {
974995
handleResult(.cancelled)
975996
}
976997
}
998+
999+
if let window {
1000+
panel.beginSheetModal(for: window, completionHandler: handleResponse)
1001+
} else {
1002+
let response = panel.runModal()
1003+
handleResponse(response)
1004+
}
9771005
}
9781006
}
9791007

Sources/Gtk3Backend/Gtk3Backend.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,7 @@ public final class Gtk3Backend: AppBackend {
760760

761761
public func showAlert(
762762
_ alert: Alert,
763-
window: Window,
763+
window: Window?,
764764
responseHandler handleResponse: @escaping (Int) -> Void
765765
) {
766766
alert.response = { _, responseId in
@@ -769,18 +769,18 @@ public final class Gtk3Backend: AppBackend {
769769
}
770770
alert.isModal = true
771771
alert.isDecorated = false
772-
alert.setTransient(for: window)
772+
alert.setTransient(for: window ?? windows[0])
773773
alert.show()
774774
}
775775

776-
public func dismissAlert(_ alert: Alert, window: Window) {
776+
public func dismissAlert(_ alert: Alert, window: Window?) {
777777
alert.destroy()
778778
}
779779

780780
public func showOpenDialog(
781781
fileDialogOptions: FileDialogOptions,
782782
openDialogOptions: OpenDialogOptions,
783-
window: Window,
783+
window: Window?,
784784
resultHandler handleResult: @escaping (DialogResult<[URL]>) -> Void
785785
) {
786786
showFileChooserDialog(
@@ -797,7 +797,7 @@ public final class Gtk3Backend: AppBackend {
797797
public func showSaveDialog(
798798
fileDialogOptions: FileDialogOptions,
799799
saveDialogOptions: SaveDialogOptions,
800-
window: Window,
800+
window: Window?,
801801
resultHandler handleResult: @escaping (DialogResult<URL>) -> Void
802802
) {
803803
showFileChooserDialog(
@@ -824,12 +824,12 @@ public final class Gtk3Backend: AppBackend {
824824
fileDialogOptions: FileDialogOptions,
825825
action: FileChooserAction,
826826
configure: (Gtk3.FileChooserNative) -> Void,
827-
window: Window,
827+
window: Window?,
828828
resultHandler handleResult: @escaping (DialogResult<[URL]>) -> Void
829829
) {
830830
let chooser = Gtk3.FileChooserNative(
831831
title: fileDialogOptions.title,
832-
parent: window.widgetPointer.cast(),
832+
parent: window?.widgetPointer.cast(),
833833
action: action.toGtk(),
834834
acceptLabel: fileDialogOptions.defaultButtonLabel,
835835
cancelLabel: "Cancel"

Sources/GtkBackend/GtkBackend.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -778,7 +778,7 @@ public final class GtkBackend: AppBackend {
778778

779779
public func showAlert(
780780
_ alert: Alert,
781-
window: Window,
781+
window: Window?,
782782
responseHandler handleResponse: @escaping (Int) -> Void
783783
) {
784784
alert.response = { _, responseId in
@@ -787,18 +787,18 @@ public final class GtkBackend: AppBackend {
787787
}
788788
alert.isModal = true
789789
alert.isDecorated = false
790-
alert.setTransient(for: window)
790+
alert.setTransient(for: window ?? windows[0])
791791
alert.show()
792792
}
793793

794-
public func dismissAlert(_ alert: Alert, window: Window) {
794+
public func dismissAlert(_ alert: Alert, window: Window?) {
795795
alert.destroy()
796796
}
797797

798798
public func showOpenDialog(
799799
fileDialogOptions: FileDialogOptions,
800800
openDialogOptions: OpenDialogOptions,
801-
window: Window,
801+
window: Window?,
802802
resultHandler handleResult: @escaping (DialogResult<[URL]>) -> Void
803803
) {
804804
showFileChooserDialog(
@@ -815,7 +815,7 @@ public final class GtkBackend: AppBackend {
815815
public func showSaveDialog(
816816
fileDialogOptions: FileDialogOptions,
817817
saveDialogOptions: SaveDialogOptions,
818-
window: Window,
818+
window: Window?,
819819
resultHandler handleResult: @escaping (DialogResult<URL>) -> Void
820820
) {
821821
showFileChooserDialog(
@@ -842,12 +842,12 @@ public final class GtkBackend: AppBackend {
842842
fileDialogOptions: FileDialogOptions,
843843
action: FileChooserAction,
844844
configure: (Gtk.FileChooserNative) -> Void,
845-
window: Window,
845+
window: Window?,
846846
resultHandler handleResult: @escaping (DialogResult<[URL]>) -> Void
847847
) {
848848
let chooser = Gtk.FileChooserNative(
849849
title: fileDialogOptions.title,
850-
parent: window.widgetPointer.cast(),
850+
parent: window?.widgetPointer.cast(),
851851
action: action.toGtk(),
852852
acceptLabel: fileDialogOptions.defaultButtonLabel,
853853
cancelLabel: "Cancel"

Sources/SwiftCrossUI/Backend/AppBackend.swift

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -415,33 +415,40 @@ public protocol AppBackend {
415415
/// have been hidden by the time the response handler gets called.
416416
///
417417
/// Must only get called once for any given alert.
418+
///
419+
/// If `window` is `nil`, the backend can either make the alert a whole
420+
/// app modal, a standalone window, or a modal for a window of its choosing.
418421
func showAlert(
419422
_ alert: Alert,
420-
window: Window,
423+
window: Window?,
421424
responseHandler handleResponse: @escaping (Int) -> Void
422425
)
423426
/// Dismisses an alert programmatically without invoking the response
424427
/// handler. Must only be called after
425428
/// ``showAlert(_:window:responseHandler:)``.
426-
///
427-
/// Must only get called once for any given alert.
428-
func dismissAlert(_ alert: Alert, window: Window)
429+
func dismissAlert(_ alert: Alert, window: Window?)
429430

430431
/// Presents an 'Open file' dialog to the user for selecting files or
431432
/// folders.
433+
///
434+
/// If `window` is `nil`, the backend can either make the dialog a whole
435+
/// app modal, a standalone window, or a modal for a window of its choosing.
432436
func showOpenDialog(
433437
fileDialogOptions: FileDialogOptions,
434438
openDialogOptions: OpenDialogOptions,
435-
window: Window,
439+
window: Window?,
436440
resultHandler handleResult: @escaping (DialogResult<[URL]>) -> Void
437441
)
438442

439443
/// Presents a 'Save file' dialog to the user for selecting a file save
440444
/// destination.
445+
///
446+
/// If `window` is `nil`, the backend can either make the dialog a whole
447+
/// app modal, a standalone window, or a modal for a window of its choosing.
441448
func showSaveDialog(
442449
fileDialogOptions: FileDialogOptions,
443450
saveDialogOptions: SaveDialogOptions,
444-
window: Window,
451+
window: Window?,
445452
resultHandler handleResult: @escaping (DialogResult<URL>) -> Void
446453
)
447454
}
@@ -705,27 +712,27 @@ extension AppBackend {
705712
}
706713
func showAlert(
707714
_ alert: Alert,
708-
window: Window,
715+
window: Window?,
709716
responseHandler handleResponse: @escaping (Int) -> Void
710717
) {
711718
todo()
712719
}
713-
func dismissAlert(_ alert: Alert, window: Window) {
720+
func dismissAlert(_ alert: Alert, window: Window?) {
714721
todo()
715722
}
716723

717724
public func showOpenDialog(
718725
fileDialogOptions: FileDialogOptions,
719726
openDialogOptions: OpenDialogOptions,
720-
window: Window,
727+
window: Window?,
721728
resultHandler handleResult: @escaping (DialogResult<[URL]>) -> Void
722729
) {
723730
todo()
724731
}
725732
public func showSaveDialog(
726733
fileDialogOptions: FileDialogOptions,
727734
saveDialogOptions: SaveDialogOptions,
728-
window: Window,
735+
window: Window?,
729736
resultHandler handleResult: @escaping (DialogResult<URL>) -> Void
730737
) {
731738
todo()

Sources/SwiftCrossUI/Environment/Actions/PresentAlertAction.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,15 @@ public struct PresentAlertAction {
2323
actionLabels: actions.map(\.label),
2424
environment: environment
2525
)
26+
let window: Backend.Window? =
27+
if let window = environment.window {
28+
.some(window as! Backend.Window)
29+
} else {
30+
nil
31+
}
2632
backend.showAlert(
2733
alert,
28-
window: environment.window! as! Backend.Window
34+
window: window
2935
) { actionIndex in
3036
actions[actionIndex].action()
3137
continuation.resume(returning: actionIndex)

Sources/SwiftCrossUI/Environment/Actions/PresentFileSaveDialogAction.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Foundation
44
/// `nil` if the user cancels the operation.
55
public struct PresentFileSaveDialogAction {
66
let backend: any AppBackend
7-
let window: Any
7+
let window: Any?
88

99
public func callAsFunction(
1010
title: String = "Save",
@@ -18,6 +18,13 @@ public struct PresentFileSaveDialogAction {
1818
func chooseFile<Backend: AppBackend>(backend: Backend) async -> URL? {
1919
return await withCheckedContinuation { continuation in
2020
backend.runInMainThread {
21+
let window: Backend.Window? =
22+
if let window = self.window {
23+
.some(window as! Backend.Window)
24+
} else {
25+
nil
26+
}
27+
2128
backend.showSaveDialog(
2229
fileDialogOptions: FileDialogOptions(
2330
title: title,
@@ -31,7 +38,7 @@ public struct PresentFileSaveDialogAction {
3138
nameFieldLabel: nameFieldLabel,
3239
defaultFileName: defaultFileName
3340
),
34-
window: window as! Backend.Window
41+
window: window
3542
) { result in
3643
switch result {
3744
case .success(let url):

Sources/SwiftCrossUI/Environment/Actions/PresentSingleFileOpenDialogAction.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import Foundation
55
/// in a single dialog. Returns `nil` if the user cancels the operation.
66
public struct PresentSingleFileOpenDialogAction {
77
let backend: any AppBackend
8-
let window: Any
8+
let window: Any?
99

1010
public func callAsFunction(
1111
title: String = "Open",
@@ -19,6 +19,13 @@ public struct PresentSingleFileOpenDialogAction {
1919
func chooseFile<Backend: AppBackend>(backend: Backend) async -> URL? {
2020
await withCheckedContinuation { continuation in
2121
backend.runInMainThread {
22+
let window: Backend.Window? =
23+
if let window = self.window {
24+
.some(window as! Backend.Window)
25+
} else {
26+
nil
27+
}
28+
2229
backend.showOpenDialog(
2330
fileDialogOptions: FileDialogOptions(
2431
title: title,
@@ -33,7 +40,7 @@ public struct PresentSingleFileOpenDialogAction {
3340
allowSelectingDirectories: allowSelectingDirectories,
3441
allowMultipleSelections: false
3542
),
36-
window: window as! Backend.Window
43+
window: window
3744
) { result in
3845
switch result {
3946
case .success(let url):

0 commit comments

Comments
 (0)