Sfoglia il codice sorgente

Merge pull request #3527 from js-john/master

Wizard UI: Better user experience for iOS Users.
osy 3 anni fa
parent
commit
7ba8c91686

+ 66 - 0
Platform/Shared/InListButtonStyle.swift

@@ -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 - 0
Platform/Shared/NumberTextField.swift

@@ -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
Platform/Shared/RAMSlider.swift

@@ -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
Platform/Shared/VMWizardDrivesView.swift

@@ -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"))
     }
 }
 

+ 61 - 38
Platform/Shared/VMWizardHardwareView.swift

@@ -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")
                 }
+                
+                Section {
+                    VMConfigStringPicker(selection: $wizardState.systemTarget, label: Text(""), rawValues: UTMQemuConfiguration.supportedTargets(forArchitecture: wizardState.systemArchitecture), displayValues: UTMQemuConfiguration.supportedTargets(forArchitecturePretty: wizardState.systemArchitecture))
+                } header: {
+                    Text("System")
+                }
+
             }
-            HStack {
-                Stepper(value: $wizardState.systemCpuCount, in: minCores...maxCores) {
-                    Text("CPU Cores")
+            Section {
+                RAMSlider(systemMemory: $wizardState.systemMemory) { _ in
+                    if wizardState.systemMemory < minMemory {
+                        wizardState.systemMemory = minMemory
+                    } else if wizardState.systemMemory > maxMemory {
+                        wizardState.systemMemory = maxMemory
+                    }
                 }
-                NumberTextField("", number: $wizardState.systemCpuCount, onEditingChanged: { _ in
-                    if wizardState.systemCpuCount < minCores {
-                        wizardState.systemCpuCount = minCores
-                    } else if wizardState.systemCpuCount > maxCores {
-                        wizardState.systemCpuCount = maxCores
+            } header: {
+                Text("Memory")
+            }
+            
+            Section {
+                HStack {
+                    Stepper(value: $wizardState.systemCpuCount, in: minCores...maxCores) {
+                        Text("CPU Cores")
                     }
-                })
-                    .frame(width: 50)
-                    .multilineTextAlignment(.trailing)
+                    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"
             }

+ 181 - 95
Platform/Shared/VMWizardOSLinuxView.swift

@@ -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
+            
+            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")
             }
-            #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)
+            
             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")
-                            }
+                        .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")
                         }
-                        
-                        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")
-                            }
+                    }
+#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)
-                    .buttonStyle(BrowseButtonStyle())
+                        .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
Platform/Shared/VMWizardOSMacView.swift

@@ -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
Platform/Shared/VMWizardOSOtherView.swift

@@ -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
Platform/Shared/VMWizardOSView.swift

@@ -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")
-            }
-            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)
+            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")
             }
-        }.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
Platform/Shared/VMWizardOSWindowsView.swift

@@ -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
Platform/Shared/VMWizardSharingView.swift

@@ -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)
-            }
-            if wizardState.isBusy {
-                BigWhiteSpinner()
+                .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.")
             }
-            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>) {

+ 61 - 28
Platform/Shared/VMWizardStartView.swift

@@ -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)
+                .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.")
                 }
-            }.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)
+            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")
             }
-            Link("Download prebuilt from UTM Gallery...", destination: URL(string: "https://mac.getutm.app/gallery/")!)
-                .buttonStyle(BorderlessButtonStyle())
-        }.buttonStyle(BigButtonStyle(width: 320, height: 100))
+
+        }
+        .navigationTitle(Text("Start"))
     }
     
     private func processIsTranslated() -> Bool {

+ 2 - 0
Platform/Shared/VMWizardState.swift

@@ -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
Platform/Shared/VMWizardSummaryView.swift

@@ -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
Platform/iOS/VMWizardView.swift

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

+ 0 - 1
Platform/macOS/VMWizardView.swift

@@ -60,7 +60,6 @@ struct VMWizardView: View {
             }
         }.padding()
             .frame(width: 450, height: 450)
-            .background(Color(NSColor.windowBackgroundColor))
             .toolbar {
                 ToolbarItem(placement: .automatic) {
                     Button("Close") {

+ 8 - 0
UTM.xcodeproj/project.pbxproj

@@ -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 */,