Ver Fonte

settings(qemu): new table view for QEMU arguments

Available on macOS 12+
osy há 2 anos atrás
pai
commit
c9afc0b8da

+ 37 - 33
Platform/Shared/VMConfigQEMUView.swift

@@ -48,14 +48,6 @@ struct VMConfigQEMUView: View {
         }
     }
     
-    private var isMontereyOrHigher: Bool {
-        if #available(macOS 12, *) {
-            return true
-        } else {
-            return false
-        }
-    }
-    
     var body: some View {
         VStack {
             Form {
@@ -102,37 +94,49 @@ struct VMConfigQEMUView: View {
                 DetailedSection("QEMU Machine Properties", description: "This is appended to the -machine argument.") {
                     DefaultTextField("", text: $config.machinePropertyOverride.bound, prompt: "Default")
                 }
-                Section(header: Text("QEMU Arguments")) {
-                    let fixedArgs = fetchFixedArguments()
-                    Button("Export QEMU Command") {
-                        showExportArgs.toggle()
-                    }.modifier(VMShareItemModifier(isPresented: $showExportArgs, shareItem: exportArgs(fixedArgs)))
-                    #if os(macOS)
-                    // SwiftUI bug: on macOS 11, the ForEach crashes during save
-                    if isMontereyOrHigher || !data.busy {
-                        VStack {
-                            ForEach(fixedArgs) { arg in
-                                TextField("", text: .constant(arg.string))
-                            }.disabled(true)
-                            CustomArguments(config: $config)
-                            NewArgumentTextField(config: $config)
-                        }
-                    }
-                    #else
-                    List {
-                        ForEach(fixedArgs) { arg in
-                            Text(arg.string)
-                        }.foregroundColor(.secondary)
-                        CustomArguments(config: $config)
-                        NewArgumentTextField(config: $config)
-                    }
-                    #endif
+                #if os(macOS)
+                // macOS 12+ uses the new VMConfigQEMUArgumentsView
+                if #unavailable(macOS 12) {
+                    additionalArguments
                 }
+                #else
+                additionalArguments
+                #endif
             }.navigationBarItems(trailing: EditButton())
             .disableAutocorrection(true)
         }
     }
     
+    @ViewBuilder
+    var additionalArguments: some View {
+        Section(header: Text("QEMU Arguments")) {
+            let fixedArgs = fetchFixedArguments()
+            Button("Export QEMU Command…") {
+                showExportArgs.toggle()
+            }.modifier(VMShareItemModifier(isPresented: $showExportArgs, shareItem: exportArgs(fixedArgs)))
+            #if os(macOS)
+            // SwiftUI bug: on macOS 11, the ForEach crashes during save
+            if !data.busy {
+                VStack {
+                    ForEach(fixedArgs) { arg in
+                        TextField("", text: .constant(arg.string))
+                    }.disabled(true)
+                    CustomArguments(config: $config)
+                    NewArgumentTextField(config: $config)
+                }
+            }
+            #else
+            List {
+                ForEach(fixedArgs) { arg in
+                    Text(arg.string)
+                }.foregroundColor(.secondary)
+                CustomArguments(config: $config)
+                NewArgumentTextField(config: $config)
+            }
+            #endif
+        }
+    }
+    
     private func exportDebugLog() -> VMShareItemModifier.ShareItem? {
         guard let srcLogPath = config.debugLogURL else {
             return nil

+ 1 - 1
Platform/es-419.lproj/Localizable.strings

@@ -566,7 +566,7 @@
 "Export Debug Log" = "Exportar registro de depuración";
 
 /* No comment provided by engineer. */
-"Export QEMU Command" = "Exportar comando de QEMU";
+"Export QEMU Command" = "Exportar comando de QEMU";
 
 /* Word for decompressing a compressed folder */
 "Extracting…" = "Extrayendo…";

+ 1 - 1
Platform/fi.lproj/Localizable.strings

@@ -374,7 +374,7 @@
 "Export Debug Log" = "Vie virheenkorjausloki";
 
 /* No comment provided by engineer. */
-"Export QEMU Command" = "Export QEMU Command";
+"Export QEMU Command" = "Export QEMU Command";
 
 /* UTMVirtualMachine+Drives */
 "Failed create bookmark." = "Kirjanmerkin luominen epäonnistui.";

+ 1 - 1
Platform/fr.lproj/Localizable.strings

@@ -582,7 +582,7 @@
 "QEMU Machine Properties" = "Propriétés de la machine QEMU";
 "This is appended to the -machine argument." = "Ceci est ajouté à l'argument -machine.";
 "QEMU Arguments" = "Arguments pour QEMU";
-"Export QEMU Command" = "Exporter la commande QEMU";
+"Export QEMU Command" = "Exporter la commande QEMU";
 "(Delete)" = "(Supprimer)";
 
 // VMConfigSerialView.swift

+ 1 - 1
Platform/ja.lproj/Localizable.strings

@@ -596,7 +596,7 @@
 "QEMU Machine Properties" = "QEMUマシンプロパティ";
 "This is appended to the -machine argument." = "-machine引数に追加されます。";
 "QEMU Arguments" = "QEMU引数";
-"Export QEMU Command" = "QEMUコマンドを書き出す";
+"Export QEMU Command" = "QEMUコマンドを書き出す";
 "(Delete)" = "(削除)";
 
 // VMConfigSerialView.swift

+ 147 - 0
Platform/macOS/VMConfigQEMUArgumentsView.swift

@@ -0,0 +1,147 @@
+//
+// Copyright © 2023 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(macOS 12, *)
+struct VMConfigQEMUArgumentsView: View {
+    @Binding var config: UTMQemuConfigurationQEMU
+    let architecture: QEMUArchitecture
+    let fixedArguments: [QEMUArgument]
+    
+    private let fixedUuids: Set<UUID>
+    @State private var selected: Set<UUID>
+    @State private var selectedArgument = QEMUArgument("")
+    @FocusState private var focused: UUID?
+    @State private var showExportArgs: Bool = false
+    
+    private var customUuids: Set<UUID> {
+        Set(config.additionalArguments.map({ $0.id }))
+    }
+    
+    private var exportShareItem: VMShareItemModifier.ShareItem {
+        var argString = "qemu-system-\(architecture.rawValue)"
+        for arg in fixedArguments {
+            if arg.string.contains(" ") {
+                argString += " \"\(arg.string)\""
+            } else {
+                argString += " \(arg.string)"
+            }
+        }
+        for arg in config.additionalArguments {
+            argString += " \(arg.string)"
+        }
+        return .qemuCommand(argString)
+    }
+    
+    init(config: Binding<UTMQemuConfigurationQEMU>, architecture: QEMUArchitecture, fixedArguments: [QEMUArgument]) {
+        self._config = config
+        self.architecture = architecture
+        self.fixedArguments = fixedArguments
+        self.fixedUuids = Set(fixedArguments.map({ $0.id }))
+        self._selected = State<Set<UUID>>(initialValue: .init())
+    }
+    
+    var body: some View {
+        VStack {
+            Table(of: QEMUArgument.self, selection: $selected) {
+                TableColumn("Arguments") { arg in
+                    let customSelected = selected.intersection(customUuids)
+                    if fixedUuids.contains(arg.id) || customSelected.count > 1 || !customSelected.contains(arg.id) {
+                        Text(arg.string)
+                            .foregroundColor(fixedUuids.contains(arg.id) ? .secondary : .primary)
+                            .textSelection(.enabled)
+                    } else {
+                        TextField("", text: $selectedArgument.string)
+                            .focused($focused, equals: arg.id)
+                            .onSubmit(of: .text) {
+                                if let index = config.additionalArguments.firstIndex(of: arg) {
+                                    config.additionalArguments[index] = selectedArgument
+                                }
+                            }
+                    }
+                }
+            } rows: {
+                ForEach(fixedArguments) { arg in
+                    TableRow(arg)
+                }
+                ForEach(config.additionalArguments) { arg in
+                    TableRow(arg)
+                }
+            }.onChange(of: selected) { newValue in
+                // save changes to last selected argument
+                if let index = config.additionalArguments.firstIndex(where: { $0.id == selectedArgument.id }) {
+                    config.additionalArguments[index] = selectedArgument
+                    selectedArgument = .init("")
+                }
+                // get new selected argument
+                if let selectedId = selected.intersection(customUuids).first {
+                    if let arg = config.additionalArguments.first(where: { $0.id == selectedId }) {
+                        selectedArgument = arg
+                    }
+                }
+            }
+            Spacer()
+            HStack {
+                Button {
+                    showExportArgs.toggle()
+                } label: {
+                    Text("Export QEMU Command…")
+                }.help("Export all arguments as a text file. This is only for debugging purposes as UTM's built-in QEMU differs from upstream QEMU in supported arguments.")
+                Spacer()
+                let customSelected = selected.intersection(customUuids)
+                if !customSelected.isEmpty {
+                    if customSelected.count > 1 || customSelected.first != config.additionalArguments.first?.id {
+                        Button {
+                            for i in 1..<config.additionalArguments.count {
+                                if customSelected.contains(config.additionalArguments[i].id) {
+                                    config.additionalArguments.move(fromOffsets: .init(integer: i), toOffset: i - 1)
+                                }
+                            }
+                        } label: {
+                            Text("Move Up")
+                        }
+                    }
+                    if customSelected.count > 1 || customSelected.first != config.additionalArguments.last?.id {
+                        Button {
+                            for i in (0..<config.additionalArguments.count-1).reversed() {
+                                if customSelected.contains(config.additionalArguments[i].id) {
+                                    config.additionalArguments.move(fromOffsets: .init(integer: i), toOffset: i + 2)
+                                }
+                            }
+                        } label: {
+                            Text("Move Down")
+                        }
+                    }
+                    Button(role: .destructive) {
+                        config.additionalArguments.removeAll(where: { customSelected.contains($0.id) })
+                    } label: {
+                        Text("Delete")
+                    }
+                }
+                Button {
+                    let new = QEMUArgument("")
+                    config.additionalArguments.append(new)
+                    selected.removeAll()
+                    selected.insert(new.id)
+                    focused = new.id
+                } label: {
+                    Text("New…")
+                }
+            }.padding([.bottom, .leading, .trailing])
+        }.modifier(VMShareItemModifier(isPresented: $showExportArgs, shareItem: exportShareItem))
+    }
+}

+ 6 - 0
Platform/macOS/VMQEMUSettingsView.swift

@@ -39,6 +39,12 @@ struct VMQEMUSettingsView: View {
         NavigationLink(destination: VMConfigQEMUView(config: $config.qemu, system: $config.system, fetchFixedArguments: { config.generatedArguments }).scrollable()) {
             Label("QEMU", systemImage: "shippingbox")
         }
+        if #available(macOS 12, *) {
+            NavigationLink(destination: VMConfigQEMUArgumentsView(config: $config.qemu, architecture: config.system.architecture, fixedArguments: config.generatedArguments)) {
+                Label("Arguments", systemImage: "character.textbox")
+                    .padding(.leading)
+            }
+        }
         NavigationLink(destination: VMConfigInputView(config: $config.input).scrollable()) {
             Label("Input", systemImage: "keyboard")
         }

+ 1 - 1
Platform/pl.lproj/Localizable.strings

@@ -526,7 +526,7 @@
 "QEMU Machine Properties" = "Właściwości maszyny QEMU";
 "This is appended to the -machine argument." = "To jest dołączone do argumentu -machine.";
 "QEMU Arguments" = "Argumenty dla QEMU";
-"Export QEMU Command" = "Eksportuj komendę QEMU";
+"Export QEMU Command" = "Eksportuj komendę QEMU";
 "(Delete)" = "(Usuń)";
 
 // VMConfigSerialView.swift

+ 1 - 1
Platform/zh-Hans.lproj/Localizable.strings

@@ -871,7 +871,7 @@
 "Cannot allocate memory" = "无法分配内存";
 
 /* No comment provided by engineer. */
-"Export QEMU Command" = "导出 QEMU 命令";
+"Export QEMU Command" = "导出 QEMU 命令";
 
 /* No comment provided by engineer. */
 "Random" = "随机生成";

+ 1 - 1
Platform/zh-Hant.lproj/Localizable.strings

@@ -374,7 +374,7 @@
 "Export Debug Log" = "匯出除錯日誌";
 
 /* No comment provided by engineer. */
-"Export QEMU Command" = "匯出 QEMU 命令";
+"Export QEMU Command" = "匯出 QEMU 命令";
 
 /* UTMVirtualMachine+Drives */
 "Failed create bookmark." = "無法建立書籤。";

+ 4 - 0
UTM.xcodeproj/project.pbxproj

@@ -845,6 +845,7 @@
 		CEBE820B26A4C8E0007AAB12 /* VMWizardSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEBE820A26A4C8E0007AAB12 /* VMWizardSummaryView.swift */; };
 		CEBE820C26A4C8E0007AAB12 /* VMWizardSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEBE820A26A4C8E0007AAB12 /* VMWizardSummaryView.swift */; };
 		CEBE820D26A4C8E0007AAB12 /* VMWizardSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEBE820A26A4C8E0007AAB12 /* VMWizardSummaryView.swift */; };
+		CEC0A30A2A7490D200980857 /* VMConfigQEMUArgumentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC0A3092A7490D200980857 /* VMConfigQEMUArgumentsView.swift */; };
 		CEC794BA294924E300121A9F /* UTMScriptingSerialPortImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC794B9294924E300121A9F /* UTMScriptingSerialPortImpl.swift */; };
 		CEC794BC2949663C00121A9F /* UTMScripting.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC794BB2949663C00121A9F /* UTMScripting.swift */; };
 		CEC794BD2949663C00121A9F /* UTMScripting.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC794BB2949663C00121A9F /* UTMScripting.swift */; };
@@ -1624,6 +1625,7 @@
 		CEBE820226A4C1B5007AAB12 /* VMWizardDrivesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMWizardDrivesView.swift; sourceTree = "<group>"; };
 		CEBE820626A4C74E007AAB12 /* VMWizardSharingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMWizardSharingView.swift; sourceTree = "<group>"; };
 		CEBE820A26A4C8E0007AAB12 /* VMWizardSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMWizardSummaryView.swift; sourceTree = "<group>"; };
+		CEC0A3092A7490D200980857 /* VMConfigQEMUArgumentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfigQEMUArgumentsView.swift; sourceTree = "<group>"; };
 		CEC794B9294924E300121A9F /* UTMScriptingSerialPortImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingSerialPortImpl.swift; sourceTree = "<group>"; };
 		CEC794BB2949663C00121A9F /* UTMScripting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UTMScripting.swift; sourceTree = "<group>"; };
 		CEC9968328AA516000E7A025 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@@ -2151,6 +2153,7 @@
 				84C584EA268FA6D1000FCABF /* VMConfigAppleSystemView.swift */,
 				848A98C7287206AE006F0550 /* VMConfigAppleVirtualizationView.swift */,
 				CE2D953E24AD4F980059923A /* VMConfigNetworkPortForwardView.swift */,
+				CEC0A3092A7490D200980857 /* VMConfigQEMUArgumentsView.swift */,
 				84A381A9268CB30C0048EE4D /* VMDrivesSettingsView.swift */,
 				CE928C3026ACCDEA0099F293 /* VMAppleRemovableDrivesView.swift */,
 				84C584E4268F8C65000FCABF /* VMAppleSettingsView.swift */,
@@ -3085,6 +3088,7 @@
 				CEECE13C25E47D9500A2AAB8 /* AppDelegate.swift in Sources */,
 				CE0B6CF724AD568400FE012D /* UTMLegacyQemuConfiguration+Display.m in Sources */,
 				CE25125529C80CD4000790AB /* UTMScriptingCreateCommand.swift in Sources */,
+				CEC0A30A2A7490D200980857 /* VMConfigQEMUArgumentsView.swift in Sources */,
 				843232B928C4816100CFBC97 /* UTMDownloadSupportToolsTask.swift in Sources */,
 				8471772A27CD3CAB00D3A50B /* DetailedSection.swift in Sources */,
 				8432329A28C3084A00CFBC97 /* GlobalFileImporter.swift in Sources */,