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
35 changes: 14 additions & 21 deletions TablePro/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {

private var hasRunPostLaunchActivation = false
private var pluginsRejectedCancellable: AnyCancellable?
private var commandCancellables: Set<AnyCancellable> = []

// MARK: - URL & File Open

Expand Down Expand Up @@ -83,18 +84,18 @@ class AppDelegate: NSObject, NSApplicationDelegate {
.sink { [weak self] rejected in
self?.handlePluginsRejected(rejected)
}
NotificationCenter.default.addObserver(
self, selector: #selector(handleFocusConnectionForm),
name: .focusConnectionFormWindowRequested, object: nil
)
NotificationCenter.default.addObserver(
self, selector: #selector(handleOpenSampleDatabase(_:)),
name: .openSampleDatabaseRequested, object: nil
)
NotificationCenter.default.addObserver(
self, selector: #selector(handleResetSampleDatabase(_:)),
name: .resetSampleDatabaseRequested, object: nil
)
AppCommands.shared.focusConnectionFormWindowRequested
.receive(on: RunLoop.main)
.sink { [weak self] _ in self?.handleFocusConnectionForm() }
.store(in: &commandCancellables)
AppCommands.shared.openSampleDatabaseRequested
.receive(on: RunLoop.main)
.sink { _ in SampleDatabaseLauncher.open() }
.store(in: &commandCancellables)
AppCommands.shared.resetSampleDatabaseRequested
.receive(on: RunLoop.main)
.sink { _ in SampleDatabaseLauncher.reset() }
.store(in: &commandCancellables)
}

func applicationDidBecomeActive(_ notification: Notification) {
Expand Down Expand Up @@ -210,20 +211,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
}

@objc func handleFocusConnectionForm() {
private func handleFocusConnectionForm() {
if let window = NSApp.windows.first(where: { AppLaunchCoordinator.isConnectionFormWindow($0) }) {
window.makeKeyAndOrderFront(nil)
}
}

@objc func handleOpenSampleDatabase(_ notification: Notification) {
SampleDatabaseLauncher.open()
}

@objc func handleResetSampleDatabase(_ notification: Notification) {
SampleDatabaseLauncher.reset()
}

// MARK: - Dock Menu

func applicationDockMenu(_ sender: NSApplication) -> NSMenu? {
Expand Down
6 changes: 4 additions & 2 deletions TablePro/Core/Database/DatabaseManager+Schema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Created by Ngo Quoc Dat on 16/12/25.
//

import Combine
import Foundation
import os
import TableProPluginKit
Expand Down Expand Up @@ -89,8 +90,9 @@ extension DatabaseManager {
)
}

// Post notification to refresh UI
NotificationCenter.default.post(name: .refreshData, object: nil)
await MainActor.run {
AppCommands.shared.refreshData.send(nil)
}
} catch {
if useTransaction {
do {
Expand Down
42 changes: 42 additions & 0 deletions TablePro/Core/Events/AppCommands.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// AppCommands.swift
// TablePro
//

import Combine
import Foundation

@MainActor
final class AppCommands {
static let shared = AppCommands()

// MARK: - Row Commands

let deleteSelectedRows = PassthroughSubject<Void, Never>()
let addNewRow = PassthroughSubject<Void, Never>()
let duplicateRow = PassthroughSubject<Void, Never>()
let copySelectedRows = PassthroughSubject<Void, Never>()
let pasteRows = PassthroughSubject<Void, Never>()

// MARK: - Refresh

let refreshData = PassthroughSubject<UUID?, Never>()

// MARK: - File / Connection Import-Export

let openSQLFiles = PassthroughSubject<[URL], Never>()
let exportConnections = PassthroughSubject<Void, Never>()
let importConnections = PassthroughSubject<Void, Never>()
let importConnectionsFromApp = PassthroughSubject<Void, Never>()
let exportQueryResults = PassthroughSubject<Void, Never>()
let saveAsFavoriteRequested = PassthroughSubject<String, Never>()

// MARK: - Window / Sheet Commands

let focusConnectionFormWindowRequested = PassthroughSubject<Void, Never>()
let openSampleDatabaseRequested = PassthroughSubject<Void, Never>()
let resetSampleDatabaseRequested = PassthroughSubject<Void, Never>()
let presentDatabaseTypeChooser = PassthroughSubject<DatabaseTypeChooserPayload, Never>()

private init() {}
}
8 changes: 4 additions & 4 deletions TablePro/Core/KeyboardHandling/ResponderChainActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
// - Clean method calls, no global event bus
// - Commands are automatically nil (disabled) when no connection is active
//
// 3. **NotificationCenter** (Multi-listener broadcasts only):
// - `.refreshData` (Sidebar + Coordinator + StructureView)
// - Non-menu notifications from AppKit views (DataGrid, SidebarView context menus)
// - Legitimate broadcasts where multiple views respond
// 3. **AppCommands** (Multi-listener broadcasts only):
// - `refreshData` (Sidebar + Coordinator + StructureView)
// - Non-menu commands from AppKit views (DataGrid, SidebarView context menus)
// - Typed Combine publishers for broadcasts where multiple views respond
//
// ## Example Flow
//
Expand Down
33 changes: 0 additions & 33 deletions TablePro/Core/Services/Infrastructure/AppNotifications.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
//

import AppKit
import Combine
import os
import SwiftUI
import TableProPluginKit
Expand Down Expand Up @@ -302,7 +303,7 @@ private struct RefreshToolbarButton: View {
var body: some View {
let state = coordinator.toolbarState
Button {
NotificationCenter.default.post(name: .refreshData, object: nil)
AppCommands.shared.refreshData.send(nil)
} label: {
Label("Refresh", systemImage: "arrow.clockwise")
}
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Core/Services/Infrastructure/WelcomeRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ internal final class WelcomeRouter {
private func drainPendingSQLFiles() {
let urls = consumePendingSQLFiles()
guard !urls.isEmpty else { return }
NotificationCenter.default.post(name: .openSQLFiles, object: urls)
AppCommands.shared.openSQLFiles.send(urls)
}

internal func routeImport(_ exportable: ExportableConnection) {
Expand Down
30 changes: 7 additions & 23 deletions TablePro/TableProApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import CodeEditTextView
import Combine
import Observation
import os
import Sparkle
Expand Down Expand Up @@ -201,11 +202,11 @@ struct AppMenuCommands: Commands {
.optionalKeyboardShortcut(shortcut(for: .manageConnections))

Button(String(localized: "Open Sample Database")) {
NotificationCenter.default.post(name: .openSampleDatabaseRequested, object: nil)
AppCommands.shared.openSampleDatabaseRequested.send(())
}

Button(String(localized: "Reset Sample Database...")) {
NotificationCenter.default.post(name: .resetSampleDatabaseRequested, object: nil)
AppCommands.shared.resetSampleDatabaseRequested.send(())
}
}

Expand Down Expand Up @@ -265,15 +266,15 @@ struct AppMenuCommands: Commands {
Divider()

Button(String(localized: "Export Connections...")) {
NotificationCenter.default.post(name: .exportConnections, object: nil)
AppCommands.shared.exportConnections.send(())
}

Button(String(localized: "Import Connections...")) {
NotificationCenter.default.post(name: .importConnections, object: nil)
AppCommands.shared.importConnections.send(())
}

Button(String(localized: "Import from Other App...")) {
NotificationCenter.default.post(name: .importConnectionsFromApp, object: nil)
AppCommands.shared.importConnectionsFromApp.send(())
}

Divider()
Expand Down Expand Up @@ -350,7 +351,7 @@ struct AppMenuCommands: Commands {
.disabled(!(actions?.isQueryExecuting ?? false))

Button("Refresh") {
NotificationCenter.default.post(name: .refreshData, object: nil)
AppCommands.shared.refreshData.send(nil)
}
.optionalKeyboardShortcut(shortcut(for: .refresh))
.disabled(!(actions?.isConnected ?? false))
Expand Down Expand Up @@ -681,23 +682,6 @@ struct TableProApp: App {
}
}

// MARK: - Notification Names

extension Notification.Name {
// Multi-listener broadcasts (Sidebar + Coordinator + StructureView)
static let refreshData = Notification.Name("refreshData")

// Data operations (still posted by DataGrid / context menus)
static let deleteSelectedRows = Notification.Name("deleteSelectedRows")
static let addNewRow = Notification.Name("addNewRow")
static let duplicateRow = Notification.Name("duplicateRow")
static let copySelectedRows = Notification.Name("copySelectedRows")
static let pasteRows = Notification.Name("pasteRows")

// File opening notifications
static let openSQLFiles = Notification.Name("openSQLFiles")
}

// MARK: - Check for Updates

/// Menu bar button that triggers Sparkle update check
Expand Down
37 changes: 13 additions & 24 deletions TablePro/ViewModels/WelcomeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ final class WelcomeViewModel {

@ObservationIgnored private var connectionUpdatedCancellable: AnyCancellable?
@ObservationIgnored private var linkedFoldersCancellable: AnyCancellable?
@ObservationIgnored private var exportObserver: NSObjectProtocol?
@ObservationIgnored private var importObserver: NSObjectProtocol?
@ObservationIgnored private var importFromAppObserver: NSObjectProtocol?
@ObservationIgnored private var exportConnectionsCancellable: AnyCancellable?
@ObservationIgnored private var importConnectionsCancellable: AnyCancellable?
@ObservationIgnored private var importFromAppCancellable: AnyCancellable?
@ObservationIgnored private var welcomeRouterTask: Task<Void, Never>?
@ObservationIgnored private var searchDebounceTask: Task<Void, Never>?
private static let searchDebounceNanoseconds: UInt64 = 150_000_000
Expand Down Expand Up @@ -166,30 +166,24 @@ final class WelcomeViewModel {
self?.loadConnections()
}

exportObserver = NotificationCenter.default.addObserver(
forName: .exportConnections, object: nil, queue: .main
) { [weak self] _ in
Task { @MainActor [weak self] in
exportConnectionsCancellable = AppCommands.shared.exportConnections
.receive(on: RunLoop.main)
.sink { [weak self] _ in
guard let self, !self.connections.isEmpty else { return }
self.activeSheet = .exportConnections(self.connections)
}
}

importObserver = NotificationCenter.default.addObserver(
forName: .importConnections, object: nil, queue: .main
) { [weak self] _ in
Task { @MainActor [weak self] in
importConnectionsCancellable = AppCommands.shared.importConnections
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.importConnectionsFromFile()
}
}

importFromAppObserver = NotificationCenter.default.addObserver(
forName: .importConnectionsFromApp, object: nil, queue: .main
) { [weak self] _ in
Task { @MainActor [weak self] in
importFromAppCancellable = AppCommands.shared.importConnectionsFromApp
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.activeSheet = .importFromApp
}
}

linkedFoldersCancellable = AppEvents.shared.linkedFoldersDidUpdate
.receive(on: RunLoop.main)
Expand Down Expand Up @@ -264,11 +258,6 @@ final class WelcomeViewModel {
deinit {
welcomeRouterTask?.cancel()
searchDebounceTask?.cancel()
[exportObserver, importObserver, importFromAppObserver].forEach {
if let observer = $0 {
NotificationCenter.default.removeObserver(observer)
}
}
}

// MARK: - Data Loading
Expand Down Expand Up @@ -580,7 +569,7 @@ final class WelcomeViewModel {
}

func focusConnectionFormWindow() {
NotificationCenter.default.post(name: .focusConnectionFormWindowRequested, object: nil)
AppCommands.shared.focusConnectionFormWindowRequested.send(())
}

// MARK: - Private Helpers
Expand Down
6 changes: 1 addition & 5 deletions TablePro/Views/Connection/WelcomeWindowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,7 @@ struct WelcomeWindowView: View {
chooserState: $welcomeChooserState,
urlImportPresented: $urlImportPresented
))
.onReceive(NotificationCenter.default.publisher(for: .presentDatabaseTypeChooser)) { note in
guard
let payload = note.userInfo?[DatabaseTypeChooserPayload.userInfoKey]
as? DatabaseTypeChooserPayload
else { return }
.onReceive(AppCommands.shared.presentDatabaseTypeChooser) { payload in
welcomeChooserState = WelcomeChooserState(
initialType: payload.initialType,
onSelected: { type in
Expand Down
3 changes: 2 additions & 1 deletion TablePro/Views/Import/ImportDialog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import AppKit
import Combine
import os
import SwiftUI
import TableProPluginKit
Expand Down Expand Up @@ -102,7 +103,7 @@ struct ImportDialog: View {
}
.sheet(isPresented: $showSuccessDialog, onDismiss: {
isPresented = false
NotificationCenter.default.post(name: .refreshData, object: connection.id)
AppCommands.shared.refreshData.send(connection.id)
}) {
ImportSuccessView(
result: importResult
Expand Down
Loading
Loading