mirror of
https://github.com/leonboe1/QuickDrop.git
synced 2026-11-11 02:51:11 +00:00
Always make alerts non-blocking
This commit is contained in:
1 parent
7c35cc7b35
commit
e9b6a477fc
8 files changed
+137
-34
No files matched your search
@@ -259,6 +259,10 @@
|
||||
EB5C383F2E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
|
||||
EB5C38402E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
|
||||
EB5C38412E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
|
||||
EB63EDB72FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB63EDB62FF42390000CC61F /* ModelessAlertPresenter.swift */; };
|
||||
EB63EDB92FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB63EDB62FF42390000CC61F /* ModelessAlertPresenter.swift */; };
|
||||
EB63EDBA2FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB63EDB62FF42390000CC61F /* ModelessAlertPresenter.swift */; };
|
||||
EB63EDBB2FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB63EDB62FF42390000CC61F /* ModelessAlertPresenter.swift */; };
|
||||
EB6E51A52F2F9370006D9812 /* IntroductionSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6E51A42F2F9370006D9812 /* IntroductionSheetManager.swift */; };
|
||||
EB6E51A62F2F9370006D9812 /* IntroductionSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6E51A42F2F9370006D9812 /* IntroductionSheetManager.swift */; };
|
||||
EB6E51AB2F311ED4006D9812 /* QuickDropIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = EB721F232E75AFA900F2099F /* QuickDropIcon.icon */; };
|
||||
@@ -575,6 +579,7 @@
|
||||
EB5C37E22E377C2E0052EFD7 /* QuickDropiOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickDropiOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
EB5C38382E38C1F60052EFD7 /* justMaybe.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = justMaybe.mp3; sourceTree = "<group>"; };
|
||||
EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLogin.swift; sourceTree = "<group>"; };
|
||||
EB63EDB62FF42390000CC61F /* ModelessAlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelessAlertPresenter.swift; sourceTree = "<group>"; };
|
||||
EB6E51A42F2F9370006D9812 /* IntroductionSheetManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroductionSheetManager.swift; sourceTree = "<group>"; };
|
||||
EB6E6B202ECBA95000E71D57 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
|
||||
EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
@@ -991,6 +996,7 @@
|
||||
A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */,
|
||||
EB1325F72E70287C00FD749C /* SendToExtension.swift */,
|
||||
EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */,
|
||||
EB63EDB62FF42390000CC61F /* ModelessAlertPresenter.swift */,
|
||||
EB886E662E4F2F7C0052B7D9 /* License.swift */,
|
||||
EB6FE7D12EFEFB2700FF2BCF /* AllowedWorkMonitor.swift */,
|
||||
EB36435F2D73A29F009CDFF8 /* AudioManager.swift */,
|
||||
@@ -1389,6 +1395,7 @@
|
||||
A7D000000000000000000103 /* AndroidAirDropModeIssueView.swift in Sources */,
|
||||
EBC05C882E33B58300EECC18 /* FirewallIssueView.swift in Sources */,
|
||||
EBC05C892E33B58300EECC18 /* ApIsolationIssueView.swift in Sources */,
|
||||
EB63EDBB2FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */,
|
||||
699DEBA62AB0573200115D22 /* ShareViewController.swift in Sources */,
|
||||
EB96ED8B2D7F7CF7004B3FF1 /* SmallSheetView.swift in Sources */,
|
||||
EBCC27F92D80C9F90079C478 /* Zip.swift in Sources */,
|
||||
@@ -1447,6 +1454,7 @@
|
||||
EB5C37C22E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */,
|
||||
EB5C37C32E377A780052EFD7 /* wire_format.pb.swift in Sources */,
|
||||
EB16B6152E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
||||
EB63EDB92FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */,
|
||||
EB5C37C42E377A780052EFD7 /* securegcm.pb.swift in Sources */,
|
||||
EB97A0102E51CD13008B8C5C /* ReceiveModel.swift in Sources */,
|
||||
A1F5B3DD2F3A1A0100A1B2C8 /* PairingQrSheetManager.swift in Sources */,
|
||||
@@ -1519,6 +1527,7 @@
|
||||
EB3B9F0F2EF7150400D6704E /* device_to_device_messages.pb.swift in Sources */,
|
||||
EB3B9F102EF7150400D6704E /* wire_format.pb.swift in Sources */,
|
||||
EB3B9F112EF7150400D6704E /* IdentityManager.swift in Sources */,
|
||||
EB63EDB72FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */,
|
||||
EB3B9F122EF7150400D6704E /* securegcm.pb.swift in Sources */,
|
||||
EB3B9F132EF7150400D6704E /* ReceiveModel.swift in Sources */,
|
||||
A1F5B3DE2F3A1A0100A1B2C9 /* PairingQrSheetManager.swift in Sources */,
|
||||
@@ -1575,6 +1584,7 @@
|
||||
A7D000000000000000000104 /* AndroidAirDropModeIssueView.swift in Sources */,
|
||||
EB3B9F542EF7151900D6704E /* FirewallIssueView.swift in Sources */,
|
||||
EB3B9F552EF7151900D6704E /* ApIsolationIssueView.swift in Sources */,
|
||||
EB63EDBA2FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */,
|
||||
EB3B9F562EF7151900D6704E /* ShareViewController.swift in Sources */,
|
||||
EB3B9F572EF7151900D6704E /* SmallSheetView.swift in Sources */,
|
||||
EB3B9F582EF7151900D6704E /* Zip.swift in Sources */,
|
||||
|
||||
@@ -106,29 +106,11 @@ class ErrorAlertHandler {
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
let primaryButtonTitle = "InformDeveloper".localized()
|
||||
let secondaryButtonTitle = "CloseAlert".localized()
|
||||
|
||||
// Shown modelessly (the native NSAlert panel via makeKeyAndOrderFront) rather than
|
||||
// runModal(), which blocks the main run loop and would freeze the next incoming transfer's
|
||||
// consent prompt until dismissed. Same window runModal() uses, so it looks identical.
|
||||
self.isAlertShown = true
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .critical
|
||||
alert.messageText = title
|
||||
alert.informativeText = description
|
||||
alert.addButton(withTitle: primaryButtonTitle)
|
||||
alert.addButton(withTitle: secondaryButtonTitle)
|
||||
|
||||
let result = alert.runModal()
|
||||
self.isAlertShown = false
|
||||
|
||||
if result == .alertFirstButtonReturn {
|
||||
if Bundle.main.bundlePath.hasSuffix(".appex") {
|
||||
if let url = URL(string: "quickdrop://sendLog") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
} else {
|
||||
LogExportPresenter.showUploadLogsAlert(openSupportMailAfterUpload: false)
|
||||
}
|
||||
}
|
||||
presentTransferErrorAlert(title: title, description: description)
|
||||
#else
|
||||
showAlert(title: title, message: description)
|
||||
#endif
|
||||
@@ -211,6 +193,44 @@ class ErrorAlertHandler {
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
window.orderFrontRegardless()
|
||||
}
|
||||
|
||||
|
||||
/// Shows the transfer-error alert as the native `NSAlert` panel, but presented modelessly
|
||||
/// (`makeKeyAndOrderFront`) instead of `runModal()`, so it does NOT block the main run loop — an
|
||||
/// incoming transfer's consent prompt can still appear while it's on screen. It's the same
|
||||
/// `_NSAlertPanel` window `runModal()` would show, so it looks identical; we just route the button
|
||||
/// clicks ourselves (there's no modal session to end) and reset state on dismissal.
|
||||
private func presentTransferErrorAlert(title: String, description: String) {
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .critical
|
||||
alert.messageText = title
|
||||
alert.informativeText = description
|
||||
alert.addButton(withTitle: "InformDeveloper".localized()) // tag == .alertFirstButtonReturn
|
||||
alert.addButton(withTitle: "CloseAlert".localized()) // tag == .alertSecondButtonReturn
|
||||
alert.buttons.first?.keyEquivalent = "\r" // default button: Return
|
||||
if alert.buttons.count > 1 { alert.buttons[1].keyEquivalent = "\u{1b}" } // Close: Escape
|
||||
|
||||
ModelessAlertPresenter.present(alert) { [weak self] response in
|
||||
self?.isAlertShown = false
|
||||
if response == .alertFirstButtonReturn {
|
||||
Self.presentLogUpload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The "Inform developer" action: from the share extension, hand off to the host app via the
|
||||
/// `quickdrop://sendLog` URL; otherwise upload logs directly. Mirrors the previous alert's primary
|
||||
/// button.
|
||||
private static func presentLogUpload() {
|
||||
if Bundle.main.bundlePath.hasSuffix(".appex") {
|
||||
if let url = URL(string: "quickdrop://sendLog") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
} else {
|
||||
LogExportPresenter.showUploadLogsAlert(openSupportMailAfterUpload: false)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// ModelessAlertPresenter.swift
|
||||
// QuickDrop
|
||||
//
|
||||
// Created by Leon Böttger on 30.06.26.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
|
||||
/// Presents an `NSAlert` as its own modeless panel (non-blocking) instead of `runModal()`. It's the
|
||||
/// exact `_NSAlertPanel` window `runModal()` would show, so it looks identical — but it does not spin a
|
||||
/// nested modal run loop, so the app keeps working (e.g. can still receive the next transfer) while the
|
||||
/// alert is on screen.
|
||||
///
|
||||
/// Use this in place of `alert.runModal()` for any alert that may appear while the app is busy. The
|
||||
/// clicked button is reported to `completion` as the `NSApplication.ModalResponse` that `runModal()`
|
||||
/// would have returned (first button = `.alertFirstButtonReturn`, second = `.alertSecondButtonReturn`,
|
||||
/// …). Must be called on the main thread.
|
||||
final class ModelessAlertPresenter: NSObject {
|
||||
|
||||
/// Keeps each live presenter — and thus its `NSAlert` and panel — retained while on screen; the
|
||||
/// entry is removed when the alert is dismissed. `NSButton.target` is weak and the panel is owned by
|
||||
/// the `NSAlert`, so without this the alert would deallocate immediately.
|
||||
private static var active: Set<ModelessAlertPresenter> = []
|
||||
|
||||
private let alert: NSAlert
|
||||
private let completion: (NSApplication.ModalResponse) -> Void
|
||||
|
||||
private init(alert: NSAlert, completion: @escaping (NSApplication.ModalResponse) -> Void) {
|
||||
self.alert = alert
|
||||
self.completion = completion
|
||||
}
|
||||
|
||||
static func present(_ alert: NSAlert,
|
||||
completion: @escaping (NSApplication.ModalResponse) -> Void = { _ in }) {
|
||||
|
||||
if let window = NSApp.mainWindow ?? NSApp.keyWindow {
|
||||
alert.beginSheetModal(for: window, completionHandler: completion)
|
||||
}
|
||||
else {
|
||||
let presenter = ModelessAlertPresenter(alert: alert, completion: completion)
|
||||
active.insert(presenter)
|
||||
presenter.show()
|
||||
}
|
||||
}
|
||||
|
||||
private func show() {
|
||||
alert.layout()
|
||||
|
||||
// runModal() makes the default button respond to Return; modelessly we set it ourselves — but
|
||||
// only if the caller specified no key equivalents, so explicit ones (e.g. Escape) are kept.
|
||||
if alert.buttons.allSatisfy({ $0.keyEquivalent.isEmpty }) {
|
||||
alert.buttons.first?.keyEquivalent = "\r"
|
||||
}
|
||||
|
||||
for button in alert.buttons {
|
||||
button.target = self
|
||||
button.action = #selector(buttonClicked(_:))
|
||||
}
|
||||
|
||||
let window = alert.window
|
||||
window.center()
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
window.orderFrontRegardless()
|
||||
}
|
||||
|
||||
@objc private func buttonClicked(_ sender: NSButton) {
|
||||
let response = NSApplication.ModalResponse(rawValue: sender.tag)
|
||||
alert.window.orderOut(nil)
|
||||
// Defer teardown so the alert and its panel aren't deallocated while this button's action is
|
||||
// still on the stack.
|
||||
DispatchQueue.main.async {
|
||||
self.completion(response)
|
||||
ModelessAlertPresenter.active.remove(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ func sendToSharingService(items: [Any]) {
|
||||
alert.informativeText = "ShareExtensionDisabledDescriptionLegacy".localized()
|
||||
}
|
||||
alert.addButton(withTitle: "CloseAlert".localized())
|
||||
alert.runModal()
|
||||
ModelessAlertPresenter.present(alert)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ func openRouterSettingsPage() async {
|
||||
alert.informativeText = "ApIsolationFindRouterFailedDescription".localized()
|
||||
alert.addButton(withTitle: "CloseAlert".localized())
|
||||
|
||||
alert.runModal()
|
||||
ModelessAlertPresenter.present(alert)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -150,8 +150,8 @@ struct SettingsView: View {
|
||||
alert.messageText = "FolderSelectionFailedTitle".localized()
|
||||
alert.informativeText = "FolderSelectionFailedMessage".localized()
|
||||
alert.addButton(withTitle: "CloseAlert".localized())
|
||||
|
||||
alert.runModal()
|
||||
|
||||
ModelessAlertPresenter.present(alert)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,12 +75,7 @@ struct TutorialView: View {
|
||||
alert.informativeText = "AndroidAirDropModeAlertMessage".localized()
|
||||
alert.addButton(withTitle: "CloseAlert".localized())
|
||||
|
||||
if let window = NSApp.mainWindow ?? NSApp.keyWindow {
|
||||
alert.beginSheetModal(for: window) { _ in }
|
||||
}
|
||||
else {
|
||||
alert.runModal()
|
||||
}
|
||||
ModelessAlertPresenter.present(alert)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -359,7 +359,7 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
||||
alert.informativeText = error.localizedDescription
|
||||
alert.addButton(withTitle: "CloseAlert".localized())
|
||||
|
||||
alert.runModal()
|
||||
ModelessAlertPresenter.present(alert)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -453,7 +453,7 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
||||
alert.informativeText = error.localizedDescription
|
||||
alert.addButton(withTitle: "CloseAlert".localized())
|
||||
|
||||
alert.runModal()
|
||||
ModelessAlertPresenter.present(alert)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user