diff --git a/Platform/macOS/Display/VMMetalView.swift b/Platform/macOS/Display/VMMetalView.swift index 74c55d22c6..c6218c07b2 100644 --- a/Platform/macOS/Display/VMMetalView.swift +++ b/Platform/macOS/Display/VMMetalView.swift @@ -26,8 +26,25 @@ class VMMetalView: MTKView { private(set) var isMouseInWindow = false @Setting("HandleInitialClick") private var isHandleInitialClick: Bool = false @Setting("IsCtrlCmdSwapped") private var isCtrlCmdSwapped = false + @Setting("IsCmdOptSwapped") private var isCmdOptSwapped = false @Setting("IsISOKeySwapped") private var isISOKeySwapped = false + private enum ModifierKeySwap { + case none + case ctrlCmd + case cmdOpt + } + + private var modifierKeySwap: ModifierKeySwap { + if isCtrlCmdSwapped { + return .ctrlCmd + } else if isCmdOptSwapped { + return .cmdOpt + } else { + return .none + } + } + /// On ISO keyboards we have to switch `kVK_ISO_Section` and `kVK_ANSI_Grave` /// from: https://chromium.googlesource.com/chromium/src/+/lkgr/ui/events/keycodes/keyboard_code_conversion_mac.mm private func convertToCurrentLayout(for keycode: Int) -> Int { @@ -199,8 +216,16 @@ class VMMetalView: MTKView { } if !modifier.isDisjoint(with: [.command, .leftCommand, .rightCommand]) { let vk = modifier.contains(.rightCommand) ? kVK_RightCommand : kVK_Command - let vkSwapped = modifier.contains(.rightCommand) ? kVK_RightControl : kVK_Control - let sc = Int(KeyCodeMap.keyCodeToScanCodes[isCtrlCmdSwapped ? vkSwapped : vk]!.down) + let vkSwapped: Int + switch modifierKeySwap { + case .ctrlCmd: + vkSwapped = modifier.contains(.rightCommand) ? kVK_RightControl : kVK_Control + case .cmdOpt: + vkSwapped = modifier.contains(.rightCommand) ? kVK_RightOption : kVK_Option + case .none: + vkSwapped = vk + } + let sc = Int(KeyCodeMap.keyCodeToScanCodes[vkSwapped]!.down) if press { inputDelegate?.keyDown(scanCode: sc) } else { @@ -227,7 +252,13 @@ class VMMetalView: MTKView { } if !modifier.isDisjoint(with: [.option, .leftOption, .rightOption]) { let vk = modifier.contains(.rightOption) ? kVK_RightOption : kVK_Option - let sc = Int(KeyCodeMap.keyCodeToScanCodes[vk]!.down) + let vkSwapped: Int + if modifierKeySwap == .cmdOpt { + vkSwapped = modifier.contains(.rightOption) ? kVK_RightCommand : kVK_Command + } else { + vkSwapped = vk + } + let sc = Int(KeyCodeMap.keyCodeToScanCodes[vkSwapped]!.down) if press { inputDelegate?.keyDown(scanCode: sc) } else { diff --git a/Platform/macOS/SettingsView.swift b/Platform/macOS/SettingsView.swift index 1c20ae6b0c..f114fdfcdc 100644 --- a/Platform/macOS/SettingsView.swift +++ b/Platform/macOS/SettingsView.swift @@ -317,6 +317,12 @@ struct SoundSettingsView: View { } struct InputSettingsView: View { + private enum ModifierKeyMapping: Hashable { + case none + case ctrlCmd + case cmdOpt + } + @AppStorage("FullScreenAutoCapture") var isFullScreenAutoCapture = false @AppStorage("WindowFocusAutoCapture") var isWindowFocusAutoCapture = false @AppStorage("OptionAsMetaKey") var isOptionAsMetaKey = false @@ -325,11 +331,27 @@ struct InputSettingsView: View { @AppStorage("IsCapsLockKey") var isCapsLockKey = false @AppStorage("IsNumLockForced") var isNumLockForced = false @AppStorage("IsCtrlCmdSwapped") var isCtrlCmdSwapped = false + @AppStorage("IsCmdOptSwapped") var isCmdOptSwapped = false @AppStorage("InvertScroll") var isInvertScroll = false @AppStorage("HandleInitialClick") var isHandleInitialClick = false @AppStorage("IsISOKeySwapped") var isISOKeySwapped = false @State private var isKeyboardShortcutsShown = false + + private var modifierKeyMappingBinding: Binding { + Binding(get: { + if isCtrlCmdSwapped { + return .ctrlCmd + } else if isCmdOptSwapped { + return .cmdOpt + } else { + return .none + } + }, set: { mapping in + isCtrlCmdSwapped = mapping == .ctrlCmd + isCmdOptSwapped = mapping == .cmdOpt + }) + } var body: some View { Form { @@ -373,9 +395,11 @@ struct InputSettingsView: View { Toggle(isOn: $isNumLockForced, label: { Text("Num Lock is forced on") }).help("If enabled, num lock will always be on to the guest. Note this may make your keyboard's num lock indicator out of sync.") - Toggle(isOn: $isCtrlCmdSwapped, label: { - Text("Swap Control (⌃) and Command (⌘) keys") - }).help("This does not apply to key binding outside the guest.") + Picker("Control, Option, and Command keys", selection: modifierKeyMappingBinding) { + Text("Default").tag(ModifierKeyMapping.none) + Text("Swap Control (⌃) and Command (⌘)").tag(ModifierKeyMapping.ctrlCmd) + Text("Swap Command (⌘) and Option (⌥)").tag(ModifierKeyMapping.cmdOpt) + }.help("This does not apply to key binding outside the guest.") Toggle(isOn: $isISOKeySwapped) { Text("Swap the leftmost key on the number row and the key next to left shift on ISO keyboards") }.help("This only applies to ISO layout keyboards.")