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

## [Unreleased]

### Added

- Open in Window on a row inspector text field, for reading or editing a long value on a bigger surface.

### Fixed

- Switch Connection and Open Database now open on a narrow window, and after you remove their toolbar button, instead of doing nothing at all.
- A large text value in the row inspector now scrolls in a resizable text view instead of being clipped, and stays selectable and copyable when the row is read-only.
- The inspector picks its multi-line editor from the value, so a large value in `VARCHAR(MAX)`, `NCLOB` or ClickHouse's `Nullable(String)` is no longer stuck on one line.
- Right-clicking a read-only inspector field now offers Copy Value instead of an empty menu.

## [0.67.1] - 2026-08-22

Expand Down
1 change: 1 addition & 0 deletions TablePro/Core/Storage/AppSettingsStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ final class AppSettingsStorage: Sendable {
saveMCP(.default)
defaults.removeObject(forKey: PreferenceKeys.selectedSettingsPane.name)
defaults.removeObject(forKey: PreferenceKeys.rowInspectorJsonFieldHeight.name)
defaults.removeObject(forKey: PreferenceKeys.rowInspectorTextFieldHeight.name)
defaults.removeObject(forKey: SidebarPersistenceKey.defaultLayout)
}

Expand Down
2 changes: 2 additions & 0 deletions TablePro/Core/Storage/Preferences/PreferenceKeys.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ enum PreferenceKeys {
static let linkedSQLFolders = DefaultsKey<[LinkedSQLFolder]>("com.TablePro.linkedSQLFolders")
static let selectedSettingsPane = DefaultsKey<String>("com.TablePro.settings.selectedPane")
static let rowInspectorJsonFieldHeight = DefaultsKey<Double>("com.TablePro.rightSidebar.jsonFieldHeight")
static let rowInspectorTextFieldHeight = DefaultsKey<Double>("com.TablePro.rightSidebar.textFieldHeight")
static let workspaceRailOrder = DefaultsKey<[WorkspaceID]>("com.TablePro.workspaceRail.order")
static let queryPlanRawFontSize = DefaultsKey<Double>("com.TablePro.queryPlan.rawFontSize")

Expand All @@ -18,6 +19,7 @@ enum PreferenceKeys {
linkedSQLFolders.name,
selectedSettingsPane.name,
rowInspectorJsonFieldHeight.name,
rowInspectorTextFieldHeight.name,
workspaceRailOrder.name,
queryPlanRawFontSize.name,
]
Expand Down
68 changes: 68 additions & 0 deletions TablePro/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -115311,6 +115311,74 @@
}
}
},
"Text Viewer" : {
"localizations" : {
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "텍스트 뷰어"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Metin Görüntüleyici"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
"value" : "Trình xem văn bản"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "文本查看器"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "文字檢視器"
}
}
}
},
"Text: %@" : {
"localizations" : {
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "텍스트: %@"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Metin: %@"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
"value" : "Văn bản: %@"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "文本:%@"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "文字:%@"
}
}
}
},
"That doesn't look like a valid license key. Check for typos and try again." : {
"localizations" : {
"ko" : {
Expand Down
51 changes: 8 additions & 43 deletions TablePro/Views/Connection/ConnectionAdvancedView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Created by Ngo Quoc Dat on 31/3/26.
//

import AppKit
import SwiftUI
import TableProPluginKit

Expand Down Expand Up @@ -121,50 +122,14 @@ struct ConnectionAdvancedView: View {

// MARK: - Startup Commands Editor

struct StartupCommandsEditor: NSViewRepresentable {
struct StartupCommandsEditor: View {
@Binding var text: String

func makeNSView(context: Context) -> NSScrollView {
let scrollView = NSTextView.scrollableTextView()
guard let textView = scrollView.documentView as? NSTextView else { return scrollView }

textView.font = .monospacedSystemFont(ofSize: NSFont.systemFontSize, weight: .regular)
textView.isAutomaticQuoteSubstitutionEnabled = false
textView.isAutomaticDashSubstitutionEnabled = false
textView.isAutomaticTextReplacementEnabled = false
textView.isAutomaticSpellingCorrectionEnabled = false
textView.isRichText = false
textView.string = text
textView.textContainerInset = NSSize(width: 4, height: 6)
textView.delegate = context.coordinator

scrollView.borderType = .bezelBorder
scrollView.hasVerticalScroller = true

return scrollView
}

func updateNSView(_ scrollView: NSScrollView, context: Context) {
guard let textView = scrollView.documentView as? NSTextView else { return }
if textView.string != text {
textView.string = text
}
}

func makeCoordinator() -> Coordinator {
Coordinator(text: $text)
}

final class Coordinator: NSObject, NSTextViewDelegate {
private var text: Binding<String>

init(text: Binding<String>) {
self.text = text
}

func textDidChange(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else { return }
text.wrappedValue = textView.string
}
var body: some View {
TextValueEditor(
text: $text,
font: .monospacedSystemFont(ofSize: NSFont.systemFontSize, weight: .regular),
borderType: .bezelBorder
)
}
}
63 changes: 8 additions & 55 deletions TablePro/Views/ConnectionForm/Panes/AIRulesPaneView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,62 +42,15 @@ struct AIRulesPaneView: View {
}
}

private struct AIRulesEditor: NSViewRepresentable {
private struct AIRulesEditor: View {
@Binding var text: String

func makeNSView(context: Context) -> NSScrollView {
let scrollView = NSTextView.scrollableTextView()
guard let textView = scrollView.documentView as? NSTextView else { return scrollView }

textView.font = .monospacedSystemFont(ofSize: NSFont.systemFontSize, weight: .regular)
textView.isAutomaticQuoteSubstitutionEnabled = false
textView.isAutomaticDashSubstitutionEnabled = false
textView.isAutomaticTextReplacementEnabled = false
textView.isAutomaticSpellingCorrectionEnabled = false
textView.isRichText = false
textView.string = text
textView.textContainerInset = NSSize(width: 4, height: 6)
textView.delegate = context.coordinator

scrollView.borderType = .bezelBorder
scrollView.hasVerticalScroller = true

return scrollView
}

func updateNSView(_ scrollView: NSScrollView, context: Context) {
guard let textView = scrollView.documentView as? NSTextView else { return }
if textView.string != text {
textView.string = text
}
}

func makeCoordinator() -> Coordinator {
Coordinator(text: $text)
}

final class Coordinator: NSObject, NSTextViewDelegate {
private var text: Binding<String>

init(text: Binding<String>) {
self.text = text
}

func textDidChange(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else { return }
text.wrappedValue = textView.string
}

func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
if commandSelector == #selector(NSResponder.insertTab(_:)) {
textView.window?.selectNextKeyView(nil)
return true
}
if commandSelector == #selector(NSResponder.insertBacktab(_:)) {
textView.window?.selectPreviousKeyView(nil)
return true
}
return false
}
var body: some View {
TextValueEditor(
text: $text,
font: .monospacedSystemFont(ofSize: NSFont.systemFontSize, weight: .regular),
borderType: .bezelBorder,
movesFocusOnTab: true
)
}
}
83 changes: 17 additions & 66 deletions TablePro/Views/Results/JSONViewerWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,84 +7,35 @@ import AppKit
import SwiftUI

@MainActor
final class JSONViewerWindowController {
private static var activeWindows: [ObjectIdentifier: JSONViewerWindowController] = [:]
private static let defaultSize = NSSize(width: 640, height: 500)
private static let minSize = NSSize(width: 400, height: 300)
private static let autosaveName: NSWindow.FrameAutosaveName = "JSONViewerWindow"

private var window: NSWindow?
private var closeObserver: NSObjectProtocol?

internal final class JSONViewerWindowController: ValueViewerWindowController {
@discardableResult
static func open(
text: String?,
columnName: String?,
isEditable: Bool,
onCommit: ((String) -> Void)?
) -> JSONViewerWindowController {
let controller = JSONViewerWindowController()
controller.showWindow(text: text, columnName: columnName, isEditable: isEditable, onCommit: onCommit)
return controller
}

/// A popped-out editor commits through the display row it was opened from, so whoever opened it
/// has to be able to close it once those rows are gone.
func close() {
window?.close()
}

private func showWindow(
text: String?,
columnName: String?,
isEditable: Bool,
onCommit: ((String) -> Void)?
) {
let window = NSWindow(
contentRect: NSRect(origin: .zero, size: Self.defaultSize),
styleMask: [.titled, .closable, .resizable, .miniaturizable],
backing: .buffered,
defer: false
)
window.identifier = NSUserInterfaceItemIdentifier("json-viewer")
let title: String
if let columnName {
window.title = String(format: String(localized: "JSON: %@"), columnName)
title = String(format: String(localized: "JSON: %@"), columnName)
} else {
window.title = String(localized: "JSON Viewer")
title = String(localized: "JSON Viewer")
}
window.isReleasedWhenClosed = false
window.minSize = Self.minSize
window.collectionBehavior = [.fullScreenPrimary]

let closeWindow: () -> Void = { [weak window] in window?.close() }
let contentView = JSONViewerWindowContent(
initialValue: text,
isEditable: isEditable,
onCommit: onCommit,
onDismiss: closeWindow
)
window.contentView = NSHostingView(rootView: contentView)

self.window = window

let key = ObjectIdentifier(self)
Self.activeWindows[key] = self

closeObserver = NotificationCenter.default.addObserver(
forName: NSWindow.willCloseNotification,
object: window,
queue: .main
) { [weak self] _ in
Task { @MainActor in
Self.activeWindows.removeValue(forKey: key)
self?.closeObserver.map { NotificationCenter.default.removeObserver($0) }
self?.closeObserver = nil
self?.window = nil
}
let controller = JSONViewerWindowController()
controller.present(
identifier: "json-viewer",
title: title,
autosaveName: "JSONViewerWindow"
) { dismiss in
JSONViewerWindowContent(
initialValue: text,
isEditable: isEditable,
onCommit: onCommit,
onDismiss: dismiss
)
}

window.applyAutosaveName(Self.autosaveName)
window.makeKeyAndOrderFront(nil)
return controller
}
}

Expand Down
Loading
Loading