-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Expand file tree
/
Copy pathProjectBuildSettingsTabView.swift
More file actions
243 lines (223 loc) · 9.28 KB
/
Copy pathProjectBuildSettingsTabView.swift
File metadata and controls
243 lines (223 loc) · 9.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
import CMUXProjectModel
import SwiftUI
/// Build Settings tab inside ``ProjectPanelView``.
///
/// Renders the "Levels" view: each row is one setting key, with columns for
/// the resolved value, the target override, and the project value. The
/// adapter does not yet shell out to `xcodebuild -showBuildSettings`, so the
/// Resolved column is computed locally as the first non-empty value in the
/// stack (target → project → empty). The cell whose value won is marked.
struct ProjectBuildSettingsTabView: View {
@ObservedObject var panel: ProjectPanel
let model: ProjectModel
/// Drives focus into the settings filter text field when requested.
@FocusState private var focus: ProjectPanelSearchFocus?
var body: some View {
let computedRows = rows
VStack(alignment: .leading, spacing: 0) {
controlsRow(rowCount: computedRows.count)
Divider()
tableView(rows: computedRows)
Spacer(minLength: 0)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.onChange(of: panel.focusState.request) { _, newValue in
if newValue == .settings {
focus = .settings
panel.focusState.request = nil
}
}
}
@ViewBuilder
private func controlsRow(rowCount: Int) -> some View {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 8) {
Text("Target")
.font(.system(size: 11))
.foregroundStyle(.secondary)
targetPicker
Spacer(minLength: 4)
Image(systemName: "magnifyingglass")
.foregroundStyle(.secondary)
TextField("Filter settings", text: $panel.settingsSearchText)
.textFieldStyle(.plain)
.font(.system(size: 12))
.focused($focus, equals: .settings)
Toggle("Customized only", isOn: $panel.settingsCustomizedOnly)
.toggleStyle(.checkbox)
.font(.system(size: 11))
Text("\(rowCount) settings")
.font(.system(size: 11))
.foregroundStyle(.secondary)
}
}
.padding(.horizontal, 14)
.padding(.vertical, 8)
.background(Color(NSColor.controlBackgroundColor).opacity(0.4))
}
@ViewBuilder
private var targetPicker: some View {
if let module = selectedModule ?? model.modules.first {
Picker(
"Target",
selection: Binding(
get: { panel.selectedTargetID ?? module.targets.first?.id ?? TargetID(rawValue: "") },
set: { panel.selectedTargetID = $0 }
)
) {
ForEach(module.targets) { target in
Text(target.displayName).tag(target.id)
}
}
.pickerStyle(.menu)
.labelsHidden()
.frame(maxWidth: 160)
}
}
private static let settingColumnWidth: CGFloat = 240
private static let valueColumnWidth: CGFloat = 170
@ViewBuilder
private func tableView(rows computedRows: [BuildSettingRow]) -> some View {
ScrollView([.horizontal, .vertical]) {
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 0) {
Spacer().frame(width: 3)
columnHeader("Setting", weight: Self.settingColumnWidth, alignment: .leading)
columnHeader("Effective", weight: Self.valueColumnWidth, alignment: .leading)
columnHeader("Target", weight: Self.valueColumnWidth, alignment: .leading)
columnHeader("Project", weight: Self.valueColumnWidth, alignment: .leading)
}
.padding(.vertical, 6)
.padding(.horizontal, 14)
.background(Color(NSColor.controlBackgroundColor).opacity(0.25))
Divider()
ForEach(Array(computedRows.enumerated()), id: \.element.key) { index, row in
SettingsRow(
row: row,
settingColumnWidth: Self.settingColumnWidth,
valueColumnWidth: Self.valueColumnWidth
)
.padding(.horizontal, 14)
.padding(.vertical, 4)
.background(
index.isMultiple(of: 2)
? Color.primary.opacity(0.025)
: Color.clear
)
.overlay(alignment: .leading) {
if row.winner == .target {
Rectangle()
.fill(Color.accentColor)
.frame(width: 3)
}
}
}
}
}
}
@ViewBuilder
private func columnHeader(_ title: String, weight: CGFloat, alignment: Alignment) -> some View {
Text(title)
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(.secondary)
.frame(width: weight, alignment: alignment)
}
private var selectedModule: ProjectModule? {
if let targetID = panel.selectedTargetID,
let owner = model.modules.first(where: { $0.target(for: targetID) != nil }) {
return owner
}
return model.modules.first
}
private var rows: [BuildSettingRow] {
guard let module = selectedModule else { return [] }
let configName = panel.selectedConfigurationName ?? module.configurationNames.first ?? ""
let projectConfig = module.configurations.first { config in
config.name == configName && config.scope == .project
}
let targetConfig: BuildConfigSummary? = {
guard let targetID = panel.selectedTargetID else { return nil }
return module.configurations.first { config in
if case let .target(id) = config.scope, id == targetID, config.name == configName {
return true
}
return false
}
}()
var keys: Set<String> = []
for k in projectConfig?.rawSettings.keys ?? [:].keys { keys.insert(k) }
for k in targetConfig?.rawSettings.keys ?? [:].keys { keys.insert(k) }
let filter = panel.settingsSearchText.lowercased()
let filteredKeys = filter.isEmpty
? Array(keys)
: keys.filter { $0.lowercased().contains(filter) }
return filteredKeys.sorted().compactMap { key in
let projectValue = projectConfig?.rawSettings[key]
let targetValue = targetConfig?.rawSettings[key]
if panel.settingsCustomizedOnly && targetValue == nil && projectValue == nil {
return nil
}
let resolved = targetValue ?? projectValue ?? ""
let winner: BuildSettingRow.Winner = {
if targetValue != nil { return .target }
if projectValue != nil { return .project }
return .none
}()
return BuildSettingRow(
key: key,
resolvedValue: resolved,
targetValue: targetValue,
projectValue: projectValue,
winner: winner
)
}
}
}
private struct BuildSettingRow {
enum Winner { case target, project, none }
let key: String
let resolvedValue: String
let targetValue: String?
let projectValue: String?
let winner: Winner
}
private struct SettingsRow: View {
let row: BuildSettingRow
let settingColumnWidth: CGFloat
let valueColumnWidth: CGFloat
var body: some View {
HStack(spacing: 0) {
Spacer().frame(width: 3)
Text(row.key)
.font(.system(size: 11, weight: .medium, design: .monospaced))
.frame(width: settingColumnWidth, alignment: .leading)
.lineLimit(1)
.truncationMode(.tail)
.foregroundStyle(row.winner == .none ? Color.secondary : Color.primary)
.help(row.key)
valueCell(row.resolvedValue, emphasis: .normal)
.frame(width: valueColumnWidth, alignment: .leading)
valueCell(row.targetValue ?? "—", emphasis: row.winner == .target ? .accent : .dim)
.frame(width: valueColumnWidth, alignment: .leading)
valueCell(row.projectValue ?? "—", emphasis: row.winner == .project ? .normal : .dim)
.frame(width: valueColumnWidth, alignment: .leading)
}
}
private enum CellEmphasis { case accent, normal, dim }
@ViewBuilder
private func valueCell(_ value: String, emphasis: CellEmphasis) -> some View {
let style: Color = {
switch emphasis {
case .accent: return Color.accentColor
case .normal: return Color.primary
case .dim: return Color.secondary
}
}()
Text(value)
.font(.system(size: 11, design: .monospaced))
.lineLimit(1)
.truncationMode(.tail)
.foregroundStyle(value == "—" ? Color.secondary.opacity(0.6) : style)
.help(value)
}
}