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
90 changes: 59 additions & 31 deletions TablePro/Core/Services/Infrastructure/WindowOpener.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,29 @@ internal final class WindowOpener {
@ObservationIgnored private var openSettingsAction: (() -> Void)?
@ObservationIgnored private var stagedDraftId: UUID?
@ObservationIgnored private var pendingCalls: [() -> Void] = []
@ObservationIgnored private var isWired = false

private init() {}
/// Not private so a test can exercise the queue on an instance with no presenters
/// registered. Production code uses `shared`.
internal init() {}

internal func openWelcome() {
run {
$0.openWelcomeAction?()
perform { opener in
guard let present = opener.openWelcomeAction else { return false }
present()
NSApp.activate()
return true
}
}

internal func openSettings(tab: SettingsPane? = nil) {
if let tab {
UserDefaults.standard.set(tab.rawValue, forKey: PreferenceKeys.selectedSettingsPane.name)
}
run { $0.openSettingsAction?() }
perform { opener in
guard let present = opener.openSettingsAction else { return false }
present()
return true
}
}

internal func closeWelcome() {
Expand All @@ -45,7 +52,11 @@ internal final class WindowOpener {
}

internal func openConnectionForm(editing connectionId: UUID) {
run { $0.openConnectionFormAction?(.edit(connectionId: connectionId)) }
perform { opener in
guard let present = opener.openConnectionFormAction else { return false }
present(.edit(connectionId: connectionId))
return true
}
}

internal func openConnectionForm() {
Expand All @@ -64,7 +75,11 @@ internal final class WindowOpener {
internal func openStagedConnectionForm() {
guard let draftId = stagedDraftId else { return }
stagedDraftId = nil
run { $0.openConnectionFormAction?(.create(draftId: draftId)) }
perform { opener in
guard let present = opener.openConnectionFormAction else { return false }
present(.create(draftId: draftId))
return true
}
}

private func discardStagedDraft() {
Expand All @@ -82,36 +97,49 @@ internal final class WindowOpener {
}

internal func openIntegrationsActivity() {
run { $0.openIntegrationsActivityAction?() }
perform { opener in
guard let present = opener.openIntegrationsActivityAction else { return false }
present()
return true
}
}

internal func wire(
openWelcome: @escaping () -> Void,
openConnectionForm: @escaping (ConnectionFormRequest) -> Void,
openIntegrationsActivity: @escaping () -> Void,
openSettings: @escaping () -> Void
) {
openWelcomeAction = openWelcome
openConnectionFormAction = openConnectionForm
openIntegrationsActivityAction = openIntegrationsActivity
openSettingsAction = openSettings
isWired = true
internal func setWelcomePresenter(_ present: @escaping () -> Void) {
openWelcomeAction = present
drainPendingCalls()
}

internal func setConnectionFormPresenter(_ present: @escaping (ConnectionFormRequest) -> Void) {
openConnectionFormAction = present
drainPendingCalls()
}

internal func setIntegrationsActivityPresenter(_ present: @escaping () -> Void) {
openIntegrationsActivityAction = present
drainPendingCalls()
}

internal func setSettingsPresenter(_ present: @escaping () -> Void) {
openSettingsAction = present
drainPendingCalls()
}

/// Returns false when the presenter for that window has not been registered yet, which
/// queues the call. Each window registers independently, so one that has already migrated
/// to AppKit never waits on one that has not.
private func perform(_ block: @escaping (WindowOpener) -> Bool) {
guard !block(self) else { return }
Self.logger.notice("WindowOpener call queued; presenter not registered yet")
pendingCalls.append { [weak self] in
self?.perform(block)
}
}

private func drainPendingCalls() {
let drained = pendingCalls
pendingCalls.removeAll()
for call in drained {
call()
}
}

private func run(_ block: @escaping (WindowOpener) -> Void) {
if isWired {
block(self)
return
}
Self.logger.notice("WindowOpener call queued; bridge not yet wired")
pendingCalls.append { [weak self] in
guard let self else { return }
block(self)
}
}
}
12 changes: 6 additions & 6 deletions TablePro/Views/Infrastructure/WindowOpenerBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ internal struct WindowOpenerBridge: View {
}

private func wireUp() {
WindowOpener.shared.wire(
openWelcome: { openWindow(id: SceneId.welcome) },
openConnectionForm: { request in openWindow(id: SceneId.connectionForm, value: request) },
openIntegrationsActivity: { openWindow(id: SceneId.integrationsActivity) },
openSettings: { openSettings() }
)
WindowOpener.shared.setWelcomePresenter { openWindow(id: SceneId.welcome) }
WindowOpener.shared.setConnectionFormPresenter { request in
openWindow(id: SceneId.connectionForm, value: request)
}
WindowOpener.shared.setIntegrationsActivityPresenter { openWindow(id: SceneId.integrationsActivity) }
WindowOpener.shared.setSettingsPresenter { openSettings() }
}
}
43 changes: 35 additions & 8 deletions TableProTests/Core/Services/Infrastructure/WindowOpenerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@ final class WindowOpenerTests: XCTestCase {
super.setUp()
_ = WelcomeRouter.shared.consumePendingRequest()
openedRequests = []
WindowOpener.shared.wire(
openWelcome: {},
openConnectionForm: { [weak self] request in
self?.openedRequests.append(request)
},
openIntegrationsActivity: {},
openSettings: {}
)
WindowOpener.shared.setWelcomePresenter {}
WindowOpener.shared.setConnectionFormPresenter { [weak self] request in
self?.openedRequests.append(request)
}
WindowOpener.shared.setIntegrationsActivityPresenter {}
WindowOpener.shared.setSettingsPresenter {}
}

override func tearDown() {
Expand Down Expand Up @@ -128,6 +126,35 @@ final class WindowOpenerTests: XCTestCase {
XCTAssertEqual(openedRequests, [.edit(connectionId: connectionId)])
}

func testACallMadeBeforeItsPresenterIsRegisteredRunsOnceItArrives() {
let opener = WindowOpener()
var opened: [ConnectionFormRequest] = []
let connectionId = UUID()

opener.openConnectionForm(editing: connectionId)
XCTAssertTrue(opened.isEmpty, "No presenter yet, so the call has to wait")

opener.setConnectionFormPresenter { opened.append($0) }

XCTAssertEqual(opened, [.edit(connectionId: connectionId)])
}

/// Registering one window's presenter must not drop another window's queued call, which is
/// what lets the windows migrate to AppKit one at a time.
func testRegisteringOneWindowDoesNotDiscardAnotherWindowsQueuedCall() {
let opener = WindowOpener()
var opened: [ConnectionFormRequest] = []
let connectionId = UUID()

opener.openConnectionForm(editing: connectionId)
opener.setWelcomePresenter {}
XCTAssertTrue(opened.isEmpty, "The connection form still has no presenter")

opener.setConnectionFormPresenter { opened.append($0) }

XCTAssertEqual(opened, [.edit(connectionId: connectionId)])
}

func testEditingTheSameConnectionTwiceRequestsTheSameWindow() {
let connectionId = UUID()

Expand Down
Loading