From bbdeaab2b813773593e33323dbe4e8fbf0b33cc9 Mon Sep 17 00:00:00 2001 From: Giorgi T Date: Thu, 14 Aug 2025 20:00:10 +0400 Subject: [PATCH 1/2] ADD: open handler callback for custom logic --- .../WelcomeWindow/Views/RecentsListView.swift | 17 ++++------- .../WelcomeWindow/Views/WelcomeWindow.swift | 28 +++++++++++++------ .../Views/WelcomeWindowView.swift | 21 ++++++++++++-- 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/Sources/WelcomeWindow/Views/RecentsListView.swift b/Sources/WelcomeWindow/Views/RecentsListView.swift index de5d92d92..a85354103 100644 --- a/Sources/WelcomeWindow/Views/RecentsListView.swift +++ b/Sources/WelcomeWindow/Views/RecentsListView.swift @@ -20,17 +20,20 @@ public struct RecentsListView: View { @FocusState.Binding private var focusedField: FocusTarget? private let dismissWindow: () -> Void + private let openHandler: WelcomeOpenHandler public init( recentProjects: Binding<[URL]>, selection: Binding>, focusedField: FocusState.Binding, - dismissWindow: @escaping () -> Void + dismissWindow: @escaping () -> Void, + openHandler: @escaping WelcomeOpenHandler ) { self._recentProjects = recentProjects self._selection = selection self._focusedField = focusedField self.dismissWindow = dismissWindow + self.openHandler = openHandler } private var isFocused: Bool { @@ -70,11 +73,7 @@ public struct RecentsListView: View { } } } primaryAction: { items in - for url in items { - NSDocumentController.shared.openDocument(at: url) { - dismissWindow() - } - } + openHandler(Array(items), dismissWindow) } .onCopyCommand { selection.map { NSItemProvider(object: $0.path(percentEncoded: false) as NSString) } @@ -84,11 +83,7 @@ public struct RecentsListView: View { } .background { Button("") { - selection.forEach { url in - NSDocumentController.shared.openDocument(at: url) { - dismissWindow() - } - } + openHandler(Array(selection), dismissWindow) } .keyboardShortcut(.defaultAction) .hidden() diff --git a/Sources/WelcomeWindow/Views/WelcomeWindow.swift b/Sources/WelcomeWindow/Views/WelcomeWindow.swift index 2f99ac859..efee8ca4c 100644 --- a/Sources/WelcomeWindow/Views/WelcomeWindow.swift +++ b/Sources/WelcomeWindow/Views/WelcomeWindow.swift @@ -15,6 +15,7 @@ public struct WelcomeWindow: Scene { private let customRecentsList: ((_ dismissWindow: @escaping () -> Void) -> RecentsView)? private let onDrop: ((_ url: URL, _ dismiss: @escaping () -> Void) -> Void)? private let subtitleView: (() -> SubtitleView)? + private let openHandler: WelcomeOpenHandler? let iconImage: Image? let title: String? @@ -33,7 +34,8 @@ public struct WelcomeWindow: 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: ((_ url: URL, _ dismiss: @escaping () -> Void) -> Void)? = nil, + openHandler: WelcomeOpenHandler? = nil ) { self.iconImage = iconImage self.title = title @@ -41,6 +43,7 @@ public struct WelcomeWindow: Scene { self.customRecentsList = customRecentsList self.subtitleView = subtitleView self.onDrop = onDrop + self.openHandler = openHandler } public var body: some Scene { @@ -52,7 +55,8 @@ public struct WelcomeWindow: Scene { subtitleView: subtitleView, buildActions: buildActions, onDrop: onDrop, - customRecentsList: customRecentsList + customRecentsList: customRecentsList, + openHandler: openHandler ) .frame(width: 740, height: isMacOS26 ? 460 - 28 : 460) .task { @@ -71,6 +75,8 @@ public struct WelcomeWindow: Scene { } } +public typealias WelcomeOpenHandler = @MainActor (_ urls: [URL], _ dismiss: @escaping () -> Void) -> Void + // ────────────────────────────────────────────────────────────── // 1) NEITHER a custom recents list NOR a subtitle view // ────────────────────────────────────────────────────────────── @@ -81,7 +87,8 @@ 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: ((_ url: URL, _ dismissWindow: @escaping () -> Void) -> Void)? = nil, + openHandler: WelcomeOpenHandler? = nil ) { self.init( iconImage: iconImage, @@ -89,7 +96,8 @@ extension WelcomeWindow where RecentsView == EmptyView, SubtitleView == EmptyVie actions: actions, customRecentsList: nil, subtitleView: nil, - onDrop: onDrop + onDrop: onDrop, + openHandler: openHandler ) } } @@ -105,7 +113,8 @@ 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: ((_ url: URL, _ dismissWindow: @escaping () -> Void) -> Void)? = nil, + openHandler: WelcomeOpenHandler? = nil ) { self.init( iconImage: iconImage, @@ -113,7 +122,8 @@ extension WelcomeWindow where RecentsView == EmptyView { actions: actions, customRecentsList: nil, subtitleView: subtitleView, - onDrop: onDrop + onDrop: onDrop, + openHandler: openHandler ) } } @@ -129,7 +139,8 @@ 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: ((_ url: URL, _ dismissWindow: @escaping () -> Void) -> Void)? = nil, + openHandler: WelcomeOpenHandler? = nil ) { self.init( iconImage: iconImage, @@ -137,7 +148,8 @@ extension WelcomeWindow where SubtitleView == EmptyView { actions: actions, customRecentsList: customRecentsList, subtitleView: nil, - onDrop: onDrop + onDrop: onDrop, + openHandler: openHandler ) } } diff --git a/Sources/WelcomeWindow/Views/WelcomeWindowView.swift b/Sources/WelcomeWindow/Views/WelcomeWindowView.swift index 3f62aab0f..644f40d4a 100644 --- a/Sources/WelcomeWindow/Views/WelcomeWindowView.swift +++ b/Sources/WelcomeWindow/Views/WelcomeWindowView.swift @@ -25,6 +25,7 @@ public struct WelcomeWindowView: View { private let onDrop: ((_ url: URL, _ dismiss: @escaping () -> Void) -> Void)? private let customRecentsList: ((_ dismissWindow: @escaping () -> Void) -> RecentsView)? private let subtitleView: (() -> SubtitleView)? + private let openHandler: WelcomeOpenHandler? let iconImage: Image? let title: String? @@ -35,7 +36,8 @@ public struct WelcomeWindowView: View { subtitleView: (() -> SubtitleView)? = nil, buildActions: @escaping (_ dismissWindow: @escaping () -> Void) -> WelcomeActions, onDrop: ((_ url: URL, _ dismiss: @escaping () -> Void) -> Void)? = nil, - customRecentsList: ((_ dismissWindow: @escaping () -> Void) -> RecentsView)? = nil + customRecentsList: ((_ dismissWindow: @escaping () -> Void) -> RecentsView)? = nil, + openHandler: WelcomeOpenHandler? = nil ) { self.iconImage = iconImage self.title = title @@ -43,11 +45,25 @@ public struct WelcomeWindowView: View { self.buildActions = buildActions self.onDrop = onDrop self.customRecentsList = customRecentsList + self.openHandler = openHandler + } + + private func defaultOpenHandler(urls: [URL], dismiss: @escaping () -> Void) { + var dismissed = false + for url in urls { + NSDocumentController.shared.openDocument(at: url) { + if !dismissed { + dismissed = true + dismiss() + } + } + } } public var body: some View { let dismiss = dismissWindow.callAsFunction let actions = buildActions(dismiss) + let effectiveOpen = openHandler ?? defaultOpenHandler return HStack(spacing: 0) { WelcomeView( @@ -67,7 +83,8 @@ public struct WelcomeWindowView: View { recentProjects: $recentProjects, selection: $selection, focusedField: $focusedField, - dismissWindow: dismiss + dismissWindow: dismiss, + openHandler: effectiveOpen ) } } From 3d493a9a6ff8c00c241ac45f199998412135ca72 Mon Sep 17 00:00:00 2001 From: Giorgi T Date: Thu, 14 Aug 2025 21:04:58 +0400 Subject: [PATCH 2/2] FIX: SwiftLint --- Sources/WelcomeWindow/Views/RecentsListView.swift | 3 +-- Sources/WelcomeWindow/Views/WelcomeView.swift | 6 ++---- Sources/WelcomeWindow/Views/WelcomeWindow.swift | 12 ++++++++++++ Sources/WelcomeWindow/Views/WelcomeWindowView.swift | 4 ++-- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Sources/WelcomeWindow/Views/RecentsListView.swift b/Sources/WelcomeWindow/Views/RecentsListView.swift index a85354103..e1ca8007e 100644 --- a/Sources/WelcomeWindow/Views/RecentsListView.swift +++ b/Sources/WelcomeWindow/Views/RecentsListView.swift @@ -40,8 +40,7 @@ public struct RecentsListView: View { focusedField == .recentProjects } - @ViewBuilder - private var listEmptyView: some View { + @ViewBuilder private var listEmptyView: some View { VStack { Spacer() Text("No Recent Projects") diff --git a/Sources/WelcomeWindow/Views/WelcomeView.swift b/Sources/WelcomeWindow/Views/WelcomeView.swift index 53820a569..d6b1164d6 100644 --- a/Sources/WelcomeWindow/Views/WelcomeView.swift +++ b/Sources/WelcomeWindow/Views/WelcomeView.swift @@ -64,8 +64,7 @@ public struct WelcomeView: View { } } - @ViewBuilder - private var mainContent: some View { + @ViewBuilder private var mainContent: some View { VStack(spacing: 0) { Spacer().frame(height: 32) ZStack { @@ -154,8 +153,7 @@ public struct WelcomeView: View { } } - @ViewBuilder - private var dismissButton: some View { + @ViewBuilder private var dismissButton: some View { Button(action: dismissWindow) { Image(systemName: "xmark.circle.fill") .foregroundColor(isHoveringCloseButton ? Color(.secondaryLabelColor) : Color(.tertiaryLabelColor)) diff --git a/Sources/WelcomeWindow/Views/WelcomeWindow.swift b/Sources/WelcomeWindow/Views/WelcomeWindow.swift index efee8ca4c..d167fd1b7 100644 --- a/Sources/WelcomeWindow/Views/WelcomeWindow.swift +++ b/Sources/WelcomeWindow/Views/WelcomeWindow.swift @@ -75,6 +75,18 @@ public struct WelcomeWindow: Scene { } } +/// A closure type used to handle opening recent items from the default `RecentsListView`. +/// +/// This allows apps to override the default `NSDocumentController` behavior for +/// opening files, making the handling of recent-item URLs fully configurable. +/// +/// The closure is executed on the **main actor** to ensure UI safety. +/// +/// - Parameters: +/// - urls: The recent-item URLs selected by the user to be opened. +/// - dismiss: A closure to invoke when the opening process is complete and the +/// `RecentsListView` can be dismissed. +/// public typealias WelcomeOpenHandler = @MainActor (_ urls: [URL], _ dismiss: @escaping () -> Void) -> Void // ────────────────────────────────────────────────────────────── diff --git a/Sources/WelcomeWindow/Views/WelcomeWindowView.swift b/Sources/WelcomeWindow/Views/WelcomeWindowView.swift index 644f40d4a..edbcb2124 100644 --- a/Sources/WelcomeWindow/Views/WelcomeWindowView.swift +++ b/Sources/WelcomeWindow/Views/WelcomeWindowView.swift @@ -45,9 +45,9 @@ public struct WelcomeWindowView: View { self.buildActions = buildActions self.onDrop = onDrop self.customRecentsList = customRecentsList - self.openHandler = openHandler + self.openHandler = openHandler } - + private func defaultOpenHandler(urls: [URL], dismiss: @escaping () -> Void) { var dismissed = false for url in urls {