forked from Provenance-Emu/Provenance
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Joseph Mattiello <git@joemattiello.com>
- Loading branch information
Showing
5 changed files
with
374 additions
and
23 deletions.
There are no files selected for viewing
84 changes: 84 additions & 0 deletions
84
PVUI/Sources/PVSwiftUI/Settings/ViewModels/CoreOptionsViewModel.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import Foundation | ||
import RealmSwift | ||
import PVCoreBridge | ||
import PVLibrary | ||
import Combine | ||
|
||
@MainActor | ||
final class CoreOptionsViewModel: ObservableObject { | ||
/// Published list of available cores that implement CoreOptional | ||
@Published private(set) var availableCores: [PVCore] = [] | ||
|
||
/// The currently selected core for options display | ||
@Published var selectedCore: (core: PVCore, coreClass: CoreOptional.Type)? | ||
|
||
private var cancellables = Set<AnyCancellable>() | ||
|
||
init() { | ||
loadAvailableCores() | ||
} | ||
|
||
/// Load all cores that implement CoreOptional | ||
private func loadAvailableCores() { | ||
let realm = try! Realm() | ||
availableCores = realm.objects(PVCore.self) | ||
.sorted(byKeyPath: "projectName") | ||
.filter { pvcore in | ||
guard let coreClass = NSClassFromString(pvcore.principleClass) as? CoreOptional.Type else { | ||
return false | ||
} | ||
return true | ||
} | ||
} | ||
|
||
/// Select a core to display its options | ||
func selectCore(_ core: PVCore) { | ||
guard let coreClass = NSClassFromString(core.principleClass) as? CoreOptional.Type else { | ||
return | ||
} | ||
|
||
selectedCore = (core: core, coreClass: coreClass) | ||
} | ||
|
||
/// Get the current value for an option | ||
func currentValue(for option: CoreOption) -> Any? { | ||
guard let coreClass = selectedCore?.coreClass else { return nil } | ||
|
||
switch option { | ||
case .bool(_, let defaultValue): | ||
return coreClass.storedValueForOption(Bool.self, option.key) ?? defaultValue | ||
case .string(_, let defaultValue): | ||
return coreClass.storedValueForOption(String.self, option.key) ?? defaultValue | ||
case .enumeration(_, _, let defaultValue): | ||
return coreClass.storedValueForOption(Int.self, option.key) ?? defaultValue | ||
case .range(_, _, let defaultValue): | ||
return coreClass.storedValueForOption(Int.self, option.key) ?? defaultValue | ||
case .rangef(_, _, let defaultValue): | ||
return coreClass.storedValueForOption(Float.self, option.key) ?? defaultValue | ||
case .multi(_, let values): | ||
return coreClass.storedValueForOption(String.self, option.key) ?? values.first?.title | ||
case .group(_, _): | ||
return nil | ||
@unknown default: | ||
return nil | ||
} | ||
} | ||
|
||
/// Set a new value for an option | ||
func setValue(_ value: Any, for option: CoreOption) { | ||
guard let coreClass = selectedCore?.coreClass else { return } | ||
|
||
switch value { | ||
case let boolValue as Bool: | ||
coreClass.setValue(boolValue, forOption: option) | ||
case let stringValue as String: | ||
coreClass.setValue(stringValue, forOption: option) | ||
case let intValue as Int: | ||
coreClass.setValue(intValue, forOption: option) | ||
case let floatValue as Float: | ||
coreClass.setValue(floatValue, forOption: option) | ||
default: | ||
break | ||
} | ||
} | ||
} |
220 changes: 220 additions & 0 deletions
220
PVUI/Sources/PVSwiftUI/Settings/Views/CoreOptionsDetailView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
import SwiftUI | ||
import PVCoreBridge | ||
import PVLibrary | ||
|
||
/// View that displays and allows editing of core options for a specific core | ||
struct CoreOptionsDetailView: View { | ||
let coreClass: CoreOptional.Type | ||
let title: String | ||
|
||
private var groupedOptions: [(title: String, options: [CoreOption])] { | ||
var rootOptions = [CoreOption]() | ||
var groups = [(title: String, options: [CoreOption])]() | ||
|
||
// Process options into groups | ||
coreClass.options.forEach { option in | ||
switch option { | ||
case let .group(display, subOptions): | ||
groups.append((title: display.title, options: subOptions)) | ||
default: | ||
rootOptions.append(option) | ||
} | ||
} | ||
|
||
// Add root options as first group if any exist | ||
if !rootOptions.isEmpty { | ||
groups.insert((title: "General", options: rootOptions), at: 0) | ||
} | ||
|
||
return groups | ||
} | ||
|
||
var body: some View { | ||
List { | ||
ForEach(groupedOptions.indices, id: \.self) { sectionIndex in | ||
let group = groupedOptions[sectionIndex] | ||
Section(header: Text(group.title)) { | ||
ForEach(group.options.indices, id: \.self) { optionIndex in | ||
let option = group.options[optionIndex] | ||
optionView(for: option) | ||
} | ||
} | ||
} | ||
} | ||
.navigationTitle(title) | ||
} | ||
|
||
@ViewBuilder | ||
private func optionView(for option: CoreOption) -> some View { | ||
switch option { | ||
case let .bool(display, defaultValue): | ||
Toggle(isOn: Binding( | ||
get: { coreClass.storedValueForOption(Bool.self, option.key) ?? defaultValue }, | ||
set: { coreClass.setValue($0, forOption: option) } | ||
)) { | ||
VStack(alignment: .leading) { | ||
Text(display.title) | ||
if let description = display.description { | ||
Text(description) | ||
.font(.caption) | ||
.foregroundColor(.secondary) | ||
} | ||
} | ||
} | ||
|
||
case let .enumeration(display, values, defaultValue): | ||
let selection = Binding( | ||
get: { coreClass.storedValueForOption(Int.self, option.key) ?? defaultValue }, | ||
set: { coreClass.setValue($0, forOption: option) } | ||
) | ||
|
||
NavigationLink { | ||
List { | ||
ForEach(values, id: \.value) { value in | ||
Button { | ||
selection.wrappedValue = value.value | ||
} label: { | ||
HStack { | ||
VStack(alignment: .leading) { | ||
Text(value.title) | ||
if let description = value.description { | ||
Text(description) | ||
.font(.caption) | ||
.foregroundColor(.secondary) | ||
} | ||
} | ||
Spacer() | ||
if value.value == selection.wrappedValue { | ||
Image(systemName: "checkmark") | ||
} | ||
} | ||
} | ||
} | ||
} | ||
.navigationTitle(display.title) | ||
} label: { | ||
VStack(alignment: .leading) { | ||
Text(display.title) | ||
if let description = display.description { | ||
Text(description) | ||
.font(.caption) | ||
.foregroundColor(.secondary) | ||
} | ||
Text(values.first { $0.value == selection.wrappedValue }?.title ?? "") | ||
.foregroundColor(.secondary) | ||
} | ||
} | ||
|
||
case let .range(display, range, defaultValue): | ||
VStack(alignment: .leading) { | ||
Text(display.title) | ||
if let description = display.description { | ||
Text(description) | ||
.font(.caption) | ||
.foregroundColor(.secondary) | ||
} | ||
Slider( | ||
value: Binding( | ||
get: { Double(coreClass.storedValueForOption(Int.self, option.key) ?? defaultValue) }, | ||
set: { coreClass.setValue(Int($0), forOption: option) } | ||
), | ||
in: Double(range.min)...Double(range.max), | ||
step: 1 | ||
) { | ||
Text(display.title) | ||
} minimumValueLabel: { | ||
Text("\(range.min)") | ||
} maximumValueLabel: { | ||
Text("\(range.max)") | ||
} | ||
} | ||
|
||
case let .rangef(display, range, defaultValue): | ||
VStack(alignment: .leading) { | ||
Text(display.title) | ||
if let description = display.description { | ||
Text(description) | ||
.font(.caption) | ||
.foregroundColor(.secondary) | ||
} | ||
Slider( | ||
value: Binding( | ||
get: { Double(coreClass.storedValueForOption(Float.self, option.key) ?? defaultValue) }, | ||
set: { coreClass.setValue(Float($0), forOption: option) } | ||
), | ||
in: Double(range.min)...Double(range.max), | ||
step: 0.1 | ||
) { | ||
Text(display.title) | ||
} minimumValueLabel: { | ||
Text(String(format: "%.1f", range.min)) | ||
} maximumValueLabel: { | ||
Text(String(format: "%.1f", range.max)) | ||
} | ||
} | ||
|
||
case let .multi(display, values): | ||
let selection = Binding( | ||
get: { coreClass.storedValueForOption(String.self, option.key) ?? values.first?.title ?? "" }, | ||
set: { coreClass.setValue($0, forOption: option) } | ||
) | ||
|
||
NavigationLink { | ||
List { | ||
ForEach(values, id: \.title) { value in | ||
Button { | ||
selection.wrappedValue = value.title | ||
} label: { | ||
HStack { | ||
VStack(alignment: .leading) { | ||
Text(value.title) | ||
if let description = value.description { | ||
Text(description) | ||
.font(.caption) | ||
.foregroundColor(.secondary) | ||
} | ||
} | ||
Spacer() | ||
if value.title == selection.wrappedValue { | ||
Image(systemName: "checkmark") | ||
} | ||
} | ||
} | ||
} | ||
} | ||
.navigationTitle(display.title) | ||
} label: { | ||
VStack(alignment: .leading) { | ||
Text(display.title) | ||
if let description = display.description { | ||
Text(description) | ||
.font(.caption) | ||
.foregroundColor(.secondary) | ||
} | ||
Text(selection.wrappedValue) | ||
.foregroundColor(.secondary) | ||
} | ||
} | ||
|
||
case let .string(display, defaultValue): | ||
let text = Binding( | ||
get: { coreClass.storedValueForOption(String.self, option.key) ?? defaultValue }, | ||
set: { coreClass.setValue($0, forOption: option) } | ||
) | ||
|
||
VStack(alignment: .leading) { | ||
Text(display.title) | ||
if let description = display.description { | ||
Text(description) | ||
.font(.caption) | ||
.foregroundColor(.secondary) | ||
} | ||
TextField("Value", text: text) | ||
.textFieldStyle(RoundedBorderTextFieldStyle()) | ||
} | ||
|
||
case .group(_, _): | ||
EmptyView() // Groups are handled at the section level | ||
} | ||
} | ||
} |
63 changes: 63 additions & 0 deletions
63
PVUI/Sources/PVSwiftUI/Settings/Views/CoreOptionsListView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import SwiftUI | ||
import PVCoreBridge | ||
import PVLibrary | ||
|
||
/// A simple struct to hold core information for the list | ||
private struct CoreListItem: Identifiable { | ||
let id: String | ||
let name: String | ||
let coreClass: CoreOptional.Type | ||
|
||
init(core: PVCore) { | ||
self.id = core.identifier | ||
self.name = core.projectName | ||
self.coreClass = NSClassFromString(core.principleClass) as! CoreOptional.Type | ||
} | ||
} | ||
|
||
/// View that lists all cores that implement CoreOptional | ||
struct CoreOptionsListView: View { | ||
@StateObject private var viewModel = CoreOptionsViewModel() | ||
|
||
private var coreItems: [CoreListItem] { | ||
viewModel.availableCores.compactMap { core in | ||
guard let _ = NSClassFromString(core.principleClass) as? CoreOptional.Type else { | ||
return nil | ||
} | ||
return CoreListItem(core: core) | ||
} | ||
} | ||
|
||
var body: some View { | ||
ScrollView { | ||
LazyVStack(spacing: 8) { | ||
ForEach(coreItems) { item in | ||
CoreListItemView(item: item) | ||
} | ||
} | ||
.padding(.vertical) | ||
} | ||
.navigationTitle("Core Options") | ||
} | ||
} | ||
|
||
/// View for a single core item in the list | ||
private struct CoreListItemView: View { | ||
let item: CoreListItem | ||
|
||
var body: some View { | ||
NavigationLink { | ||
CoreOptionsDetailView(coreClass: item.coreClass, title: item.name) | ||
} label: { | ||
VStack(alignment: .leading) { | ||
Text(item.name) | ||
.font(.headline) | ||
} | ||
.frame(maxWidth: .infinity, alignment: .leading) | ||
.padding() | ||
.background(Color(.secondarySystemGroupedBackground)) | ||
.cornerRadius(10) | ||
} | ||
.padding(.horizontal) | ||
} | ||
} |
Oops, something went wrong.