Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- The window title bar shows the open table's name, with its database and schema below, so you can tell which table you're viewing without checking the sidebar. (#1475)

## [0.46.0] - 2026-05-28

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
didSet { view.window?.title = windowTitle }
}

var windowSubtitle: String {
didSet { view.window?.subtitle = windowSubtitle }
}

// MARK: - Split View Items

private var sidebarSplitItem: NSSplitViewItem!
Expand Down Expand Up @@ -77,6 +81,38 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
return String(localized: "SQL Query")
}

static func resolveDefaultSubtitle(tab: QueryTab?, connection: DatabaseConnection) -> String {
guard let tab, tab.tabType == .table,
let tableName = tab.tableContext.tableName, !tableName.isEmpty else {
return connection.name
}
return tableSubtitle(
databaseName: tab.tableContext.databaseName,
schemaName: tab.tableContext.schemaName,
fallback: connection.name
)
}

static func resolveDefaultSubtitle(payload: EditorTabPayload?, connection: DatabaseConnection) -> String {
guard let payload, payload.tabType == .table,
let tableName = payload.tableName, !tableName.isEmpty else {
return connection.name
}
return tableSubtitle(
databaseName: payload.databaseName ?? "",
schemaName: payload.schemaName,
fallback: connection.name
)
}

private static func tableSubtitle(databaseName: String, schemaName: String?, fallback: String) -> String {
guard !databaseName.isEmpty else { return fallback }
if let schemaName, !schemaName.isEmpty {
return "\(databaseName) · \(schemaName)"
}
return databaseName
}

// MARK: - Init

init(payload: EditorTabPayload?, sessionState: SessionStateFactory.SessionState?) {
Expand Down Expand Up @@ -105,6 +141,13 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
}
self.currentSession = resolvedSession

let subtitleConnection = self.payloadConnection ?? resolvedSession?.connection
if let subtitleConnection {
self.windowSubtitle = Self.resolveDefaultSubtitle(payload: payload, connection: subtitleConnection)
} else {
self.windowSubtitle = ""
}

if let session = resolvedSession {
self.rightPanelState = RightPanelState()
let state: SessionStateFactory.SessionState
Expand Down Expand Up @@ -191,9 +234,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
guard let window = view.window else { return }

window.title = windowTitle
if let session = currentSession {
window.subtitle = session.connection.name
}
window.subtitle = windowSubtitle

if let sessionState {
sessionState.coordinator.inspectorProxy = self
Expand Down Expand Up @@ -291,8 +332,8 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
if payload?.tableName == nil,
windowTitle == String(localized: "SQL Query") || windowTitle.hasSuffix(" Query") {
windowTitle = newSession.connection.name
windowSubtitle = newSession.connection.name
}
view.window?.subtitle = newSession.connection.name

if rightPanelState == nil {
rightPanelState = RightPanelState()
Expand Down Expand Up @@ -375,6 +416,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
connection: currentSession.connection,
payload: payload,
windowTitle: windowTitleBinding,
windowSubtitle: windowSubtitleBinding,
sidebarState: SharedSidebarState.forConnection(currentSession.connection.id),
pendingTruncates: sessionPendingTruncatesBinding,
pendingDeletes: sessionPendingDeletesBinding,
Expand Down Expand Up @@ -458,6 +500,13 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
)
}

private var windowSubtitleBinding: Binding<String> {
Binding(
get: { [weak self] in self?.windowSubtitle ?? "" },
set: { [weak self] in self?.windowSubtitle = $0 }
)
}

// MARK: - InspectorVisibilityProxy

var isInspectorVisible: Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {
window.isRestorable = AppSettingsStorage.shared.loadGeneral().startupBehavior == .reopenLast
window.restorationClass = TabWindowRestoration.self
window.toolbarStyle = .unified
window.titleVisibility = .hidden
window.titleVisibility = .visible
window.tabbingMode = .preferred
window.tabbingIdentifier = WindowManager.tabbingIdentifier(for: payload.connectionId)
window.collectionBehavior.insert([.fullScreenPrimary, .managed])
Expand Down
22 changes: 0 additions & 22 deletions TablePro/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -661,28 +661,6 @@
}
}
},
"%@ - Preview" : {
"localizations" : {
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@ - Önizleme"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@ - Xem trước"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@ - 预览"
}
}
}
},
"%@ (%@@%@)" : {
"localizations" : {
"en" : {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,6 @@ extension MainContentCoordinator {
}
toolbarState.isTableTab = true
}
updatePreviewSubtitle(isPreview: createAsPreview)
restoreLastHiddenColumnsForTable(tableName)
restoreFiltersForTable(tableName)
if isInPlace, let dbIndex = Int(currentDatabase) {
Expand Down Expand Up @@ -260,7 +259,6 @@ extension MainContentCoordinator {
}
toolbarState.isTableTab = true
}
updatePreviewSubtitle(isPreview: createAsPreview)
restoreLastHiddenColumnsForTable(tableName)
restoreFiltersForTable(tableName)
runQuery()
Expand Down Expand Up @@ -288,13 +286,6 @@ extension MainContentCoordinator {
guard let (tab, tabIndex) = tabManager.selectedTabAndIndex,
tab.isPreview else { return }
tabManager.mutate(at: tabIndex) { $0.isPreview = false }
updatePreviewSubtitle(isPreview: false)
}

private func updatePreviewSubtitle(isPreview: Bool) {
contentWindow?.subtitle = isPreview
? String(format: String(localized: "%@ - Preview"), connection.name)
: connection.name
}

func showAllTablesMetadata() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ struct FocusedCommandActionsModifier: ViewModifier {
connection: DatabaseConnection.preview,
payload: nil,
windowTitle: .constant("SQL Query"),
windowSubtitle: .constant(""),
sidebarState: SharedSidebarState(),
pendingTruncates: .constant([]),
pendingDeletes: .constant([]),
Expand Down
9 changes: 4 additions & 5 deletions TablePro/Views/Main/Extensions/MainContentView+Setup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ extension MainContentView {
?? selectedTab?.title
?? (tabManager.tabs.isEmpty ? connection.name : queryLabel)
}
windowSubtitle = MainSplitViewController.resolveDefaultSubtitle(
tab: selectedTab,
connection: connection
)
Comment on lines +200 to +203
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve preview state in the subtitle

When preview tabs are enabled and a table is opened as a preview, this now sets the subtitle only from the table context, while the commit also removed the previous updatePreviewSubtitle calls. I checked the remaining isPreview usages and there is no other UI marker for a preview tab, even though preview tabs are filtered out of persistence, so a temporary preview window tab is indistinguishable from a promoted table tab until it unexpectedly gets replaced or dropped on restart.

Useful? React with 👍 / 👎.

viewWindow?.representedURL = selectedTab?.content.sourceFileURL
viewWindow?.isDocumentEdited = selectedTab?.content.isFileDirty ?? false
}
Expand All @@ -208,11 +212,6 @@ extension MainContentView {
"[open] configureWindow start windowId=\(windowId, privacy: .public) connId=\(connection.id, privacy: .public)"
)
let isPreview = tabManager.selectedTab?.isPreview ?? payload?.isPreview ?? false
if isPreview {
window.subtitle = String(format: String(localized: "%@ - Preview"), connection.name)
} else {
window.subtitle = connection.name
}

let resolvedId = WindowManager.tabbingIdentifier(for: connection.id)
window.tabbingIdentifier = resolvedId
Expand Down
3 changes: 3 additions & 0 deletions TablePro/Views/Main/MainContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ struct MainContentView: View {

// Shared state from parent
@Binding var windowTitle: String
@Binding var windowSubtitle: String
@Bindable var schemaService = SchemaService.shared
var sidebarState: SharedSidebarState
@Binding var pendingTruncates: Set<String>
Expand Down Expand Up @@ -68,6 +69,7 @@ struct MainContentView: View {
connection: DatabaseConnection,
payload: EditorTabPayload?,
windowTitle: Binding<String>,
windowSubtitle: Binding<String>,
sidebarState: SharedSidebarState,
pendingTruncates: Binding<Set<String>>,
pendingDeletes: Binding<Set<String>>,
Expand All @@ -81,6 +83,7 @@ struct MainContentView: View {
self.connection = connection
self.payload = payload
self._windowTitle = windowTitle
self._windowSubtitle = windowSubtitle
self.sidebarState = sidebarState
self._pendingTruncates = pendingTruncates
self._pendingDeletes = pendingDeletes
Expand Down
123 changes: 123 additions & 0 deletions TableProTests/Services/MainSplitViewControllerTitleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,129 @@ struct MainSplitViewControllerTitleTests {
}
}

@Suite("MainSplitViewController.resolveDefaultSubtitle")
@MainActor
struct MainSplitViewControllerSubtitleTests {
private let connection = DatabaseConnection(name: "MyConnection")

private func tableTab(database: String, schema: String?) -> QueryTab {
var tab = QueryTab(id: UUID(), title: "users", query: "SELECT 1", tabType: .table, tableName: "users")
tab.tableContext.databaseName = database
tab.tableContext.schemaName = schema
return tab
}

@Test("Table tab with database and schema joins them with a middle dot")
func tableTabWithSchemaAndDatabase() {
let subtitle = MainSplitViewController.resolveDefaultSubtitle(
tab: tableTab(database: "myapp", schema: "public"),
connection: connection
)
#expect(subtitle == "myapp · public")
}

@Test("Table tab without schema shows the database alone")
func tableTabWithDatabaseNoSchema() {
let subtitle = MainSplitViewController.resolveDefaultSubtitle(
tab: tableTab(database: "myapp", schema: nil),
connection: connection
)
#expect(subtitle == "myapp")
}

@Test("Table tab with an empty schema shows the database alone")
func tableTabWithEmptySchema() {
let subtitle = MainSplitViewController.resolveDefaultSubtitle(
tab: tableTab(database: "myapp", schema: ""),
connection: connection
)
#expect(subtitle == "myapp")
}

@Test("Table tab with no database falls back to the connection name")
func tableTabWithEmptyDatabaseName() {
let subtitle = MainSplitViewController.resolveDefaultSubtitle(
tab: tableTab(database: "", schema: nil),
connection: connection
)
#expect(subtitle == connection.name)
}

@Test("Table tab with no table name falls back to the connection name")
func tableTabWithNilTableName() {
var tab = QueryTab(id: UUID(), title: "x", query: "SELECT 1", tabType: .table)
tab.tableContext.databaseName = "myapp"
let subtitle = MainSplitViewController.resolveDefaultSubtitle(tab: tab, connection: connection)
#expect(subtitle == connection.name)
}

@Test("Query tab never shows a table subtitle even with a resolved table name")
func queryTabReturnsConnectionName() {
let tab = QueryTab(id: UUID(), title: "q", query: "SELECT 1", tabType: .query, tableName: "users")
let subtitle = MainSplitViewController.resolveDefaultSubtitle(tab: tab, connection: connection)
#expect(subtitle == connection.name)
}

@Test("Nil tab falls back to the connection name")
func nilTabReturnsConnectionName() {
let subtitle = MainSplitViewController.resolveDefaultSubtitle(tab: nil, connection: connection)
#expect(subtitle == connection.name)
}

@Test("Server dashboard tab falls back to the connection name")
func serverDashboardTabReturnsConnectionName() {
let tab = QueryTab(id: UUID(), title: "d", query: "", tabType: .serverDashboard)
let subtitle = MainSplitViewController.resolveDefaultSubtitle(tab: tab, connection: connection)
#expect(subtitle == connection.name)
}

@Test("ER diagram tab falls back to the connection name")
func erDiagramTabReturnsConnectionName() {
let tab = QueryTab(id: UUID(), title: "e", query: "", tabType: .erDiagram)
let subtitle = MainSplitViewController.resolveDefaultSubtitle(tab: tab, connection: connection)
#expect(subtitle == connection.name)
}

@Test("Table payload with database and schema joins them with a middle dot")
func tablePayloadWithSchemaAndDatabase() {
let payload = EditorTabPayload(
connectionId: UUID(),
tabType: .table,
tableName: "users",
databaseName: "myapp",
schemaName: "public"
)
let subtitle = MainSplitViewController.resolveDefaultSubtitle(payload: payload, connection: connection)
#expect(subtitle == "myapp · public")
}

@Test("Table payload without schema shows the database alone")
func tablePayloadWithDatabaseNoSchema() {
let payload = EditorTabPayload(
connectionId: UUID(),
tabType: .table,
tableName: "users",
databaseName: "myapp"
)
let subtitle = MainSplitViewController.resolveDefaultSubtitle(payload: payload, connection: connection)
#expect(subtitle == "myapp")
}

@Test("Table payload with no database falls back to the connection name")
func tablePayloadWithNilDatabase() {
let payload = EditorTabPayload(connectionId: UUID(), tabType: .table, tableName: "users")
let subtitle = MainSplitViewController.resolveDefaultSubtitle(payload: payload, connection: connection)
#expect(subtitle == connection.name)
}

@Test("Query payload falls back to the connection name")
func queryPayloadReturnsConnectionName() {
let payload = EditorTabPayload(connectionId: UUID(), tabType: .query, tableName: "users")
let subtitle = MainSplitViewController.resolveDefaultSubtitle(payload: payload, connection: connection)
#expect(subtitle == connection.name)
}
}

@Suite("QueryTab.fileDisplayTitle")
struct QueryTabFileDisplayTitleTests {
@Test("Returns FileManager display name for the URL")
Expand Down
Loading