From 46180a9d7ca7d48bea8b0ab3fc51a9a737e28a14 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Fri, 20 Oct 2023 13:22:32 -0700 Subject: [PATCH] config(apple): support legacy keyboard configurations Determine from the wizard which keyboard configuration to use. Fixes #5810 --- .../UTMAppleConfigurationVirtualization.swift | 62 ++++++++++++------- Platform/Shared/VMWizardOSMacView.swift | 2 +- Platform/Shared/VMWizardState.swift | 34 ++++++++-- .../VMConfigAppleVirtualizationView.swift | 8 +-- Services/UTMExtensions.swift | 13 ++++ 5 files changed, 82 insertions(+), 37 deletions(-) diff --git a/Configuration/UTMAppleConfigurationVirtualization.swift b/Configuration/UTMAppleConfigurationVirtualization.swift index e683b9534..6949c05f9 100644 --- a/Configuration/UTMAppleConfigurationVirtualization.swift +++ b/Configuration/UTMAppleConfigurationVirtualization.swift @@ -29,8 +29,22 @@ struct UTMAppleConfigurationVirtualization: Codable { var prettyValue: String { switch self { case .disabled: return NSLocalizedString("Disabled", comment: "UTMAppleConfigurationDevices") - case .mouse: return NSLocalizedString("Mouse", comment: "UTMAppleConfigurationDevices") - case .trackpad: return NSLocalizedString("Trackpad", comment: "UTMAppleConfigurationDevices") + case .mouse: return NSLocalizedString("Generic Mouse", comment: "UTMAppleConfigurationDevices") + case .trackpad: return NSLocalizedString("Mac Trackpad (macOS 13+)", comment: "UTMAppleConfigurationDevices") + } + } + } + + enum KeyboardDevice: String, CaseIterable, QEMUConstant { + case disabled = "Disabled" + case generic = "Generic" + case mac = "Mac" + + var prettyValue: String { + switch self { + case .disabled: return NSLocalizedString("Disabled", comment: "UTMAppleConfigurationDevices") + case .generic: return NSLocalizedString("Generic USB", comment: "UTMAppleConfigurationDevices") + case .mac: return NSLocalizedString("Mac Keyboard (macOS 14+)", comment: "UTMAppleConfigurationDevices") } } } @@ -41,11 +55,9 @@ struct UTMAppleConfigurationVirtualization: Codable { var hasEntropy: Bool = true - var hasKeyboard: Bool = false + var keyboard: KeyboardDevice = .disabled - var hasPointer: Bool = false - - var hasTrackpad: Bool = false + var pointer: PointerDevice = .disabled var hasRosetta: Bool? @@ -55,8 +67,8 @@ struct UTMAppleConfigurationVirtualization: Codable { case hasAudio = "Audio" case hasBalloon = "Balloon" case hasEntropy = "Entropy" - case hasKeyboard = "Keyboard" - case hasPointer = "Pointer" + case keyboard = "Keyboard" + case pointer = "Pointer" case hasTrackpad = "Trackpad" case rosetta = "Rosetta" case hasClipboardSharing = "ClipboardSharing" @@ -70,13 +82,16 @@ struct UTMAppleConfigurationVirtualization: Codable { hasAudio = try values.decode(Bool.self, forKey: .hasAudio) hasBalloon = try values.decode(Bool.self, forKey: .hasBalloon) hasEntropy = try values.decode(Bool.self, forKey: .hasEntropy) - hasKeyboard = try values.decode(Bool.self, forKey: .hasKeyboard) - if let legacyPointer = try? values.decode(PointerDevice.self, forKey: .hasPointer) { - hasPointer = legacyPointer != .disabled - hasTrackpad = legacyPointer == .trackpad + if let hasKeyboard = try? values.decode(Bool.self, forKey: .keyboard) { + keyboard = hasKeyboard ? .generic : .disabled + } else { + keyboard = try values.decode(KeyboardDevice.self, forKey: .keyboard) + } + if let hasPointer = try? values.decode(Bool.self, forKey: .pointer) { + let hasTrackpad = try values.decodeIfPresent(Bool.self, forKey: .hasTrackpad) ?? false + pointer = hasTrackpad ? .trackpad : hasPointer ? .mouse : .disabled } else { - hasPointer = try values.decode(Bool.self, forKey: .hasPointer) - hasTrackpad = try values.decodeIfPresent(Bool.self, forKey: .hasTrackpad) ?? false + pointer = try values.decode(PointerDevice.self, forKey: .pointer) } if #available(macOS 13, *) { hasRosetta = try values.decodeIfPresent(Bool.self, forKey: .rosetta) @@ -89,9 +104,8 @@ struct UTMAppleConfigurationVirtualization: Codable { try container.encode(hasAudio, forKey: .hasAudio) try container.encode(hasBalloon, forKey: .hasBalloon) try container.encode(hasEntropy, forKey: .hasEntropy) - try container.encode(hasKeyboard, forKey: .hasKeyboard) - try container.encode(hasPointer, forKey: .hasPointer) - try container.encode(hasTrackpad, forKey: .hasTrackpad) + try container.encode(keyboard, forKey: .keyboard) + try container.encode(pointer, forKey: .pointer) try container.encodeIfPresent(hasRosetta, forKey: .rosetta) try container.encode(hasClipboardSharing, forKey: .hasClipboardSharing) } @@ -108,8 +122,8 @@ extension UTMAppleConfigurationVirtualization { hasEntropy = oldConfig.isEntropyEnabled if #available(macOS 12, *) { hasAudio = oldConfig.isAudioEnabled - hasKeyboard = oldConfig.isKeyboardEnabled - hasPointer = oldConfig.isPointingEnabled + keyboard = oldConfig.isKeyboardEnabled ? .generic : .disabled + pointer = oldConfig.isPointingEnabled ? .mouse : .disabled } } } @@ -138,25 +152,25 @@ extension UTMAppleConfigurationVirtualization { audioOutputConfiguration.streams = [audioOutput] vzconfig.audioDevices = [audioInputConfiguration, audioOutputConfiguration] } - if hasKeyboard { + if keyboard != .disabled { vzconfig.keyboards = [VZUSBKeyboardConfiguration()] #if arch(arm64) - if #available(macOS 14, *), isMacOSGuest { + if #available(macOS 14, *), isMacOSGuest && keyboard == .mac { vzconfig.keyboards = [VZMacKeyboardConfiguration()] } #endif } - if hasPointer { + if pointer != .disabled { vzconfig.pointingDevices = [VZUSBScreenCoordinatePointingDeviceConfiguration()] #if arch(arm64) - if #available(macOS 13, *), isMacOSGuest && hasTrackpad { + if #available(macOS 13, *), isMacOSGuest && pointer == .trackpad { // replace with trackpad device vzconfig.pointingDevices = [VZMacTrackpadConfiguration()] } #endif } } else { - if hasAudio || hasKeyboard || hasPointer { + if hasAudio || keyboard != .disabled || pointer != .disabled { throw UTMAppleConfigurationError.featureNotSupported } } diff --git a/Platform/Shared/VMWizardOSMacView.swift b/Platform/Shared/VMWizardOSMacView.swift index e3b820674..9a30d6f47 100644 --- a/Platform/Shared/VMWizardOSMacView.swift +++ b/Platform/Shared/VMWizardOSMacView.swift @@ -67,7 +67,7 @@ struct VMWizardOSMacView: View { await MainActor.run { wizardState.macPlatform = UTMAppleConfigurationMacPlatform(newHardware: model) wizardState.macRecoveryIpswURL = url - wizardState.macIsMonterey = image.buildVersion.hasPrefix("21") + wizardState.macPlatformVersion = image.buildVersion.integerPrefix() wizardState.isSkipBootImage = true wizardState.bootImageURL = nil wizardState.next() diff --git a/Platform/Shared/VMWizardState.swift b/Platform/Shared/VMWizardState.swift index 893811bd3..b4803cb00 100644 --- a/Platform/Shared/VMWizardState.swift +++ b/Platform/Shared/VMWizardState.swift @@ -83,7 +83,21 @@ enum VMWizardOS: String, Identifiable { #if os(macOS) && arch(arm64) @Published var macPlatform: UTMAppleConfigurationMacPlatform? @Published var macRecoveryIpswURL: URL? - @Published var macIsMonterey: Bool = false + @Published var macPlatformVersion: Int? + var macIsLeastVentura: Bool { + if let macPlatformVersion = macPlatformVersion { + return macPlatformVersion >= 22 + } else { + return false + } + } + var macIsLeastSonoma: Bool { + if let macPlatformVersion = macPlatformVersion { + return macPlatformVersion >= 23 + } else { + return false + } + } #endif @Published var isSkipBootImage: Bool = false @Published var bootImageURL: URL? @@ -282,7 +296,6 @@ enum VMWizardOS: String, Identifiable { config.system.boot = try! UTMAppleConfigurationBoot(for: .macOS) config.system.boot.macRecoveryIpswURL = macRecoveryIpswURL config.system.macPlatform = macPlatform - config.virtualization.hasTrackpad = !macIsMonterey } #endif case .Linux: @@ -318,16 +331,25 @@ enum VMWizardOS: String, Identifiable { } // some meaningful defaults if #available(macOS 12, *) { - var hasDisplay = operatingSystem == .macOS + let isMac = operatingSystem == .macOS + var hasDisplay = isMac if #available(macOS 13, *) { hasDisplay = hasDisplay || (operatingSystem == .Linux) } if hasDisplay { config.displays = [UTMAppleConfigurationDisplay(width: 1920, height: 1200)] config.virtualization.hasAudio = true - config.virtualization.hasKeyboard = true - config.virtualization.hasPointer = true + config.virtualization.keyboard = .generic + config.virtualization.pointer = .mouse + } + #if arch(arm64) + if isMac && macIsLeastVentura { + config.virtualization.pointer = .trackpad + } + if isMac && macIsLeastSonoma { + config.virtualization.keyboard = .mac } + #endif } config.virtualization.hasBalloon = true config.virtualization.hasEntropy = true @@ -351,7 +373,7 @@ enum VMWizardOS: String, Identifiable { if let hardwareModel = restoreImage.mostFeaturefulSupportedConfiguration?.hardwareModel { self.macPlatform = UTMAppleConfigurationMacPlatform(newHardware: hardwareModel) self.macRecoveryIpswURL = restoreImage.url - self.macIsMonterey = restoreImage.buildVersion.hasPrefix("21") + self.macPlatformVersion = restoreImage.buildVersion.integerPrefix() } else { self.alertMessage = AlertMessage(NSLocalizedString("Failed to get latest macOS version from Apple.", comment: "VMWizardState")) } diff --git a/Platform/macOS/VMConfigAppleVirtualizationView.swift b/Platform/macOS/VMConfigAppleVirtualizationView.swift index 8ccbaa40d..152de1d70 100644 --- a/Platform/macOS/VMConfigAppleVirtualizationView.swift +++ b/Platform/macOS/VMConfigAppleVirtualizationView.swift @@ -28,12 +28,8 @@ struct VMConfigAppleVirtualizationView: View { Toggle("Enable Entropy Device", isOn: $config.hasEntropy) if #available(macOS 12, *) { Toggle("Enable Sound", isOn: $config.hasAudio) - Toggle("Enable Keyboard", isOn: $config.hasKeyboard) - Toggle("Enable Pointer", isOn: $config.hasPointer) - } - if #available(macOS 13, *), config.hasPointer { - Toggle("Use Trackpad", isOn: $config.hasTrackpad) - .help("Allows passing through additional input from trackpads. Only supported on macOS 13+ guests.") + VMConfigConstantPicker("Keyboard", selection: $config.keyboard) + VMConfigConstantPicker("Pointer", selection: $config.pointer) } if #available(macOS 13, *), operatingSystem == .linux { #if arch(arm64) diff --git a/Services/UTMExtensions.swift b/Services/UTMExtensions.swift index 9d1ef8b49..4e464941b 100644 --- a/Services/UTMExtensions.swift +++ b/Services/UTMExtensions.swift @@ -371,3 +371,16 @@ extension URL { bookmarkDataIsStale: &stale) } } + +extension String { + func integerPrefix() -> Int? { + var numeric = "" + for char in self { + if !char.isNumber { + break + } + numeric.append(char) + } + return Int(numeric) + } +}