Merge pull request #3527 from js-john/master

Wizard UI: Better user experience for iOS Users.
This commit is contained in:
osy
2022-02-06 20:25:12 -08:00
committed by GitHub
17 changed files with 645 additions and 325 deletions
+66
View File
@@ -0,0 +1,66 @@
//
// Copyright © 2021 osy. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
@available(iOS 14, macOS 11, *)
struct InListButtonStyle: ButtonStyle {
fileprivate struct InListButtonView: View {
let configuration: InListButtonStyle.Configuration
@Environment(\.isEnabled) private var isEnabled: Bool
#if os(macOS)
let defaultColor = Color(NSColor.controlColor)
let pressedColor = Color(NSColor.controlAccentColor)
let foregroundColor = Color(NSColor.controlTextColor)
let foregroundDisabledColor = Color(NSColor.disabledControlTextColor)
let foregroundPressedColor = Color(NSColor.selectedControlColor)
#else
let defaultColor = Color(UIColor.secondarySystemBackground)
let pressedColor = Color(UIColor.systemFill)
let foregroundColor = Color(UIColor.label)
let foregroundDisabledColor = Color(UIColor.systemGray)
let foregroundPressedColor = Color(UIColor.secondaryLabel)
#endif
var body: some View {
#if os(macOS)
ZStack {
RoundedRectangle(cornerRadius: 10.0)
.fill(configuration.isPressed ? pressedColor : defaultColor)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.shadow(color: .gray, radius: 1, x: 0, y: 0)
.padding(5)
configuration.label
.foregroundColor(isEnabled ? (configuration.isPressed ? foregroundPressedColor : foregroundColor) : foregroundDisabledColor)
}
#else
HStack {
configuration.label
Spacer()
}
.foregroundColor(isEnabled ? (configuration.isPressed ? foregroundPressedColor : foregroundColor) : foregroundDisabledColor)
.contentShape(Rectangle())
.listRowBackground(configuration.isPressed ? pressedColor : defaultColor)
#endif
}
}
func makeBody(configuration: Configuration) -> some View {
InListButtonView(configuration: configuration)
}
}
+2
View File
@@ -43,6 +43,7 @@ struct NumberTextFieldOld: View {
self.number = self.formatter.number(from: $0) ?? NSNumber(value: 0)
}), onEditingChanged: onEditingChanged)
.keyboardType(.numberPad)
.multilineTextAlignment(.trailing)
}
}
@@ -76,6 +77,7 @@ struct NumberTextFieldNew: View {
focused = false
onEditingChanged(false)
}
.multilineTextAlignment(.trailing)
}
}
#endif
+5 -7
View File
@@ -70,12 +70,10 @@ struct RAMSlider: View {
validateMemorySize(false)
}
} label: {
Text("Memory")
Text("")
}
GeometryReader { geo in
NumberTextField("Size", number: $systemMemory, onEditingChanged: validateMemorySize)
.position(x: 20, y: geo.size.height / 2)
}.frame(width: 50)
NumberTextField("Size", number: $systemMemory, onEditingChanged: validateMemorySize)
.frame(width: 80)
Text("MB")
}
} else {
@@ -85,10 +83,10 @@ struct RAMSlider: View {
validateMemorySize(false)
}
} label: {
Text("Memory")
Text("")
}
NumberTextField("Size", number: $systemMemory, onEditingChanged: validateMemorySize)
.frame(width: 50, height: nil)
.frame(width: 80, height: nil)
Text("MB")
}
}
+22 -13
View File
@@ -21,21 +21,30 @@ struct VMWizardDrivesView: View {
@ObservedObject var wizardState: VMWizardState
var body: some View {
VStack {
Text("Storage")
.font(.largeTitle)
Text("Specify the size of the drive where data will be stored into.")
.padding()
HStack {
NumberTextField("", number: $wizardState.storageSizeGib, onEditingChanged: { _ in
if wizardState.storageSizeGib < 1 {
wizardState.storageSizeGib = 1
}
}).frame(maxWidth: 50)
Text("GB")
#if os(macOS)
Text("Storage")
.font(.largeTitle)
#endif
List {
Section {
HStack {
Text("Specify the size of the drive where data will be stored into.")
Spacer()
NumberTextField("", number: $wizardState.storageSizeGib, onEditingChanged: { _ in
if wizardState.storageSizeGib < 1 {
wizardState.storageSizeGib = 1
}
})
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 50)
Text("GB")
}
} header: {
Text("Size")
}
Spacer()
}
.navigationTitle(Text("Storage"))
}
}
+63 -40
View File
@@ -56,52 +56,75 @@ struct VMWizardHardwareView: View {
}
var body: some View {
VStack {
Text("Hardware")
.font(.largeTitle)
#if os(macOS)
Text("Hardware")
.font(.largeTitle)
#endif
List {
if !wizardState.useVirtualization {
VMConfigStringPicker(selection: $wizardState.systemArchitecture, label: Text("Architecture"), rawValues: UTMQemuConfiguration.supportedArchitectures(), displayValues: UTMQemuConfiguration.supportedArchitecturesPretty())
.onChange(of: wizardState.systemArchitecture) { newValue in
let targets = UTMQemuConfiguration.supportedTargets(forArchitecture: newValue)
let index = UTMQemuConfiguration.defaultTargetIndex(forArchitecture: newValue)
wizardState.systemTarget = targets![index]
}
#if !os(macOS)
Text(wizardState.systemArchitecture ?? " ")
.font(.caption)
#endif
VMConfigStringPicker(selection: $wizardState.systemTarget, label: Text("System"), rawValues: UTMQemuConfiguration.supportedTargets(forArchitecture: wizardState.systemArchitecture), displayValues: UTMQemuConfiguration.supportedTargets(forArchitecturePretty: wizardState.systemArchitecture))
#if !os(macOS)
Text(wizardState.systemTarget ?? " ")
.font(.caption)
#endif
}
RAMSlider(systemMemory: $wizardState.systemMemory) { _ in
if wizardState.systemMemory < minMemory {
wizardState.systemMemory = minMemory
} else if wizardState.systemMemory > maxMemory {
wizardState.systemMemory = maxMemory
Section {
VMConfigStringPicker(selection: $wizardState.systemArchitecture, label: Text(""), rawValues: UTMQemuConfiguration.supportedArchitectures(), displayValues: UTMQemuConfiguration.supportedArchitecturesPretty())
.onChange(of: wizardState.systemArchitecture) { newValue in
let targets = UTMQemuConfiguration.supportedTargets(forArchitecture: newValue)
let index = UTMQemuConfiguration.defaultTargetIndex(forArchitecture: newValue)
wizardState.systemTarget = targets![index]
}
} header: {
Text("Architecture")
}
}
HStack {
Stepper(value: $wizardState.systemCpuCount, in: minCores...maxCores) {
Text("CPU Cores")
Section {
VMConfigStringPicker(selection: $wizardState.systemTarget, label: Text(""), rawValues: UTMQemuConfiguration.supportedTargets(forArchitecture: wizardState.systemArchitecture), displayValues: UTMQemuConfiguration.supportedTargets(forArchitecturePretty: wizardState.systemArchitecture))
} header: {
Text("System")
}
NumberTextField("", number: $wizardState.systemCpuCount, onEditingChanged: { _ in
if wizardState.systemCpuCount < minCores {
wizardState.systemCpuCount = minCores
} else if wizardState.systemCpuCount > maxCores {
wizardState.systemCpuCount = maxCores
}
})
.frame(width: 50)
.multilineTextAlignment(.trailing)
}
Section {
RAMSlider(systemMemory: $wizardState.systemMemory) { _ in
if wizardState.systemMemory < minMemory {
wizardState.systemMemory = minMemory
} else if wizardState.systemMemory > maxMemory {
wizardState.systemMemory = maxMemory
}
}
} header: {
Text("Memory")
}
Section {
HStack {
Stepper(value: $wizardState.systemCpuCount, in: minCores...maxCores) {
Text("CPU Cores")
}
NumberTextField("", number: $wizardState.systemCpuCount, onEditingChanged: { _ in
if wizardState.systemCpuCount < minCores {
wizardState.systemCpuCount = minCores
} else if wizardState.systemCpuCount > maxCores {
wizardState.systemCpuCount = maxCores
}
})
.frame(width: 50)
.multilineTextAlignment(.trailing)
}
} header: {
Text("CPU")
}
if !wizardState.useAppleVirtualization && wizardState.operatingSystem == .Linux {
Toggle("Enable hardware OpenGL acceleration (experimental)", isOn: $wizardState.isGLEnabled)
Section {
Toggle("Enable hardware OpenGL acceleration (experimental)", isOn: $wizardState.isGLEnabled)
} header: {
Text("Hardware OpenGL Acceleration")
}
}
Spacer()
}.onAppear {
}
.navigationTitle(Text("Hardware"))
.textFieldStyle(.roundedBorder)
.onAppear {
if wizardState.systemArchitecture == nil {
wizardState.systemArchitecture = "x86_64"
}
+183 -97
View File
@@ -30,119 +30,205 @@ struct VMWizardOSLinuxView: View {
@State private var selectImage: SelectImage = .kernel
var body: some View {
VStack {
Text("Linux")
.font(.largeTitle)
#if os(macOS)
if wizardState.useVirtualization {
Toggle("Use Apple Virtualization", isOn: $wizardState.useAppleVirtualization)
.help("If set, use Apple's virtualization engine. Otherwise, use QEMU's virtualization engine.")
#if os(macOS)
Text("Linux")
.font(.largeTitle)
#endif
List {
#if os(macOS)
Section {
if wizardState.useVirtualization {
Toggle("Use Apple Virtualization", isOn: $wizardState.useAppleVirtualization)
}
} header: {
Text("Virtualization Engine")
} footer: {
Text("If set, use Apple's virtualization engine. Otherwise, use QEMU's virtualization engine.")
}
#endif
Toggle("Boot from kernel image", isOn: $wizardState.useLinuxKernel)
.help("If set, boot directly from a raw kernel image and initrd. Otherwise, boot from a supported ISO.")
.disabled(wizardState.useAppleVirtualization)
#endif
Section {
Toggle("Boot from kernel image", isOn: $wizardState.useLinuxKernel)
.help("If set, boot directly from a raw kernel image and initrd. Otherwise, boot from a supported ISO.")
.disabled(wizardState.useAppleVirtualization)
if !wizardState.useLinuxKernel {
#if arch(arm64)
Link("Download Ubuntu Server for ARM", destination: URL(string: "https://ubuntu.com/download/server/arm")!)
.buttonStyle(BorderlessButtonStyle())
#else
Link("Download Ubuntu Desktop", destination: URL(string: "https://ubuntu.com/download/desktop")!)
.buttonStyle(BorderlessButtonStyle())
#endif
}
} header: {
Text("Boot Image Type")
}
if wizardState.useLinuxKernel {
ScrollView {
Group {
Text("Linux kernel (required):")
.padding(.top)
Text(wizardState.linuxKernelURL?.lastPathComponent ?? " ")
.font(.caption)
Section {
Text(wizardState.linuxKernelURL?.lastPathComponent ?? "Empty")
.font(.caption)
Button {
selectImage = .kernel
isFileImporterPresented.toggle()
} label: {
Text("Browse")
}
.padding(.leading, 1)
} header: {
Text("Linux kernel (required):")
}
Section {
Text(wizardState.linuxInitialRamdiskURL?.lastPathComponent ?? "Empty")
.font(.caption)
#if os(macOS)
HStack {
Button {
selectImage = .kernel
selectImage = .initialRamdisk
isFileImporterPresented.toggle()
} label: {
Text("Browse")
}
Text("Linux initial ramdisk (optional):")
.padding(.top)
Text(wizardState.linuxInitialRamdiskURL?.lastPathComponent ?? " ")
.font(.caption)
HStack {
Button {
selectImage = .initialRamdisk
isFileImporterPresented.toggle()
} label: {
Text("Browse")
}
Button {
wizardState.linuxInitialRamdiskURL = nil
} label: {
Text("Clear")
}
.disabled(wizardState.isBusy)
.padding(.leading, 1)
Button {
wizardState.linuxInitialRamdiskURL = nil
} label: {
Text("Clear")
}
}.disabled(wizardState.isBusy)
.buttonStyle(BrowseButtonStyle())
Group{
Text("Linux Root FS Image (optional):")
.padding(.top)
Text(wizardState.linuxRootImageURL?.lastPathComponent ?? " ")
.font(.caption)
HStack {
Button {
selectImage = .rootImage
isFileImporterPresented.toggle()
} label: {
Text("Browse")
}
Button {
wizardState.linuxRootImageURL = nil
} label: {
Text("Clear")
}
}
Text("Boot ISO Image (optional):")
.padding(.top)
Text(wizardState.bootImageURL?.lastPathComponent ?? " ")
.font(.caption)
HStack {
Button {
selectImage = .bootImage
isFileImporterPresented.toggle()
} label: {
Text("Browse")
}
Button {
wizardState.bootImageURL = nil
wizardState.isSkipBootImage = true
} label: {
Text("Clear")
}
}
}.disabled(wizardState.isBusy)
.buttonStyle(BrowseButtonStyle())
.padding(.leading, 1)
}
#else
Button {
selectImage = .initialRamdisk
isFileImporterPresented.toggle()
} label: {
Text("Browse")
}
.disabled(wizardState.isBusy)
.padding(.leading, 1)
Button {
wizardState.linuxInitialRamdiskURL = nil
} label: {
Text("Clear")
}
.padding(.leading, 1)
#endif
} header: {
Text("Linux initial ramdisk (optional):")
}
Section {
Text(wizardState.linuxRootImageURL?.lastPathComponent ?? "Empty")
.font(.caption)
#if os(macOS)
HStack {
Button {
selectImage = .rootImage
isFileImporterPresented.toggle()
} label: {
Text("Browse")
}
Button {
wizardState.linuxRootImageURL = nil
} label: {
Text("Clear")
}
}
#else
Button {
selectImage = .rootImage
isFileImporterPresented.toggle()
} label: {
Text("Browse")
}
Button {
wizardState.linuxRootImageURL = nil
} label: {
Text("Clear")
}
#endif
} header: {
Text("Linux Root FS Image (optional):")
}
Section {
Text(wizardState.bootImageURL?.lastPathComponent ?? "Empty")
.font(.caption)
#if os(macOS)
HStack {
Button {
selectImage = .bootImage
isFileImporterPresented.toggle()
} label: {
Text("Browse")
}
.disabled(wizardState.isBusy)
.padding(.leading, 1)
Button {
wizardState.bootImageURL = nil
wizardState.isSkipBootImage = true
} label: {
Text("Clear")
}
.disabled(wizardState.isBusy)
.padding(.leading, 1)
}
#else
Button {
selectImage = .bootImage
isFileImporterPresented.toggle()
} label: {
Text("Browse")
}
.disabled(wizardState.isBusy)
.padding(.leading, 1)
Button {
wizardState.bootImageURL = nil
wizardState.isSkipBootImage = true
} label: {
Text("Clear")
}
.disabled(wizardState.isBusy)
.padding(.leading, 1)
#endif
} header: {
Text("Boot ISO Image (optional):")
}
Section {
TextField("Boot Arguments", text: $wizardState.linuxBootArguments)
} header: {
Text("Boot Arguments")
}
} else {
#if arch(arm64)
Link("Download Ubuntu Server for ARM", destination: URL(string: "https://ubuntu.com/download/server/arm")!)
.buttonStyle(BorderlessButtonStyle())
#else
Link("Download Ubuntu Desktop", destination: URL(string: "https://ubuntu.com/download/desktop")!)
.buttonStyle(BorderlessButtonStyle())
#endif
Text("Boot ISO Image:")
.padding(.top)
Text(wizardState.bootImageURL?.lastPathComponent ?? " ")
.font(.caption)
Button {
selectImage = .bootImage
isFileImporterPresented.toggle()
} label: {
Text("Browse")
}.disabled(wizardState.isBusy)
.buttonStyle(BrowseButtonStyle())
Section {
Text("Boot ISO Image:")
Text(wizardState.bootImageURL?.lastPathComponent ?? "Empty")
.font(.caption)
Button {
selectImage = .bootImage
isFileImporterPresented.toggle()
} label: {
Text("Browse")
}.disabled(wizardState.isBusy)
} header: {
Text("File Imported")
}
}
if wizardState.isBusy {
BigWhiteSpinner()
}
Spacer()
}.fileImporter(isPresented: $isFileImporterPresented, allowedContentTypes: [.data], onCompletion: processImage)
}
.navigationTitle(Text("Linux"))
.fileImporter(isPresented: $isFileImporterPresented, allowedContentTypes: [.data], onCompletion: processImage)
}
private func processImage(_ result: Result<URL, Error>) {
+34 -27
View File
@@ -23,36 +23,43 @@ struct VMWizardOSMacView: View {
@State private var isFileImporterPresented: Bool = false
var body: some View {
VStack {
Text("macOS")
.font(.largeTitle)
Text("To install macOS, you need to download a recovery IPSW. If you do not select an existing IPSW, the latest macOS IPSW will be downloaded from Apple.")
.padding()
#if arch(arm64)
if let selected = wizardState.macRecoveryIpswURL {
Text(selected.lastPathComponent)
.font(.caption)
}
HStack {
Button {
isFileImporterPresented.toggle()
} label: {
Text("Browse")
#if os(macOS)
Text("macOS")
.font(.largeTitle)
#endif
List {
Section {
Text("To install macOS, you need to download a recovery IPSW. If you do not select an existing IPSW, the latest macOS IPSW will be downloaded from Apple.")
.padding()
#if arch(arm64)
if let selected = wizardState.macRecoveryIpswURL {
Text(selected.lastPathComponent)
.font(.caption)
}
Button {
wizardState.macRecoveryIpswURL = nil
wizardState.macPlatform = nil
} label: {
Text("Clear")
HStack {
Button {
isFileImporterPresented.toggle()
} label: {
Text("Browse")
}
Button {
wizardState.macRecoveryIpswURL = nil
wizardState.macPlatform = nil
} label: {
Text("Clear")
}
}.disabled(wizardState.isBusy)
.buttonStyle(BrowseButtonStyle())
#endif
if wizardState.isBusy {
BigWhiteSpinner()
}
}.disabled(wizardState.isBusy)
.buttonStyle(BrowseButtonStyle())
#endif
if wizardState.isBusy {
BigWhiteSpinner()
Spacer()
} header: {
Text("Import IPSW")
}
Spacer()
}.fileImporter(isPresented: $isFileImporterPresented, allowedContentTypes: [.data], onCompletion: processIpsw)
}
.fileImporter(isPresented: $isFileImporterPresented, allowedContentTypes: [.data], onCompletion: processIpsw)
}
private func processIpsw(_ result: Result<URL, Error>) {
+29 -18
View File
@@ -22,27 +22,38 @@ struct VMWizardOSOtherView: View {
@State private var isFileImporterPresented: Bool = false
var body: some View {
VStack {
Text("Boot Image")
.font(.largeTitle)
Toggle("Skip ISO boot (advanced)", isOn: $wizardState.isSkipBootImage)
#if os(macOS)
Text("Other")
.font(.largeTitle)
#endif
List {
if !wizardState.isSkipBootImage {
Text("Boot ISO Image:")
.padding(.top)
Text(wizardState.bootImageURL?.lastPathComponent ?? " ")
.font(.caption)
Button {
isFileImporterPresented.toggle()
} label: {
Text("Browse")
}.disabled(wizardState.isBusy)
.buttonStyle(BrowseButtonStyle())
if wizardState.isBusy {
BigWhiteSpinner()
Section {
Text("Boot ISO Image:")
Text(wizardState.bootImageURL?.lastPathComponent ?? "Empty")
.font(.caption)
Button {
isFileImporterPresented.toggle()
} label: {
Text("Browse")
}
.disabled(wizardState.isBusy)
.padding(.leading, 1)
if wizardState.isBusy {
BigWhiteSpinner()
}
} header: {
Text("File Imported")
}
}
Spacer()
}.fileImporter(isPresented: $isFileImporterPresented, allowedContentTypes: [.data], onCompletion: processImage)
Section {
Toggle("Skip ISO boot", isOn: $wizardState.isSkipBootImage)
} header: {
Text("Advanced")
}
}
.navigationTitle(Text("Other"))
.fileImporter(isPresented: $isFileImporterPresented, allowedContentTypes: [.data], onCompletion: processImage)
}
private func processImage(_ result: Result<URL, Error>) {
+54 -34
View File
@@ -19,44 +19,63 @@ import SwiftUI
@available(iOS 14, macOS 11, *)
struct VMWizardOSView: View {
@ObservedObject var wizardState: VMWizardState
var body: some View {
VStack {
Text("Operating System")
.font(.largeTitle)
#if os(macOS) && arch(arm64)
if #available(macOS 12, *), wizardState.useVirtualization {
#if os(macOS)
Text("Operating System")
.font(.largeTitle)
#endif
List {
Section {
#if os(macOS) && arch(arm64)
if #available(macOS 12, *), wizardState.useVirtualization {
Button {
wizardState.operatingSystem = .macOS
wizardState.useAppleVirtualization = true
wizardState.next()
} label: {
OperatingSystem(imageName: "mac", name: "macOS 12+")
}
}
#endif
Button {
wizardState.operatingSystem = .macOS
wizardState.useAppleVirtualization = true
wizardState.operatingSystem = .Windows
wizardState.useAppleVirtualization = false
wizardState.next()
} label: {
OperatingSystem(imageName: "mac", name: "macOS 12+")
OperatingSystem(imageName: "windows", name: "Windows")
}
Button {
wizardState.operatingSystem = .Linux
wizardState.next()
} label: {
OperatingSystem(imageName: "linux", name: "Linux")
}
} header: {
Text("Preconfigured")
}
#endif
Button {
wizardState.operatingSystem = .Windows
wizardState.useAppleVirtualization = false
wizardState.next()
} label: {
OperatingSystem(imageName: "windows", name: "Windows")
Section {
Button {
wizardState.operatingSystem = .Other
wizardState.useAppleVirtualization = false
wizardState.next()
} label: {
HStack {
Image(systemName: "gearshape")
.resizable()
.frame(width: 30.0, height: 30.0)
.aspectRatio(contentMode: .fit)
Text("Other")
.font(.title)
}
.padding()
}
} header: {
Text("Custom")
}
Button {
wizardState.operatingSystem = .Linux
wizardState.next()
} label: {
OperatingSystem(imageName: "linux", name: "Linux")
}
Button {
wizardState.operatingSystem = .Other
wizardState.useAppleVirtualization = false
wizardState.next()
} label: {
Text("Other")
.font(.title)
}
}.buttonStyle(BigButtonStyle(width: 320, height: 50))
}
.navigationTitle(Text("Operating System"))
.buttonStyle(InListButtonStyle())
}
}
@@ -70,15 +89,15 @@ struct OperatingSystem: View {
return URL(fileURLWithPath: path)
}
#if os(macOS)
#if os(macOS)
private var icon: Image {
Image(nsImage: NSImage(byReferencing: imageURL))
}
#else
#else
private var icon: Image {
Image(uiImage: UIImage(contentsOfURL: imageURL)!)
}
#endif
#endif
var body: some View {
HStack {
@@ -89,6 +108,7 @@ struct OperatingSystem: View {
Text(name)
.font(.title)
}
.padding()
}
}
+56 -33
View File
@@ -23,44 +23,67 @@ struct VMWizardOSWindowsView: View {
@State private var useVhdx: Bool = false
var body: some View {
VStack {
Text("Windows")
.font(.largeTitle)
if useVhdx {
Link("Download Windows 11 for ARM64 Preview VHDX", destination: URL(string: "https://www.microsoft.com/en-us/software-download/windowsinsiderpreviewARM64")!)
Text("Boot VHDX Image:")
.padding(.top)
} else {
Link("Generate Windows Installer ISO", destination: URL(string: "https://uupdump.net/")!)
Text("Boot ISO Image:")
.padding(.top)
#if os(macOS)
Text("Windows")
.font(.largeTitle)
#endif
List {
Section {
Toggle("Import VHDX Image", isOn: $useVhdx)
if useVhdx {
Link("Download Windows 11 for ARM64 Preview VHDX", destination: URL(string: "https://www.microsoft.com/en-us/software-download/windowsinsiderpreviewARM64")!)
} else {
Link("Generate Windows Installer ISO", destination: URL(string: "https://uupdump.net/")!)
}
} header: {
Text("Image File Type")
}
Toggle("Import VHDX Image", isOn: $useVhdx)
Text((useVhdx ? wizardState.windowsBootVhdx?.lastPathComponent : wizardState.bootImageURL?.lastPathComponent) ?? " ")
.font(.caption)
Button {
isFileImporterPresented.toggle()
} label: {
Text("Browse")
}.disabled(wizardState.isBusy)
.buttonStyle(BrowseButtonStyle())
if wizardState.isBusy {
BigWhiteSpinner()
.onAppear {
if wizardState.windowsBootVhdx != nil {
useVhdx = true
} else {
#if arch(arm64)
useVhdx = wizardState.useVirtualization
#endif
}
}
Spacer()
if #available(iOS 15, macOS 12, *) {
Text(try! AttributedString(markdown: "Hint: For the best Windows experience, make sure to download and install the latest [SPICE tools and QEMU drivers](https://mac.getutm.app/support/)."))
Section {
Toggle("UEFI Boot", isOn: $wizardState.systemBootUefi)
} footer: {
Text("Some older systems do not support UEFI boot, such as Windows 7 and below.")
}
}.fileImporter(isPresented: $isFileImporterPresented, allowedContentTypes: [.data], onCompletion: processImage)
.onAppear {
if wizardState.windowsBootVhdx != nil {
useVhdx = true
} else {
#if arch(arm64)
useVhdx = wizardState.useVirtualization
#endif
Section {
if useVhdx {
Text("Boot VHDX Image:")
} else {
Text("Boot ISO Image:")
}
Text((useVhdx ? wizardState.windowsBootVhdx?.lastPathComponent : wizardState.bootImageURL?.lastPathComponent) ?? "Empty")
.font(.caption)
Button {
isFileImporterPresented.toggle()
} label: {
Text("Browse")
}
.disabled(wizardState.isBusy)
.padding(.leading, 1)
if wizardState.isBusy {
BigWhiteSpinner()
}
} header: {
Text("File Imported")
} footer: {
if #available(iOS 15, macOS 12, *) {
Text(try! AttributedString(markdown: "Hint: For the best Windows experience, make sure to download and install the latest [SPICE tools and QEMU drivers](https://mac.getutm.app/support/)."))
}
}
}
.navigationTitle(Text("Windows"))
.fileImporter(isPresented: $isFileImporterPresented, allowedContentTypes: [.data], onCompletion: processImage)
}
private func processImage(_ result: Result<URL, Error>) {
+48 -17
View File
@@ -22,34 +22,65 @@ struct VMWizardSharingView: View {
@State private var isFileImporterPresented: Bool = false
var body: some View {
VStack {
Text("Shared Directory")
.font(.largeTitle)
Text("Optionally select a directory to make accessible inside the VM. Note that support for shared directories varies by the guest operating system and may require additional guest drivers to be installed. See UTM support pages for more details.")
.padding()
Text(wizardState.sharingDirectoryURL?.lastPathComponent ?? " ")
.font(.caption)
HStack {
#if os(macOS)
Text("Shared Directory")
.font(.largeTitle)
#endif
List {
Section {
HStack {
Text("Directory")
Spacer()
Text(wizardState.sharingDirectoryURL?.lastPathComponent ?? "Empty")
.font(.caption)
}
if !wizardState.useAppleVirtualization {
Toggle("Read only share?", isOn: $wizardState.sharingReadOnly)
}
} header: {
Text("Directory Selected")
}
Section {
#if os(macOS)
HStack {
Button {
isFileImporterPresented.toggle()
} label: {
Text("Browse")
}
.disabled(wizardState.isBusy)
Button {
wizardState.sharingDirectoryURL = nil
} label: {
Text("Clear")
}
.disabled(wizardState.isBusy)
}
.padding(.leading, 1)
#else
Button {
isFileImporterPresented.toggle()
} label: {
Text("Browse")
}
.disabled(wizardState.isBusy)
Button {
wizardState.sharingDirectoryURL = nil
} label: {
Text("Clear")
}
}.disabled(wizardState.isBusy)
.buttonStyle(BrowseButtonStyle())
if !wizardState.useAppleVirtualization {
Toggle("Read only share?", isOn: $wizardState.sharingReadOnly)
.disabled(wizardState.isBusy)
#endif
if wizardState.isBusy {
BigWhiteSpinner()
}
} footer: {
Text("Optionally select a directory to make accessible inside the VM. Note that support for shared directories varies by the guest operating system and may require additional guest drivers to be installed. See UTM support pages for more details.")
}
if wizardState.isBusy {
BigWhiteSpinner()
}
Spacer()
}.fileImporter(isPresented: $isFileImporterPresented, allowedContentTypes: [.folder], onCompletion: processDirectory)
}
.navigationTitle(Text("Shared Directory"))
.fileImporter(isPresented: $isFileImporterPresented, allowedContentTypes: [.folder], onCompletion: processDirectory)
}
private func processDirectory(_ result: Result<URL, Error>) {
+62 -29
View File
@@ -32,38 +32,71 @@ struct VMWizardStartView: View {
}
var body: some View {
VStack {
Text("I want to...")
.font(.largeTitle)
Button {
wizardState.useVirtualization = true
wizardState.next()
} label: {
VStack {
Text("Virtualize")
.font(.title)
Text("Faster, but can only run the native CPU architecture.")
.font(.caption)
#if os(macOS)
Text("Start")
.font(.largeTitle)
#endif
List {
Section {
Button {
wizardState.useVirtualization = true
wizardState.next()
} label: {
HStack {
Image(systemName: "hare")
.font(.title)
VStack(alignment: .leading, spacing: 10) {
Text("Virtualize")
.font(.title)
Text("Faster, but can only run the native CPU architecture.")
.font(.caption)
}
Spacer()
}
.padding()
}
}.disabled(!isVirtualizationSupported)
if !isVirtualizationSupported {
Text("Virtualization is not supported on your system.")
.font(.footnote)
}
Button {
wizardState.useVirtualization = false
wizardState.next()
} label: {
VStack {
Text("Emulate")
.font(.title)
Text("Slower, but can run other CPU architectures.")
.font(.caption)
.disabled(!isVirtualizationSupported)
.buttonStyle(InListButtonStyle())
Button {
wizardState.useVirtualization = false
wizardState.next()
} label: {
HStack {
Image(systemName: "tortoise")
.font(.title)
VStack(alignment: .leading, spacing: 10) {
Text("Emulate")
.font(.title)
Text("Slower, but can run other CPU architectures.")
.font(.caption)
}
Spacer()
}
.padding()
}
.buttonStyle(InListButtonStyle())
} header: {
Text("Custom")
} footer: {
if !isVirtualizationSupported {
Text("Virtualization is not supported on your system.")
}
}
Link("Download prebuilt from UTM Gallery...", destination: URL(string: "https://mac.getutm.app/gallery/")!)
.buttonStyle(BorderlessButtonStyle())
}.buttonStyle(BigButtonStyle(width: 320, height: 100))
Section {
Link(destination: URL(string: "https://mac.getutm.app/gallery/")!) {
HStack {
Image(systemName: "arrow.down.doc")
Text("Download prebuilt from UTM Gallery...")
}
}
} header: {
Text("Prebuilt")
}
}
.navigationTitle(Text("Start"))
}
private func processIsTranslated() -> Bool {
+2
View File
@@ -58,6 +58,7 @@ class VMWizardState: ObservableObject {
@Published var nextPageBinding: Binding<VMWizardPage?> = .constant(nil)
@Published var alertMessage: AlertMessage?
@Published var isBusy: Bool = false
@Published var systemBootUefi: Bool = true
@Published var useVirtualization: Bool = false {
didSet {
if !useVirtualization {
@@ -393,6 +394,7 @@ class VMWizardState: ObservableObject {
config.systemCPUCount = NSNumber(value: systemCpuCount)
config.useHypervisor = useVirtualization
config.shareDirectoryReadOnly = sharingReadOnly
config.systemBootUefi = systemBootUefi
if isGLEnabled, let displayCard = config.displayCard {
let newCard = displayCard + "-gl"
let allCards = UTMQemuConfiguration.supportedDisplayCards(forArchitecture: systemArchitecture)!
+3 -1
View File
@@ -78,7 +78,9 @@ struct VMWizardSummaryView: View {
}
}.textFieldStyle(DefaultTextFieldStyle())
#endif
}.onAppear {
}
.navigationTitle(Text("Summary"))
.onAppear {
if wizardState.name == nil {
let os = wizardState.operatingSystem
if os == .Other {
+8 -8
View File
@@ -45,23 +45,23 @@ fileprivate struct WizardWrapper: View {
VStack {
switch page {
case .start:
VMWizardStartView(wizardState: wizardState).padding()
VMWizardStartView(wizardState: wizardState)
case .operatingSystem:
VMWizardOSView(wizardState: wizardState).padding()
VMWizardOSView(wizardState: wizardState)
case .macOSBoot:
EmptyView()
case .linuxBoot:
VMWizardOSLinuxView(wizardState: wizardState).padding()
VMWizardOSLinuxView(wizardState: wizardState)
case .windowsBoot:
VMWizardOSWindowsView(wizardState: wizardState).padding()
VMWizardOSWindowsView(wizardState: wizardState)
case .otherBoot:
VMWizardOSOtherView(wizardState: wizardState).padding()
VMWizardOSOtherView(wizardState: wizardState)
case .hardware:
VMWizardHardwareView(wizardState: wizardState).padding()
VMWizardHardwareView(wizardState: wizardState)
case .drives:
VMWizardDrivesView(wizardState: wizardState).padding()
VMWizardDrivesView(wizardState: wizardState)
case .sharing:
VMWizardSharingView(wizardState: wizardState).padding()
VMWizardSharingView(wizardState: wizardState)
case .summary:
VMWizardSummaryView(wizardState: wizardState)
}
-1
View File
@@ -60,7 +60,6 @@ struct VMWizardView: View {
}
}.padding()
.frame(width: 450, height: 450)
.background(Color(NSColor.windowBackgroundColor))
.toolbar {
ToolbarItem(placement: .automatic) {
Button("Close") {
+8
View File
@@ -29,6 +29,9 @@
2CE8EB0A2572E173000E2EBB /* qapi-visit-block-export.c in Sources */ = {isa = PBXBuildFile; fileRef = 2CE8EB082572E173000E2EBB /* qapi-visit-block-export.c */; };
2CE8EB41257811E8000E2EBB /* UTMQemuConfiguration+Defaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CE8EB40257811E8000E2EBB /* UTMQemuConfiguration+Defaults.m */; };
2CE8EB42257811E8000E2EBB /* UTMQemuConfiguration+Defaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 2CE8EB40257811E8000E2EBB /* UTMQemuConfiguration+Defaults.m */; };
4B224B9D279D4D8100B63CFF /* InListButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B224B9C279D4D8100B63CFF /* InListButtonStyle.swift */; };
4B224B9E279D4D8100B63CFF /* InListButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B224B9C279D4D8100B63CFF /* InListButtonStyle.swift */; };
4B224B9F279D4D8100B63CFF /* InListButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B224B9C279D4D8100B63CFF /* InListButtonStyle.swift */; };
53A0BDD726D79FE40010EDC5 /* SavePanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A0BDD426D79FE40010EDC5 /* SavePanel.swift */; };
83034C0726AB630F006B4BAF /* UTMPendingVMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83034C0626AB630F006B4BAF /* UTMPendingVMView.swift */; };
83034C0826AB630F006B4BAF /* UTMPendingVMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83034C0626AB630F006B4BAF /* UTMPendingVMView.swift */; };
@@ -1547,6 +1550,7 @@
2CE8EB40257811E8000E2EBB /* UTMQemuConfiguration+Defaults.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UTMQemuConfiguration+Defaults.m"; sourceTree = "<group>"; };
423BCE65240F6A80001989AC /* VMConfigSystemArgumentsViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VMConfigSystemArgumentsViewController.m; sourceTree = "<group>"; };
423BCE67240F6A8A001989AC /* VMConfigSystemArgumentsViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VMConfigSystemArgumentsViewController.h; sourceTree = "<group>"; };
4B224B9C279D4D8100B63CFF /* InListButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InListButtonStyle.swift; sourceTree = "<group>"; };
521F3EFA2414F73800130500 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
52459A322440C84E006A58D0 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
525535A1241B8A52003C80FC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = "<group>"; };
@@ -3200,6 +3204,7 @@
children = (
E28394B7240C2191006742E2 /* HTerm */,
CEF0304C26A2AFBE00667B63 /* BigButtonStyle.swift */,
4B224B9C279D4D8100B63CFF /* InListButtonStyle.swift */,
CEF0304D26A2AFBE00667B63 /* BigWhiteSpinner.swift */,
CE7D972B24B2B17D0080CB69 /* BusyOverlay.swift */,
CE772AAB25C8B0F600E4E379 /* ContentView.swift */,
@@ -3818,6 +3823,7 @@
CE2D92B824AD46670059923A /* qapi-events-trace.c in Sources */,
2C6D9E142571AFE5003298E6 /* UTMQcow2.c in Sources */,
2CE8EAFE2572E14D000E2EBB /* qapi-types-block-export.c in Sources */,
4B224B9D279D4D8100B63CFF /* InListButtonStyle.swift in Sources */,
CE2D92B924AD46670059923A /* qapi-events-qdev.c in Sources */,
CEBE02642588494100B9BCA8 /* CSSession+Sharing.m in Sources */,
2C33B3A92566C9B100A954A6 /* VMContextMenuModifier.swift in Sources */,
@@ -4174,6 +4180,7 @@
CE0B6D6424AD584D00FE012D /* qapi-visit-error.c in Sources */,
CE0B6D5A24AD584C00FE012D /* qapi-events-trace.c in Sources */,
CE0B6CFD24AD569A00FE012D /* gst_ios_init.m in Sources */,
4B224B9F279D4D8100B63CFF /* InListButtonStyle.swift in Sources */,
CEF83EBB24F9ABEA00557D15 /* UTMQemuManager+BlockDevices.m in Sources */,
CEECE13C25E47D9500A2AAB8 /* AppDelegate.swift in Sources */,
CE0B6CF724AD568400FE012D /* UTMQemuConfiguration+Display.m in Sources */,
@@ -4496,6 +4503,7 @@
CEA45EF6263519B5002FA97D /* UTMQemuConfiguration.m in Sources */,
CEA45EF7263519B5002FA97D /* UTMQemuVirtualMachine+SPICE.m in Sources */,
CEA45EF8263519B5002FA97D /* VMDetailsView.swift in Sources */,
4B224B9E279D4D8100B63CFF /* InListButtonStyle.swift in Sources */,
835AA7B226AB7C85007A0411 /* UTMPendingVirtualMachine.swift in Sources */,
CEA45EF9263519B5002FA97D /* VMDisplayMetalViewController.m in Sources */,
CEA45EFA263519B5002FA97D /* qapi-commands-misc.c in Sources */,