added typeaheead and introspetion
This commit is contained in:
@@ -123,7 +123,7 @@ struct MainView: View {
|
|||||||
set: { if !$0 { schemaDiagramDatabase = nil } }
|
set: { if !$0 { schemaDiagramDatabase = nil } }
|
||||||
)) {
|
)) {
|
||||||
if let database = schemaDiagramDatabase {
|
if let database = schemaDiagramDatabase {
|
||||||
DBSynSchemaDiagramView(model: model, database: database)
|
DBSynSchemaDiagramView(model: model, database: database, onClose: { schemaDiagramDatabase = nil })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showingQueryPlan) {
|
.sheet(isPresented: $showingQueryPlan) {
|
||||||
|
|||||||
@@ -11,23 +11,32 @@ final class SchemaDiagramPrintView: NSView {
|
|||||||
let diagram: String
|
let diagram: String
|
||||||
let entries: [Entry]
|
let entries: [Entry]
|
||||||
let links: [Link]
|
let links: [Link]
|
||||||
|
let theme: String
|
||||||
private let cardWidth: CGFloat = 260
|
private let cardWidth: CGFloat = 260
|
||||||
private let headerHeight: CGFloat = 34
|
private let headerHeight: CGFloat = 34
|
||||||
private let rowHeight: CGFloat = 19
|
private let rowHeight: CGFloat = 19
|
||||||
private let margin: CGFloat = 55
|
private let margin: CGFloat = 55
|
||||||
private let titleHeight: CGFloat = 76
|
private let titleHeight: CGFloat = 76
|
||||||
private let navy = NSColor(calibratedRed: 0.12, green: 0.20, blue: 0.34, alpha: 1)
|
private let navy = NSColor(calibratedRed: 0.12, green: 0.20, blue: 0.34, alpha: 1)
|
||||||
private let blue = NSColor(calibratedRed: 0.14, green: 0.38, blue: 0.64, alpha: 1)
|
private var blue: NSColor {
|
||||||
private let violet = NSColor(calibratedRed: 0.34, green: 0.30, blue: 0.56, alpha: 1)
|
switch theme {
|
||||||
|
case "Slate": return NSColor(calibratedRed: 0.30, green: 0.38, blue: 0.49, alpha: 1)
|
||||||
|
case "Forest": return NSColor(calibratedRed: 0.16, green: 0.43, blue: 0.36, alpha: 1)
|
||||||
|
case "Plum": return NSColor(calibratedRed: 0.42, green: 0.32, blue: 0.55, alpha: 1)
|
||||||
|
default: return NSColor(calibratedRed: 0.12, green: 0.38, blue: 0.66, alpha: 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var violet: NSColor { blue.blended(withFraction: 0.25, of: .systemPurple) ?? blue }
|
||||||
private let ink = NSColor(calibratedRed: 0.16, green: 0.23, blue: 0.32, alpha: 1)
|
private let ink = NSColor(calibratedRed: 0.16, green: 0.23, blue: 0.32, alpha: 1)
|
||||||
private let muted = NSColor(calibratedRed: 0.43, green: 0.49, blue: 0.57, alpha: 1)
|
private let muted = NSColor(calibratedRed: 0.43, green: 0.49, blue: 0.57, alpha: 1)
|
||||||
private let border = NSColor(calibratedRed: 0.79, green: 0.83, blue: 0.88, alpha: 1)
|
private let border = NSColor(calibratedRed: 0.79, green: 0.83, blue: 0.88, alpha: 1)
|
||||||
|
|
||||||
init(database: String, diagram: String, entries: [Entry], links: [Link]) {
|
init(database: String, diagram: String, entries: [Entry], links: [Link], theme: String = "Ocean") {
|
||||||
self.database = database
|
self.database = database
|
||||||
self.diagram = diagram
|
self.diagram = diagram
|
||||||
self.entries = entries
|
self.entries = entries
|
||||||
self.links = links
|
self.links = links
|
||||||
|
self.theme = theme
|
||||||
let width = max(900, (entries.map { $0.origin.x + 260 }.max() ?? 750) + 110)
|
let width = max(900, (entries.map { $0.origin.x + 260 }.max() ?? 750) + 110)
|
||||||
let height = max(650, (entries.map { $0.origin.y + CGFloat($0.columns.count) * 19 + 46 }.max() ?? 500) + 130)
|
let height = max(650, (entries.map { $0.origin.y + CGFloat($0.columns.count) * 19 + 46 }.max() ?? 500) + 130)
|
||||||
super.init(frame: CGRect(x: 0, y: 0, width: width, height: height))
|
super.init(frame: CGRect(x: 0, y: 0, width: width, height: height))
|
||||||
@@ -117,10 +126,15 @@ final class SchemaDiagramPrintView: NSView {
|
|||||||
let delta: CGFloat = right ? 55 : -55
|
let delta: CGFloat = right ? 55 : -55
|
||||||
svg += "<path d=\"M \(sx) \(a.midY) C \(sx + delta) \(a.midY), \(ex - delta) \(b.midY), \(ex) \(b.midY)\" fill=\"none\" stroke=\"#6488af\" stroke-width=\"1.7\"/>\n"
|
svg += "<path d=\"M \(sx) \(a.midY) C \(sx + delta) \(a.midY), \(ex - delta) \(b.midY), \(ex) \(b.midY)\" fill=\"none\" stroke=\"#6488af\" stroke-width=\"1.7\"/>\n"
|
||||||
}
|
}
|
||||||
|
let svgBlue = blue.usingColorSpace(.deviceRGB) ?? blue
|
||||||
|
let svgViolet = violet.usingColorSpace(.deviceRGB) ?? violet
|
||||||
|
func hex(_ color: NSColor) -> String {
|
||||||
|
String(format: "#%02X%02X%02X", Int(color.redComponent * 255), Int(color.greenComponent * 255), Int(color.blueComponent * 255))
|
||||||
|
}
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
let r = cardRect(entry)
|
let r = cardRect(entry)
|
||||||
svg += "<rect x=\"\(r.minX)\" y=\"\(r.minY)\" width=\"\(r.width)\" height=\"\(r.height)\" rx=\"7\" fill=\"white\" stroke=\"#cbd4e0\"/>\n"
|
svg += "<rect x=\"\(r.minX)\" y=\"\(r.minY)\" width=\"\(r.width)\" height=\"\(r.height)\" rx=\"7\" fill=\"white\" stroke=\"#cbd4e0\"/>\n"
|
||||||
svg += "<rect x=\"\(r.minX)\" y=\"\(r.minY)\" width=\"\(r.width)\" height=\"34\" fill=\"\(entry.isView ? "#574c8f" : "#2461a3")\"/>\n"
|
svg += "<rect x=\"\(r.minX)\" y=\"\(r.minY)\" width=\"\(r.width)\" height=\"34\" fill=\"\(entry.isView ? hex(svgViolet) : hex(svgBlue))\"/>\n"
|
||||||
svg += "<text x=\"\(r.minX + 12)\" y=\"\(r.minY + 23)\" font-family=\"Arial,sans-serif\" font-size=\"12\" font-weight=\"600\" fill=\"white\">\(escape(entry.name))</text>\n"
|
svg += "<text x=\"\(r.minX + 12)\" y=\"\(r.minY + 23)\" font-family=\"Arial,sans-serif\" font-size=\"12\" font-weight=\"600\" fill=\"white\">\(escape(entry.name))</text>\n"
|
||||||
for (index, column) in entry.columns.enumerated() {
|
for (index, column) in entry.columns.enumerated() {
|
||||||
let y = r.minY + 34 + CGFloat(index) * 19
|
let y = r.minY + 34 + CGFloat(index) * 19
|
||||||
|
|||||||
+126
-20
@@ -18,6 +18,7 @@ private struct DiagramPoint: Codable {
|
|||||||
struct DBSynSchemaDiagramView: View {
|
struct DBSynSchemaDiagramView: View {
|
||||||
@ObservedObject var model: DatabaseModel
|
@ObservedObject var model: DatabaseModel
|
||||||
let database: String
|
let database: String
|
||||||
|
var onClose: () -> Void = {}
|
||||||
@State private var positions: [String: CGPoint] = [:]
|
@State private var positions: [String: CGPoint] = [:]
|
||||||
@State private var relations: [DiagramRelation] = []
|
@State private var relations: [DiagramRelation] = []
|
||||||
@State private var zoom: CGFloat = 1
|
@State private var zoom: CGFloat = 1
|
||||||
@@ -26,6 +27,10 @@ struct DBSynSchemaDiagramView: View {
|
|||||||
@State private var loadingRelationships = false
|
@State private var loadingRelationships = false
|
||||||
@State private var errorMessage: String?
|
@State private var errorMessage: String?
|
||||||
@State private var showViews = true
|
@State private var showViews = true
|
||||||
|
@State private var showingObjects = true
|
||||||
|
@State private var includedObjects: Set<String> = []
|
||||||
|
@State private var theme = "Ocean"
|
||||||
|
@State private var showAllColumns = false
|
||||||
@State private var relatedOnly = false
|
@State private var relatedOnly = false
|
||||||
@State private var diagramName = "Main"
|
@State private var diagramName = "Main"
|
||||||
@State private var diagrams: [String] = ["Main"]
|
@State private var diagrams: [String] = ["Main"]
|
||||||
@@ -38,13 +43,13 @@ struct DBSynSchemaDiagramView: View {
|
|||||||
private let nodeWidth: CGFloat = 245
|
private let nodeWidth: CGFloat = 245
|
||||||
private let headerHeight: CGFloat = 36
|
private let headerHeight: CGFloat = 36
|
||||||
private let rowHeight: CGFloat = 21
|
private let rowHeight: CGFloat = 21
|
||||||
private let maxColumns = 10
|
private var maxColumns: Int { showAllColumns ? 200 : 10 }
|
||||||
private var snapshot: DatabaseModel.SchemaIntrospection? { model.introspection(for: database) }
|
private var snapshot: DatabaseModel.SchemaIntrospection? { model.introspection(for: database) }
|
||||||
private var objects: [String] {
|
private var objects: [String] {
|
||||||
guard let snapshot else { return [] }
|
guard let snapshot else { return [] }
|
||||||
let all = snapshot.tables + (showViews ? snapshot.views : [])
|
let all = snapshot.tables + (showViews ? snapshot.views : [])
|
||||||
return all.filter { name in
|
return all.filter { name in
|
||||||
(search.isEmpty || name.localizedCaseInsensitiveContains(search)) &&
|
includedObjects.contains(name) && (search.isEmpty || name.localizedCaseInsensitiveContains(search)) &&
|
||||||
(!relatedOnly || selectedObject == nil || name == selectedObject || relations.contains { ($0.from == selectedObject && $0.to == name) || ($0.to == selectedObject && $0.from == name) })
|
(!relatedOnly || selectedObject == nil || name == selectedObject || relations.contains { ($0.from == selectedObject && $0.to == name) || ($0.to == selectedObject && $0.from == name) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,6 +58,27 @@ struct DBSynSchemaDiagramView: View {
|
|||||||
}
|
}
|
||||||
private var storageKey: String { "\(diagramNamespace).layout.\(diagramName)" }
|
private var storageKey: String { "\(diagramNamespace).layout.\(diagramName)" }
|
||||||
private var catalogKey: String { "\(diagramNamespace).catalog" }
|
private var catalogKey: String { "\(diagramNamespace).catalog" }
|
||||||
|
private var selectionKey: String { "\(diagramNamespace).objects.\(diagramName)" }
|
||||||
|
private var themeKey: String { "\(diagramNamespace).theme.\(diagramName)" }
|
||||||
|
private var allObjects: [String] { (snapshot?.tables ?? []) + (snapshot?.views ?? []) }
|
||||||
|
private var nodeColor: Color {
|
||||||
|
switch theme {
|
||||||
|
case "Slate": return Color(red: 0.30, green: 0.38, blue: 0.49)
|
||||||
|
case "Forest": return Color(red: 0.16, green: 0.43, blue: 0.36)
|
||||||
|
case "Plum": return Color(red: 0.42, green: 0.32, blue: 0.55)
|
||||||
|
default: return Color(red: 0.12, green: 0.38, blue: 0.66)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private func restoreOptions() {
|
||||||
|
if let stored = UserDefaults.standard.stringArray(forKey: selectionKey) {
|
||||||
|
includedObjects = Set(stored)
|
||||||
|
} else { includedObjects = Set(allObjects) }
|
||||||
|
theme = UserDefaults.standard.string(forKey: themeKey) ?? "Ocean"
|
||||||
|
}
|
||||||
|
private func saveOptions() {
|
||||||
|
UserDefaults.standard.set(Array(includedObjects).sorted(), forKey: selectionKey)
|
||||||
|
UserDefaults.standard.set(theme, forKey: themeKey)
|
||||||
|
}
|
||||||
private func columns(_ name: String) -> [DatabaseModel.IntrospectedColumn] {
|
private func columns(_ name: String) -> [DatabaseModel.IntrospectedColumn] {
|
||||||
snapshot?.columnsByTable[name] ?? []
|
snapshot?.columnsByTable[name] ?? []
|
||||||
}
|
}
|
||||||
@@ -69,9 +95,18 @@ struct DBSynSchemaDiagramView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
HStack(spacing: 12) {
|
// Two compact toolbar rows avoid the truncated controls shown in the original sheet.
|
||||||
Label("Schema Diagram — \(database)", systemImage: "point.3.connected.trianglepath.dotted")
|
HStack(spacing: 10) {
|
||||||
.font(.headline)
|
Label(database, systemImage: "point.3.connected.trianglepath.dotted")
|
||||||
|
.font(.headline).lineLimit(1)
|
||||||
|
Text("SCHEMA DESIGNER").font(.caption2.weight(.semibold)).foregroundStyle(.secondary)
|
||||||
|
Spacer(minLength: 12)
|
||||||
|
Button("Close", systemImage: "xmark") { savePositions(); saveOptions(); onClose() }
|
||||||
|
.labelStyle(.iconOnly).help("Close diagram")
|
||||||
|
.keyboardShortcut(.cancelAction)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16).padding(.top, 12).padding(.bottom, 8)
|
||||||
|
HStack(spacing: 10) {
|
||||||
Picker("Diagram", selection: $diagramName) {
|
Picker("Diagram", selection: $diagramName) {
|
||||||
ForEach(diagrams, id: \.self) { Text($0).tag($0) }
|
ForEach(diagrams, id: \.self) { Text($0).tag($0) }
|
||||||
}
|
}
|
||||||
@@ -80,22 +115,37 @@ struct DBSynSchemaDiagramView: View {
|
|||||||
savePositions(named: oldValue)
|
savePositions(named: oldValue)
|
||||||
positions = [:]
|
positions = [:]
|
||||||
restorePositions()
|
restorePositions()
|
||||||
|
restoreOptions()
|
||||||
if positions.isEmpty { arrange() }
|
if positions.isEmpty { arrange() }
|
||||||
}
|
}
|
||||||
Button { showingNewDiagram = true } label: { Image(systemName: "plus") }
|
Button { showingNewDiagram = true } label: { Image(systemName: "plus") }
|
||||||
.help("New diagram")
|
.help("New diagram")
|
||||||
Button { showingDeleteConfirmation = true } label: { Image(systemName: "trash") }
|
Button { showingDeleteConfirmation = true } label: { Image(systemName: "trash") }
|
||||||
.disabled(diagrams.count <= 1)
|
.disabled(diagrams.count <= 1).help("Delete diagram")
|
||||||
.help("Delete current diagram")
|
Divider().frame(height: 18)
|
||||||
Spacer()
|
Button { showingObjects.toggle() } label: {
|
||||||
|
Label("Tables", systemImage: "sidebar.left")
|
||||||
|
}.help("Show or hide the table selection sidebar")
|
||||||
TextField("Find table or view", text: $search)
|
TextField("Find table or view", text: $search)
|
||||||
.textFieldStyle(.roundedBorder)
|
.textFieldStyle(.roundedBorder).frame(minWidth: 130, idealWidth: 220, maxWidth: 260)
|
||||||
.frame(width: 200)
|
|
||||||
Toggle("Views", isOn: $showViews).toggleStyle(.checkbox)
|
Toggle("Views", isOn: $showViews).toggleStyle(.checkbox)
|
||||||
Toggle("Related", isOn: $relatedOnly).toggleStyle(.checkbox)
|
Toggle("Related", isOn: $relatedOnly).toggleStyle(.checkbox)
|
||||||
Button("Auto Layout") { arrange(); savePositions() }
|
Spacer(minLength: 8)
|
||||||
|
Menu {
|
||||||
|
ForEach(["Ocean", "Slate", "Forest", "Plum"], id: \.self) { option in
|
||||||
|
Button { theme = option; saveOptions() } label: {
|
||||||
|
if theme == option { Label(option, systemImage: "checkmark") }
|
||||||
|
else { Text(option) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: { Label("Colours", systemImage: "paintpalette") }
|
||||||
|
Menu {
|
||||||
|
Toggle("Show all columns", isOn: $showAllColumns)
|
||||||
|
Button("Auto Layout") { arrange(); savePositions() }
|
||||||
|
Button("Refresh Metadata") { model.introspect(database: database); loadRelationships() }
|
||||||
|
} label: { Label("Layout", systemImage: "slider.horizontal.3") }
|
||||||
Button { zoom = max(0.4, zoom - 0.15) } label: { Image(systemName: "minus.magnifyingglass") }
|
Button { zoom = max(0.4, zoom - 0.15) } label: { Image(systemName: "minus.magnifyingglass") }
|
||||||
Text("\(Int(zoom * 100))%").monospacedDigit().frame(width: 46)
|
Text("\(Int(zoom * 100))%").monospacedDigit().frame(width: 45)
|
||||||
Button { zoom = min(2, zoom + 0.15) } label: { Image(systemName: "plus.magnifyingglass") }
|
Button { zoom = min(2, zoom + 0.15) } label: { Image(systemName: "plus.magnifyingglass") }
|
||||||
Menu {
|
Menu {
|
||||||
Button("Export PDF…") { exportDiagram(.pdf) }
|
Button("Export PDF…") { exportDiagram(.pdf) }
|
||||||
@@ -104,10 +154,46 @@ struct DBSynSchemaDiagramView: View {
|
|||||||
Divider()
|
Divider()
|
||||||
Button("Print…") { printDiagram() }
|
Button("Print…") { printDiagram() }
|
||||||
} label: { Label("Export / Print", systemImage: "square.and.arrow.up") }
|
} label: { Label("Export / Print", systemImage: "square.and.arrow.up") }
|
||||||
Button("Refresh") { model.introspect(database: database); loadRelationships() }
|
|
||||||
}
|
}
|
||||||
.padding(12)
|
.controlSize(.small)
|
||||||
|
.padding(.horizontal, 16).padding(.bottom, 12)
|
||||||
Divider()
|
Divider()
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
if showingObjects {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
HStack {
|
||||||
|
Text("OBJECTS").font(.caption.weight(.semibold)).foregroundStyle(.secondary)
|
||||||
|
Spacer()
|
||||||
|
Text("\(includedObjects.count)/\(allObjects.count)")
|
||||||
|
.font(.caption.monospacedDigit()).foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Button("All") { includedObjects = Set(allObjects); saveOptions() }
|
||||||
|
Button("None") { includedObjects.removeAll(); saveOptions() }
|
||||||
|
Spacer()
|
||||||
|
}.buttonStyle(.link)
|
||||||
|
ScrollView {
|
||||||
|
LazyVStack(alignment: .leading, spacing: 5) {
|
||||||
|
ForEach(allObjects.filter { search.isEmpty || $0.localizedCaseInsensitiveContains(search) }, id: \.self) { name in
|
||||||
|
Toggle(isOn: Binding(
|
||||||
|
get: { includedObjects.contains(name) },
|
||||||
|
set: { enabled in
|
||||||
|
if enabled { includedObjects.insert(name) }
|
||||||
|
else { includedObjects.remove(name) }
|
||||||
|
saveOptions()
|
||||||
|
})) {
|
||||||
|
Label(name, systemImage: snapshot?.views.contains(name) == true ? "eye" : "tablecells")
|
||||||
|
.lineLimit(1).font(.system(size: 12))
|
||||||
|
}.toggleStyle(.checkbox)
|
||||||
|
}
|
||||||
|
}.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(12).frame(width: 235)
|
||||||
|
.background(Color(nsColor: .controlBackgroundColor))
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
VStack(spacing: 0) {
|
||||||
if snapshot == nil {
|
if snapshot == nil {
|
||||||
ContentUnavailableView("No Introspection Data", systemImage: "externaldrive.badge.questionmark", description: Text("Run Introspection on this database to load tables and views."))
|
ContentUnavailableView("No Introspection Data", systemImage: "externaldrive.badge.questionmark", description: Text("Run Introspection on this database to load tables and views."))
|
||||||
Button("Introspect Now") { model.introspect(database: database); loadRelationships() }.padding()
|
Button("Introspect Now") { model.introspect(database: database); loadRelationships() }.padding()
|
||||||
@@ -142,9 +228,14 @@ struct DBSynSchemaDiagramView: View {
|
|||||||
}
|
}
|
||||||
.background(Color(nsColor: .textBackgroundColor))
|
.background(Color(nsColor: .textBackgroundColor))
|
||||||
.onChange(of: snapshot?.loadedAt) { _, _ in
|
.onChange(of: snapshot?.loadedAt) { _, _ in
|
||||||
|
if UserDefaults.standard.stringArray(forKey: selectionKey) == nil {
|
||||||
|
includedObjects = Set(allObjects)
|
||||||
|
}
|
||||||
if positions.isEmpty { arrange() }
|
if positions.isEmpty { arrange() }
|
||||||
loadRelationships()
|
loadRelationships()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Divider()
|
Divider()
|
||||||
HStack {
|
HStack {
|
||||||
@@ -157,9 +248,10 @@ struct DBSynSchemaDiagramView: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.padding(9)
|
.padding(9)
|
||||||
}
|
}
|
||||||
.frame(minWidth: 950, minHeight: 650)
|
.frame(minWidth: 850, idealWidth: 1320, minHeight: 570, idealHeight: 850)
|
||||||
.onAppear { loadCatalog(); restorePositions(); if positions.isEmpty { arrange() }; loadRelationships() }
|
.background(DiagramResizableWindow())
|
||||||
.onDisappear { savePositions() }
|
.onAppear { loadCatalog(); restorePositions(); restoreOptions(); if positions.isEmpty { arrange() }; loadRelationships() }
|
||||||
|
.onDisappear { savePositions(); saveOptions() }
|
||||||
.alert("New Schema Diagram", isPresented: $showingNewDiagram) {
|
.alert("New Schema Diagram", isPresented: $showingNewDiagram) {
|
||||||
TextField("Diagram name", text: $newDiagramName)
|
TextField("Diagram name", text: $newDiagramName)
|
||||||
Button("Create") { createDiagram() }
|
Button("Create") { createDiagram() }
|
||||||
@@ -194,7 +286,7 @@ struct DBSynSchemaDiagramView: View {
|
|||||||
.padding(.horizontal, 10)
|
.padding(.horizontal, 10)
|
||||||
.frame(height: headerHeight)
|
.frame(height: headerHeight)
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
.background(isView ? Color.indigo : Color.accentColor)
|
.background(isView ? nodeColor.opacity(0.76) : nodeColor)
|
||||||
ForEach(Array(columns(name).prefix(maxColumns)), id: \.id) { column in
|
ForEach(Array(columns(name).prefix(maxColumns)), id: \.id) { column in
|
||||||
HStack(spacing: 5) {
|
HStack(spacing: 5) {
|
||||||
Image(systemName: column.key == "PRI" ? "key.fill" : "circle.fill")
|
Image(systemName: column.key == "PRI" ? "key.fill" : "circle.fill")
|
||||||
@@ -236,7 +328,7 @@ struct DBSynSchemaDiagramView: View {
|
|||||||
@State private var dragOrigins: [String: CGPoint] = [:]
|
@State private var dragOrigins: [String: CGPoint] = [:]
|
||||||
|
|
||||||
private func arrange() {
|
private func arrange() {
|
||||||
let names = (snapshot?.tables ?? []) + (snapshot?.views ?? [])
|
let names = allObjects.filter { includedObjects.contains($0) }
|
||||||
let countPerRow = max(1, Int(ceil(sqrt(Double(max(1, names.count))))))
|
let countPerRow = max(1, Int(ceil(sqrt(Double(max(1, names.count))))))
|
||||||
for (index, name) in names.enumerated() {
|
for (index, name) in names.enumerated() {
|
||||||
positions[name] = CGPoint(x: CGFloat(index % countPerRow) * 290 + 30,
|
positions[name] = CGPoint(x: CGFloat(index % countPerRow) * 290 + 30,
|
||||||
@@ -308,7 +400,7 @@ struct DBSynSchemaDiagramView: View {
|
|||||||
}
|
}
|
||||||
let links = relations.filter { objects.contains($0.from) && objects.contains($0.to) }
|
let links = relations.filter { objects.contains($0.from) && objects.contains($0.to) }
|
||||||
.map { SchemaDiagramPrintView.Link(from: $0.from, to: $0.to, column: $0.column, target: $0.targetColumn) }
|
.map { SchemaDiagramPrintView.Link(from: $0.from, to: $0.to, column: $0.column, target: $0.targetColumn) }
|
||||||
return SchemaDiagramPrintView(database: database, diagram: diagramName, entries: entries, links: links)
|
return SchemaDiagramPrintView(database: database, diagram: diagramName, entries: entries, links: links, theme: theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func exportDiagram(_ format: DiagramFormat) {
|
private func exportDiagram(_ format: DiagramFormat) {
|
||||||
@@ -352,3 +444,17 @@ struct DBSynSchemaDiagramView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Allows the schema designer sheet to be resized rather than locking its initial dimensions.
|
||||||
|
private struct DiagramResizableWindow: NSViewRepresentable {
|
||||||
|
func makeNSView(context: Context) -> NSView {
|
||||||
|
let view = NSView(frame: .zero)
|
||||||
|
DispatchQueue.main.async { [weak view] in
|
||||||
|
guard let window = view?.window else { return }
|
||||||
|
window.styleMask.insert(.resizable)
|
||||||
|
window.minSize = NSSize(width: 850, height: 570)
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
func updateNSView(_ nsView: NSView, context: Context) { }
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Phase 73.4 — Schema Designer UI
|
||||||
|
|
||||||
|
- Two-row toolbar avoids clipping and narrow, vertical labels.
|
||||||
|
- Explicit Close control in the header.
|
||||||
|
- Resizable schema designer sheet with a practical minimum size.
|
||||||
|
- Show/hide object sidebar; include/exclude individual tables and views.
|
||||||
|
- All/None selection and search.
|
||||||
|
- Four colour palettes (Ocean, Slate, Forest, Plum) applied to on-screen cards and PDF/PNG/SVG output.
|
||||||
|
- Diagram-specific saved object selections and colour palette.
|
||||||
|
- Optional full column display and auto-layout controls.
|
||||||
|
- Existing printing and export retained.
|
||||||
|
|
||||||
|
The schema designer remains read-only and does not alter MySQL objects.
|
||||||
|
|
||||||
|
Full Xcode compilation and macOS UI verification are required.
|
||||||
Reference in New Issue
Block a user