Skip to content

Commit

Permalink
Merge branch 'feature/coreOptionsSwiftUI' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeMatt committed Dec 5, 2024
2 parents 02c3269 + 8a4eded commit 8670d72
Show file tree
Hide file tree
Showing 7 changed files with 524 additions and 46 deletions.
31 changes: 0 additions & 31 deletions PVUI/Sources/PVSwiftUI/Settings/Views/CoreOptionsView.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,38 @@ import Foundation
import PVSupport
import PVEmulatorCore
import PVCoreBridge
import SwiftUI

extension PVEmulatorViewController {
func showCoreOptions() {
var coreClass = type(of: core)
guard let coreClass = type(of: core) as? CoreOptional.Type else { return }

coreClass.coreClassName = core.coreIdentifier ?? ""
// Create the SwiftUI view
let coreOptionsView = CoreOptionsDetailView(
coreClass: coreClass,
title: "Core Options"
)

let optionsVC = CoreOptionsViewController(withCore: coreClass as! any CoreOptional.Type) // Assuming this initializer expects a PVEmulatorCore.Type
optionsVC.title = "Core Options"
let nav = UINavigationController(rootViewController: optionsVC)
// Create a hosting controller
let hostingController = UIHostingController(rootView: coreOptionsView)
let nav = UINavigationController(rootViewController: hostingController)

#if os(iOS)
optionsVC.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissNav))
// Add done button for iOS
#if os(iOS)
hostingController.navigationItem.rightBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .done,
target: self,
action: #selector(dismissNav)
)
// disable iOS 13 swipe to dismiss...
nav.isModalInPresentation = true
present(nav, animated: true, completion: nil)
#else
present(nav, animated: true)
#else
// Add menu button gesture for tvOS
let tap = UITapGestureRecognizer(target: self, action: #selector(self.dismissNav))
tap.allowedPressTypes = [.menu]
optionsVC.view.addGestureRecognizer(tap)
present(TVFullscreenController(rootViewController: nav), animated: true, completion: nil)
#endif
hostingController.view.addGestureRecognizer(tap)
present(TVFullscreenController(rootViewController: nav), animated: true)
#endif
}
}
292 changes: 292 additions & 0 deletions PVUI/Sources/PVUIBase/SwiftUI/CoreOptions/CoreOptionsDetailView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
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

/// State to track current values of options
@State private var optionValues: [String: Any] = [:]

private struct IdentifiableOption: Identifiable {
let id = UUID()
let option: CoreOption
}

private struct OptionGroup: Identifiable {
let id = UUID()
let title: String
let options: [IdentifiableOption]

init(title: String, options: [CoreOption]) {
self.title = title
self.options = options.map { IdentifiableOption(option: $0) }
}
}

private var groupedOptions: [OptionGroup] {
var rootOptions = [CoreOption]()
var groups = [OptionGroup]()

// Process options into groups
coreClass.options.forEach { option in
switch option {
case let .group(display, subOptions):
groups.append(OptionGroup(title: display.title, options: subOptions))
default:
rootOptions.append(option)
}
}

// Add root options as first group if any exist
if !rootOptions.isEmpty {
groups.insert(OptionGroup(title: "General", options: rootOptions), at: 0)
}

return groups
}

var body: some View {
Form {
ForEach(groupedOptions) { group in
SwiftUI.Section {
ForEach(group.options) { identifiableOption in
optionView(for: identifiableOption.option)
}
} header: {
Text(group.title)
}
}
}
.navigationTitle(title)
.onAppear {
// Load initial values
loadOptionValues()
}
}

private func loadOptionValues() {
for group in groupedOptions {
for identifiableOption in group.options {
let value = getCurrentValue(for: identifiableOption.option)
if let value = value {
optionValues[identifiableOption.option.key] = value
}
}
}
}

private func getCurrentValue(for option: CoreOption) -> Any? {
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
}
}

private func setValue(_ value: Any, for option: CoreOption) {
optionValues[option.key] = value

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
}
}

@ViewBuilder
private func optionView(for option: CoreOption) -> some View {
switch option {
case let .bool(display, defaultValue):
Toggle(isOn: Binding(
get: { optionValues[option.key] as? Bool ?? defaultValue },
set: { setValue($0, for: 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: { optionValues[option.key] as? Int ?? defaultValue },
set: { setValue($0, for: 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(optionValues[option.key] as? Int ?? defaultValue) },
set: { setValue(Int($0), for: 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(optionValues[option.key] as? Float ?? defaultValue) },
set: { setValue(Float($0), for: 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: { optionValues[option.key] as? String ?? values.first?.title ?? "" },
set: { setValue($0, for: 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: { optionValues[option.key] as? String ?? defaultValue },
set: { setValue($0, for: 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
}
}
}
Loading

0 comments on commit 8670d72

Please sign in to comment.