Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 29 additions & 25 deletions Sources/WelcomeWindow/Model/NSDocumentController+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ extension NSDocumentController {
public func createFileDocumentWithDialog(
configuration: DocumentSaveDialogConfiguration = .init(),
onDialogPresented: @escaping () -> Void = {},
onCompletion: @escaping () -> Void = {},
onCompletion: @MainActor @escaping () -> Void = {},
onCancel: @escaping () -> Void = {}
) {
_createDocument(
Expand Down Expand Up @@ -53,7 +53,7 @@ extension NSDocumentController {
public func createFolderDocumentWithDialog(
configuration: DocumentSaveDialogConfiguration,
onDialogPresented: @escaping () -> Void = {},
onCompletion: @escaping () -> Void = {},
onCompletion: @MainActor @escaping () -> Void = {},
onCancel: @escaping () -> Void = {}
) {
_createDocument(
Expand Down Expand Up @@ -100,7 +100,7 @@ extension NSDocumentController {
mode: SaveMode,
configuration: DocumentSaveDialogConfiguration,
onDialogPresented: @escaping () -> Void,
onCompletion: @escaping () -> Void,
onCompletion: @MainActor @escaping () -> Void,
onCancel: @escaping () -> Void
) {
// 1 ────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -177,8 +177,8 @@ extension NSDocumentController {
public func openDocumentWithDialog(
configuration: DocumentOpenDialogConfiguration = DocumentOpenDialogConfiguration(),
onDialogPresented: @escaping () -> Void = {},
onCompletion: @escaping () -> Void = {},
onCancel: @escaping () -> Void = {}
onCompletion: @MainActor @escaping () -> Void = {},
onCancel: @MainActor @escaping () -> Void = {}
) {
let panel = NSOpenPanel()
panel.title = configuration.title
Expand All @@ -189,12 +189,14 @@ extension NSDocumentController {
panel.level = .modalPanel

panel.begin { result in
guard result == .OK, let selectedURL = panel.url else {
onCancel()
return
}
DispatchQueue.mainIfNot {
guard result == .OK, let selectedURL = panel.url else {
onCancel()
return
}

self.openDocument(at: selectedURL, onCompletion: onCompletion, onError: { _ in onCancel() })
self.openDocument(at: selectedURL, onCompletion: onCompletion, onError: { _ in onCancel() })
}
}
onDialogPresented()
}
Expand All @@ -208,25 +210,27 @@ extension NSDocumentController {
@MainActor
public func openDocument(
at url: URL,
onCompletion: @escaping () -> Void = {},
onError: @escaping (Error) -> Void = { _ in }
onCompletion: @MainActor @escaping () -> Void = {},
onError: @MainActor @escaping (Error) -> Void = { _ in }
) {
let accessGranted = RecentsStore.beginAccessing(url)
openDocument(withContentsOf: url, display: true) { _, _, error in
if let error {
if accessGranted {
RecentsStore.endAccessing(url)
}
DispatchQueue.main.async {
NSAlert(error: error).runModal()
}
onError(error)
} else {
RecentsStore.documentOpened(at: url)
DispatchQueue.main.async {
NSApp.activate(ignoringOtherApps: true)
DispatchQueue.mainIfNot {
if let error {
if accessGranted {
RecentsStore.endAccessing(url)
}
DispatchQueue.main.async {
NSAlert(error: error).runModal()
}
onError(error)
} else {
RecentsStore.documentOpened(at: url)
DispatchQueue.main.async {
NSApp.activate(ignoringOtherApps: true)
}
onCompletion()
}
onCompletion()
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/WelcomeWindow/Model/RecentsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public enum RecentsStore {
}
}

private static let logger = Logger(
private nonisolated static let logger = Logger(
subsystem: Bundle.main.bundleIdentifier ?? "com.example.app",
category: "RecentsStore"
)
Expand Down
22 changes: 22 additions & 0 deletions Sources/WelcomeWindow/Utils/DispatchQueue+asyncIfNot.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// DispatchQueue+asyncIfNot.swift
// WelcomeWindow
//
// Created by Khan Winter on 8/28/25.
//

import Foundation

extension DispatchQueue {
/// Dispatch an operation to the main queue if it's not already on it.
/// - Parameter operation: The operation to enqueue.
static func mainIfNot(_ operation: @MainActor @escaping () -> Void) {
if Thread.isMainThread {
MainActor.assumeIsolated {
operation()
}
} else {
DispatchQueue.main.async(execute: operation)
}
}
}
10 changes: 5 additions & 5 deletions Sources/WelcomeWindow/Views/WelcomeWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public struct WelcomeWindow<RecentsView: View, SubtitleView: View>: Scene {

private let buildActions: (_ dismissWindow: @escaping () -> Void) -> WelcomeActions
private let customRecentsList: ((_ dismissWindow: @escaping () -> Void) -> RecentsView)?
private let onDrop: ((_ url: URL, _ dismiss: @escaping () -> Void) -> Void)?
private let onDrop: (@Sendable (_ url: URL, _ dismiss: @escaping () -> Void) -> Void)?
private let subtitleView: (() -> SubtitleView)?
private let openHandler: WelcomeOpenHandler?

Expand All @@ -34,7 +34,7 @@ public struct WelcomeWindow<RecentsView: View, SubtitleView: View>: Scene {
@ActionsBuilder actions: @escaping (_ dismissWindow: @escaping () -> Void) -> WelcomeActions,
customRecentsList: ((_ dismissWindow: @escaping () -> Void) -> RecentsView)? = nil,
subtitleView: (() -> SubtitleView)? = nil,
onDrop: ((_ url: URL, _ dismiss: @escaping () -> Void) -> Void)? = nil,
onDrop: (@Sendable (_ url: URL, _ dismiss: @escaping () -> Void) -> Void)? = nil,
openHandler: WelcomeOpenHandler? = nil
) {
self.iconImage = iconImage
Expand Down Expand Up @@ -99,7 +99,7 @@ extension WelcomeWindow where RecentsView == EmptyView, SubtitleView == EmptyVie
iconImage: Image? = nil,
title: String? = nil,
@ActionsBuilder actions: @escaping (_ dismissWindow: @escaping () -> Void) -> WelcomeActions,
onDrop: ((_ url: URL, _ dismissWindow: @escaping () -> Void) -> Void)? = nil,
onDrop: (@Sendable (_ url: URL, _ dismissWindow: @escaping () -> Void) -> Void)? = nil,
openHandler: WelcomeOpenHandler? = nil
) {
self.init(
Expand All @@ -125,7 +125,7 @@ extension WelcomeWindow where RecentsView == EmptyView {
title: String? = nil,
subtitleView: @escaping () -> SubtitleView,
@ActionsBuilder actions: @escaping (_ dismissWindow: @escaping () -> Void) -> WelcomeActions,
onDrop: ((_ url: URL, _ dismissWindow: @escaping () -> Void) -> Void)? = nil,
onDrop: (@Sendable (_ url: URL, _ dismissWindow: @escaping () -> Void) -> Void)? = nil,
openHandler: WelcomeOpenHandler? = nil
) {
self.init(
Expand All @@ -151,7 +151,7 @@ extension WelcomeWindow where SubtitleView == EmptyView {
title: String? = nil,
@ActionsBuilder actions: @escaping (_ dismissWindow: @escaping () -> Void) -> WelcomeActions,
customRecentsList: ((_ dismissWindow: @escaping () -> Void) -> RecentsView)? = nil,
onDrop: ((_ url: URL, _ dismissWindow: @escaping () -> Void) -> Void)? = nil,
onDrop: (@Sendable (_ url: URL, _ dismissWindow: @escaping () -> Void) -> Void)? = nil,
openHandler: WelcomeOpenHandler? = nil
) {
self.init(
Expand Down
26 changes: 17 additions & 9 deletions Sources/WelcomeWindow/Views/WelcomeWindowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public struct WelcomeWindowView<RecentsView: View, SubtitleView: View>: View {
@State private var selection: Set<URL> = []

private let buildActions: (_ dismissWindow: @escaping () -> Void) -> WelcomeActions
private let onDrop: ((_ url: URL, _ dismiss: @escaping () -> Void) -> Void)?
private let onDrop: (@Sendable (_ url: URL, _ dismiss: @escaping () -> Void) -> Void)?
private let customRecentsList: ((_ dismissWindow: @escaping () -> Void) -> RecentsView)?
private let subtitleView: (() -> SubtitleView)?
private let openHandler: WelcomeOpenHandler?
Expand All @@ -35,7 +35,7 @@ public struct WelcomeWindowView<RecentsView: View, SubtitleView: View>: View {
title: String? = nil,
subtitleView: (() -> SubtitleView)? = nil,
buildActions: @escaping (_ dismissWindow: @escaping () -> Void) -> WelcomeActions,
onDrop: ((_ url: URL, _ dismiss: @escaping () -> Void) -> Void)? = nil,
onDrop: (@Sendable (_ url: URL, _ dismiss: @escaping () -> Void) -> Void)? = nil,
customRecentsList: ((_ dismissWindow: @escaping () -> Void) -> RecentsView)? = nil,
openHandler: WelcomeOpenHandler? = nil
) {
Expand All @@ -60,12 +60,20 @@ public struct WelcomeWindowView<RecentsView: View, SubtitleView: View>: View {
}
}

public var body: some View {
let dismiss = dismissWindow.callAsFunction
let actions = buildActions(dismiss)
let effectiveOpen = openHandler ?? defaultOpenHandler
var dismiss: () -> Void {
dismissWindow.callAsFunction
}

var actions: WelcomeActions {
buildActions(dismiss)
}

return HStack(spacing: 0) {
var effectiveOpen: (@MainActor ([URL], @escaping () -> Void) -> Void) {
openHandler ?? defaultOpenHandler
}

public var body: some View {
HStack(spacing: 0) {
WelcomeView(
iconImage: iconImage,
title: title,
Expand Down Expand Up @@ -114,11 +122,11 @@ public struct WelcomeWindowView<RecentsView: View, SubtitleView: View>: View {
}
.onDrop(of: [.fileURL], isTargeted: .constant(true)) { providers in
NSApp.activate(ignoringOtherApps: true)
providers.forEach {
providers.forEach { [onDrop, dismissWindow] in
_ = $0.loadDataRepresentation(for: .fileURL) { data, _ in
if let data, let url = URL(dataRepresentation: data, relativeTo: nil) {
Task { @MainActor in
onDrop?(url, dismiss)
onDrop?(url, dismissWindow.callAsFunction)
}
}
}
Expand Down