Explorar el Código

display(macOS): add keyboard shortcuts

Resolves #5698
Resolves #6310
osy hace 3 semanas
padre
commit
ceaaf4212c

+ 18 - 2
Platform/macOS/Display/Base.lproj/VMDisplayWindow.xib

@@ -14,6 +14,7 @@
                 <outlet property="captureMouseToolbarItem" destination="FN7-zs-mWC" id="qzI-Kk-0D1"/>
                 <outlet property="displayView" destination="M5X-Is-pc9" id="U6s-s4-48i"/>
                 <outlet property="drivesToolbarItem" destination="bKL-Th-FFw" id="3SQ-Qt-5jn"/>
+                <outlet property="keyboardShortcutsItem" destination="0aR-4a-Su7" id="hUV-ll-S6s"/>
                 <outlet property="overlayView" destination="nKs-QY-EOf" id="sD4-fu-HIL"/>
                 <outlet property="resizeConsoleToolbarItem" destination="Ulf-oT-4cP" id="gIb-1X-LHA"/>
                 <outlet property="restartToolbarItem" destination="G7P-HJ-bcy" id="R8T-hV-Gr6"/>
@@ -36,7 +37,7 @@
             <windowCollectionBehavior key="collectionBehavior" fullScreenPrimary="YES"/>
             <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
             <rect key="contentRect" x="196" y="240" width="800" height="600"/>
-            <rect key="screenRect" x="0.0" y="0.0" width="1728" height="1079"/>
+            <rect key="screenRect" x="0.0" y="0.0" width="3008" height="1667"/>
             <view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
                 <rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
                 <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -144,6 +145,19 @@
                             </connections>
                         </button>
                     </toolbarItem>
+                    <toolbarItem implicitItemIdentifier="8B5CDC68-CEC0-4C3D-8E0A-AFA4A3BE8BA5" label="Keyboard Shortcuts" paletteLabel="Keyboard Shortcuts" toolTip="Keyboard shortcuts" image="keyboard" catalog="system" bordered="YES" sizingBehavior="auto" id="0aR-4a-Su7">
+                        <button key="view" verticalHuggingPriority="750" id="Ut7-KA-ER4">
+                            <rect key="frame" x="40" y="14" width="31" height="23"/>
+                            <autoresizingMask key="autoresizingMask"/>
+                            <buttonCell key="cell" type="roundTextured" bezelStyle="texturedRounded" image="keyboard" catalog="system" imagePosition="only" alignment="center" alternateImage="command.square" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="M2Z-2r-cfE">
+                                <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
+                                <font key="font" metaFont="system"/>
+                            </buttonCell>
+                            <connections>
+                                <action selector="keyboardShortcutsButtonPressed:" target="-2" id="u8k-Gr-q6g"/>
+                            </connections>
+                        </button>
+                    </toolbarItem>
                     <toolbarItem implicitItemIdentifier="E86439F7-239E-4570-B1FE-FFF2B4BA3F10" label="Capture Input" paletteLabel="Capture Input" toolTip="Capture input devices" image="cursorarrow.rays" catalog="system" sizingBehavior="auto" id="FN7-zs-mWC">
                         <button key="view" verticalHuggingPriority="750" id="Ge3-wo-FzQ">
                             <rect key="frame" x="26" y="14" width="28" height="23"/>
@@ -227,6 +241,7 @@
                     <toolbarItem reference="Bkx-Ph-j0D"/>
                     <toolbarItem reference="kT2-2U-cYm"/>
                     <toolbarItem reference="G7P-HJ-bcy"/>
+                    <toolbarItem reference="0aR-4a-Su7"/>
                     <toolbarItem reference="FN7-zs-mWC"/>
                     <toolbarItem reference="Ulf-oT-4cP"/>
                     <toolbarItem reference="tlw-Fb-ne3"/>
@@ -253,7 +268,8 @@
         <image name="arrow.up.left.and.arrow.down.right" catalog="system" width="16" height="15"/>
         <image name="command.square" catalog="system" width="15" height="14"/>
         <image name="cursorarrow.rays" catalog="system" width="16" height="16"/>
-        <image name="folder.badge.person.crop" catalog="system" width="19" height="14"/>
+        <image name="folder.badge.person.crop" catalog="system" width="20" height="15"/>
+        <image name="keyboard" catalog="system" width="19" height="13"/>
         <image name="opticaldisc" catalog="system" width="15" height="15"/>
         <image name="play.circle.fill" catalog="system" width="15" height="15"/>
         <image name="play.fill" catalog="system" width="12" height="13"/>

+ 1 - 12
Platform/macOS/Display/VMDisplayAppleWindowController.swift

@@ -87,6 +87,7 @@ class VMDisplayAppleWindowController: VMDisplayWindowController {
         drivesToolbarItem.isEnabled = false
         usbToolbarItem.isEnabled = false
         resizeConsoleToolbarItem.isEnabled = false
+        keyboardShortcutsItem.isEnabled = false
         if #available(macOS 13, *) {
             sharedFolderToolbarItem.isEnabled = true
         } else if #available(macOS 12, *) {
@@ -336,18 +337,6 @@ extension VMDisplayAppleWindowController {
         menu.update()
     }
 
-    @nonobjc private func withErrorAlert(_ callback: @escaping () async throws -> Void) {
-        Task.detached(priority: .background) { [self] in
-            do {
-                try await callback()
-            } catch {
-                Task { @MainActor in
-                    showErrorAlert(error.localizedDescription)
-                }
-            }
-        }
-    }
-
     @available(macOS 15, *)
     func ejectDrive(sender: AnyObject) {
         guard let menu = sender as? NSMenuItem else {

+ 53 - 0
Platform/macOS/Display/VMDisplayQemuMetalWindowController.swift

@@ -16,6 +16,7 @@
 
 import CocoaSpiceRenderer
 import Carbon.HIToolbox
+import SwiftUI
 
 class VMDisplayQemuMetalWindowController: VMDisplayQemuWindowController {
     var metalView: VMMetalView!
@@ -680,3 +681,55 @@ extension VMDisplayQemuMetalWindowController: VMMetalViewInputDelegate {
         }
     }
 }
+
+// MARK: - Keyboard shortcuts menu
+extension VMDisplayQemuMetalWindowController {
+    override func keyboardShortcutsButtonPressed(_ sender: Any) {
+        let menu = NSMenu()
+        let keyboardShortcuts = UTMKeyboardShortcuts.shared.loadKeyboardShortcuts()
+        for (index, keyboardShortcut) in keyboardShortcuts.enumerated() {
+            let item = NSMenuItem()
+            item.title = keyboardShortcut.title
+            item.target = self
+            item.action = #selector(keyboardShortcutHandler)
+            item.tag = index
+            menu.addItem(item)
+        }
+        menu.addItem(.separator())
+        let item = NSMenuItem()
+        item.title = NSLocalizedString("Edit…", comment: "VMDisplayQemuMetalWindowController")
+        item.target = self
+        item.action = #selector(keyboardShortcutEdit)
+        menu.addItem(item)
+        menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)
+    }
+    
+    @MainActor @objc private func keyboardShortcutHandler(sender: AnyObject) {
+        let keyboardShortcuts = UTMKeyboardShortcuts.shared.loadKeyboardShortcuts()
+        let item = sender as! NSMenuItem
+        let index = item.tag
+        guard index < keyboardShortcuts.count else {
+            return
+        }
+        let keys = keyboardShortcuts[index]
+        withErrorAlert {
+            try await self.qemuVM.monitor?.sendKeys(keys)
+        }
+    }
+    
+    @MainActor @objc private func keyboardShortcutEdit(sender: AnyObject) {
+        guard let window = window else {
+            return
+        }
+        let content = NSHostingController(rootView: VMKeyboardShortcutsView {
+            if let sheet = window.attachedSheet {
+                window.endSheet(sheet)
+            }
+        }.padding())
+        var fittingSize = content.view.fittingSize
+        fittingSize.width = 400
+        let sheetWindow = NSWindow(contentViewController: content)
+        sheetWindow.setContentSize(fittingSize)
+        window.beginSheet(sheetWindow)
+    }
+}

+ 1 - 0
Platform/macOS/Display/VMDisplayQemuTerminalWindowController.swift

@@ -55,6 +55,7 @@ class VMDisplayQemuTerminalWindowController: VMDisplayQemuWindowController, VMDi
         isSizeChangeIgnored = true
         setupTerminal(terminalView, using: serialConfig!.terminal!, id: id, for: window!)
         isSizeChangeIgnored = false
+        keyboardShortcutsItem.isEnabled = false
     }
     
     override func enterSuspended(isBusy busy: Bool) {

+ 20 - 2
Platform/macOS/Display/VMDisplayWindowController.swift

@@ -35,7 +35,8 @@ class VMDisplayWindowController: NSWindowController, UTMVirtualMachineDelegate {
     @IBOutlet weak var sharedFolderToolbarItem: NSToolbarItem!
     @IBOutlet weak var resizeConsoleToolbarItem: NSToolbarItem!
     @IBOutlet weak var windowsToolbarItem: NSToolbarItem!
-    
+    @IBOutlet weak var keyboardShortcutsItem: NSToolbarItem!
+
     var shouldAutoStartVM: Bool = true
     var vm: (any UTMVirtualMachine)!
     var onClose: (() -> Void)?
@@ -121,7 +122,10 @@ class VMDisplayWindowController: NSWindowController, UTMVirtualMachineDelegate {
     
     @IBAction dynamic func windowsButtonPressed(_ sender: Any) {
     }
-    
+
+    @IBAction dynamic func keyboardShortcutsButtonPressed(_ sender: Any) {
+    }
+
     // MARK: - UI states
     
     override func windowDidLoad() {
@@ -162,6 +166,7 @@ class VMDisplayWindowController: NSWindowController, UTMVirtualMachineDelegate {
         captureMouseToolbarItem.isEnabled = true
         resizeConsoleToolbarItem.isEnabled = true
         windowsToolbarItem.isEnabled = true
+        keyboardShortcutsItem.isEnabled = true
         window!.makeFirstResponder(displayView.subviews.first)
         if isPreventIdleSleep && !isSecondary {
             var preventIdleSleepAssertion: IOPMAssertionID = .zero
@@ -200,6 +205,7 @@ class VMDisplayWindowController: NSWindowController, UTMVirtualMachineDelegate {
         sharedFolderToolbarItem.isEnabled = false
         usbToolbarItem.isEnabled = false
         windowsToolbarItem.isEnabled = false
+        keyboardShortcutsItem.isEnabled = false
         window!.makeFirstResponder(nil)
         if let preventIdleSleepAssertion = preventIdleSleepAssertion {
             IOPMAssertionRelease(preventIdleSleepAssertion)
@@ -234,6 +240,18 @@ class VMDisplayWindowController: NSWindowController, UTMVirtualMachineDelegate {
         }
     }
     
+    @nonobjc func withErrorAlert(_ callback: @escaping () async throws -> Void) {
+        Task.detached(priority: .background) { [self] in
+            do {
+                try await callback()
+            } catch {
+                Task { @MainActor in
+                    showErrorAlert(error.localizedDescription)
+                }
+            }
+        }
+    }
+    
     // MARK: - Create a secondary window
     
     func registerSecondaryWindow(_ secondaryWindow: VMDisplayWindowController, at index: Int? = nil) {

+ 9 - 0
Platform/macOS/SettingsView.swift

@@ -257,6 +257,8 @@ struct InputSettingsView: View {
     @AppStorage("HandleInitialClick") var isHandleInitialClick = false
     @AppStorage("NoUsbPrompt") var isNoUsbPrompt = false
     
+    @State private var isKeyboardShortcutsShown = false
+    
     var body: some View {
         Form {
             Section(header: Text("Mouse/Keyboard")) {
@@ -287,6 +289,9 @@ struct InputSettingsView: View {
             }
             
             Section(header: Text("QEMU Keyboard")) {
+                Button("Keyboard Shortcuts…") {
+                    isKeyboardShortcutsShown.toggle()
+                }.help("Set up custom keyboard shortcuts that can be triggered from the keyboard menu.")
                 Toggle(isOn: $isAlternativeCaptureKey, label: {
                     Text("Use Command+Option (⌘+⌥) for input capture/release")
                 }).help("If disabled, the default combination Control+Option (⌃+⌥) will be used.")
@@ -300,6 +305,10 @@ struct InputSettingsView: View {
                     Text("Swap Control (⌃) and Command (⌘) keys")
                 }).help("This does not apply to key binding outside the guest.")
             }
+            .sheet(isPresented: $isKeyboardShortcutsShown) {
+                VMKeyboardShortcutsView().padding()
+                    .frame(idealWidth: 400)
+            }
             
             Section(header: Text("QEMU USB")) {
                 Toggle(isOn: $isNoUsbPrompt, label: {

+ 204 - 0
Platform/macOS/VMKeyboardShortcutsView.swift

@@ -0,0 +1,204 @@
+//
+// Copyright © 2025 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
+
+struct VMKeyboardShortcutsView: View {
+    @Environment(\.presentationMode) var presentationMode
+    @State private var keyboardShortcuts: [[QEMUKeyCode]] = []
+    @State private var isEditing: Bool = false
+    @State private var currentlyEditingIndex: SelectedIndex?
+    @State private var currentlyEditingShortcut: [QEMUKeyCode] = []
+    
+    let onDismiss: () -> Void
+    
+    init(onDismiss: @escaping () -> Void = {}) {
+        self.onDismiss = onDismiss
+    }
+    
+    var body: some View {
+        VStack {
+            HStack {
+                Spacer()
+                Button {
+                    onDismiss()
+                    presentationMode.wrappedValue.dismiss()
+                } label: {
+                    Text("Done")
+                }
+            }
+            List(selection: $currentlyEditingIndex) {
+                ForEach(Array(keyboardShortcuts.enumerated()), id: \.element) { index, element in
+                    Text(element.title)
+                        .tag(SelectedIndex(id: index))
+                        .contextMenu {
+                            Button("Edit") {
+                                isEditing.toggle()
+                            }
+                            DestructiveButton("Delete") {
+                                keyboardShortcuts.remove(at: index)
+                            }
+                        }
+                }.onDelete { indexSet in
+                    keyboardShortcuts.remove(atOffsets: indexSet)
+                }.onMove { indexSet, offset in
+                    keyboardShortcuts.move(fromOffsets: indexSet, toOffset: offset)
+                }
+            }.borderedList()
+            .frame(height: 200)
+            HStack {
+                Spacer()
+                if let index = currentlyEditingIndex?.id {
+                    DestructiveButton("Delete") {
+                        keyboardShortcuts.remove(at: index)
+                    }
+                    Button {
+                        isEditing.toggle()
+                    } label: {
+                        Text("Edit")
+                    }
+                }
+                Button {
+                    currentlyEditingIndex = nil
+                    currentlyEditingShortcut = []
+                    isEditing.toggle()
+                } label: {
+                    Text("New")
+                }
+            }
+        }
+        .sheet(isPresented: $isEditing, onDismiss: {
+            if let index = currentlyEditingIndex {
+                if !currentlyEditingShortcut.isEmpty {
+                    keyboardShortcuts[index.id] = currentlyEditingShortcut
+                } else {
+                    keyboardShortcuts.remove(at: index.id)
+                }
+            } else {
+                if !currentlyEditingShortcut.isEmpty {
+                    keyboardShortcuts.append(currentlyEditingShortcut)
+                }
+            }
+        }, content: {
+            EditKeyboardShortcutView(keyboardShortcut: $currentlyEditingShortcut).padding()
+        })
+        .onAppear {
+            keyboardShortcuts = UTMKeyboardShortcuts.shared.loadKeyboardShortcuts()
+        }
+        .onChange(of: keyboardShortcuts) { newValue in
+            UTMKeyboardShortcuts.shared.saveKeyboardShortcuts(newValue)
+        }
+        .onChange(of: currentlyEditingIndex) { newValue in
+            if let index = newValue?.id {
+                currentlyEditingShortcut = keyboardShortcuts[index]
+            } else {
+                currentlyEditingShortcut = []
+            }
+        }
+    }
+}
+
+private struct EditKeyboardShortcutView: View {
+    @Environment(\.presentationMode) var presentationMode
+    @Binding var keyboardShortcut: [QEMUKeyCode]
+    @State private var currentlyEditingIndex: SelectedIndex?
+    @State private var currentlyEditingKey: QEMUKeyCode?
+
+    var body: some View {
+        VStack {
+            HStack {
+                Spacer()
+                Button {
+                    presentationMode.wrappedValue.dismiss()
+                } label: {
+                    Text("Done")
+                }
+            }
+            List(selection: $currentlyEditingIndex) {
+                ForEach(Array(keyboardShortcut.enumerated()), id: \.element) { index, keyCode in
+                    Text(keyCode.title)
+                        .tag(SelectedIndex(id: index))
+                        .contextMenu {
+                            DestructiveButton("Delete") {
+                                keyboardShortcut.remove(at: index)
+                            }
+                        }
+                }.onDelete { indexSet in
+                    keyboardShortcut.remove(atOffsets: indexSet)
+                }.onMove { indexSet, offset in
+                    keyboardShortcut.move(fromOffsets: indexSet, toOffset: offset)
+                }
+            }.borderedList()
+            .frame(height: 100)
+            Spacer()
+            HStack {
+                Picker("New Key", selection: $currentlyEditingKey) {
+                    Text("").tag(nil as QEMUKeyCode?)
+                    ForEach(QEMUKeyCode.allCases) { keyCode in
+                        if !keyboardShortcut.contains(keyCode) {
+                            Text(keyCode.title).tag(keyCode)
+                        }
+                    }
+                }
+                Spacer()
+                if let index = currentlyEditingIndex?.id {
+                    DestructiveButton("Delete") {
+                        keyboardShortcut.remove(at: index)
+                    }
+                    Button {
+                        if let currentlyEditingKey = currentlyEditingKey {
+                            keyboardShortcut[index] = currentlyEditingKey
+                        }
+                        currentlyEditingKey = nil
+                    } label: {
+                        Text("Update")
+                    }.disabled(currentlyEditingKey == nil)
+                }
+                Button {
+                    if let currentlyEditingKey = currentlyEditingKey {
+                        keyboardShortcut.append(currentlyEditingKey)
+                    }
+                    currentlyEditingKey = nil
+                    currentlyEditingIndex = nil
+                } label: {
+                    Text("Add")
+                }.disabled(currentlyEditingKey == nil)
+            }
+        }
+    }
+}
+
+private struct SelectedIndex: Identifiable, Hashable {
+    var id: Int
+    func hash(into hasher: inout Hasher) {
+        hasher.combine(id)
+    }
+}
+
+private extension View {
+    @ViewBuilder
+    func borderedList() -> some View {
+        if #available(macOS 12, *) {
+            self.listStyle(.bordered)
+        } else {
+            self.border(.gray)
+        }
+    }
+}
+
+#Preview {
+    VMKeyboardShortcutsView()
+}

+ 65 - 0
Services/UTMKeyboardShortcuts.swift

@@ -0,0 +1,65 @@
+//
+// Copyright © 2025 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 Foundation
+import QEMUKit
+import QEMUKitInternal
+
+final class UTMKeyboardShortcuts {
+    static let shared = UTMKeyboardShortcuts()
+    @Setting("KeyboardShortcuts") private var savedKeyboardShortcuts: Data? = nil
+    
+    private init() {}
+    
+    func loadKeyboardShortcuts() -> [[QEMUKeyCode]] {
+        let decoder = PropertyListDecoder()
+        if let data = savedKeyboardShortcuts {
+            if let decoded = try? decoder.decode([[QEMUKeyCode]].self, from: data) {
+                return decoded
+            }
+        }
+        // default entry
+        return [[.keyCtrl, .keyAlt, .keyDelete]]
+    }
+    
+    func saveKeyboardShortcuts(_ keyboardShortcuts: [[QEMUKeyCode]]) {
+        let encoder = PropertyListEncoder()
+        encoder.outputFormat = .binary
+        if let data = try? encoder.encode(keyboardShortcuts) {
+            savedKeyboardShortcuts = data
+        }
+    }
+}
+
+extension QEMUKeyCode: @retroactive Codable, @retroactive Identifiable, @retroactive CaseIterable {
+    public var id: Self {
+        self
+    }
+    
+    public var title: String {
+        QEMUMonitor.keyMap[self] ?? ""
+    }
+    
+    public static var allCases: [QEMUKeyCode] {
+        Array(QEMUMonitor.keyMap.keys).sorted { $0.rawValue < $1.rawValue }
+    }
+}
+
+extension Array where Element == QEMUKeyCode {
+    var title: String {
+        self.map({ $0.title }).joined(separator: "+")
+    }
+}

+ 22 - 0
UTM.xcodeproj/project.pbxproj

@@ -149,6 +149,11 @@
 		845F95E42A57628400A016D7 /* UTMSWTPM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845F95E22A57628400A016D7 /* UTMSWTPM.swift */; };
 		845F95E52A57628400A016D7 /* UTMSWTPM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845F95E22A57628400A016D7 /* UTMSWTPM.swift */; };
 		846D878629050B6B0095F10B /* InAppSettingsKit in Frameworks */ = {isa = PBXBuildFile; platformFilter = ios; productRef = 846D878529050B6B0095F10B /* InAppSettingsKit */; };
+		846F8D582E3850620037162B /* VMKeyboardShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846F8D572E3850620037162B /* VMKeyboardShortcutsView.swift */; };
+		846F8D5A2E3891FE0037162B /* UTMKeyboardShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846F8D592E3891FE0037162B /* UTMKeyboardShortcuts.swift */; };
+		846F8D5B2E3891FE0037162B /* UTMKeyboardShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846F8D592E3891FE0037162B /* UTMKeyboardShortcuts.swift */; };
+		846F8D5C2E3891FE0037162B /* UTMKeyboardShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846F8D592E3891FE0037162B /* UTMKeyboardShortcuts.swift */; };
+		846F8D5D2E3891FE0037162B /* UTMKeyboardShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846F8D592E3891FE0037162B /* UTMKeyboardShortcuts.swift */; };
 		8471770627CC974F00D3A50B /* DefaultTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471770527CC974F00D3A50B /* DefaultTextField.swift */; };
 		8471770727CC974F00D3A50B /* DefaultTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471770527CC974F00D3A50B /* DefaultTextField.swift */; };
 		8471770827CC974F00D3A50B /* DefaultTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471770527CC974F00D3A50B /* DefaultTextField.swift */; };
@@ -642,6 +647,7 @@
 		CE612AC624D3B50700FA6300 /* VMDisplayWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE612AC524D3B50700FA6300 /* VMDisplayWindowController.swift */; };
 		CE65BABF26A4D8DD0001BD6B /* VMConfigDisplayConsoleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8401FDA5269D44E400265F0D /* VMConfigDisplayConsoleView.swift */; };
 		CE65BAC026A4D8DE0001BD6B /* VMConfigDisplayConsoleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8401FDA5269D44E400265F0D /* VMConfigDisplayConsoleView.swift */; };
+		CE68E5412E38A278006B3645 /* QEMUKit in Frameworks */ = {isa = PBXBuildFile; productRef = CE68E5402E38A278006B3645 /* QEMUKit */; };
 		CE6C13CA2B63610C003B7032 /* UTMRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6C13C92B63610C003B7032 /* UTMRemoteMessage.swift */; };
 		CE6C13CB2B63610C003B7032 /* UTMRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6C13C92B63610C003B7032 /* UTMRemoteMessage.swift */; };
 		CE6D21DC2553A6ED001D29C5 /* VMConfirmActionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6D21DB2553A6ED001D29C5 /* VMConfirmActionModifier.swift */; };
@@ -1725,6 +1731,8 @@
 		845F170A289CB07200944904 /* VMDisplayAppleDisplayWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMDisplayAppleDisplayWindowController.swift; sourceTree = "<group>"; };
 		845F170C289CB3DE00944904 /* VMDisplayTerminal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMDisplayTerminal.swift; sourceTree = "<group>"; };
 		845F95E22A57628400A016D7 /* UTMSWTPM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMSWTPM.swift; sourceTree = "<group>"; };
+		846F8D572E3850620037162B /* VMKeyboardShortcutsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMKeyboardShortcutsView.swift; sourceTree = "<group>"; };
+		846F8D592E3891FE0037162B /* UTMKeyboardShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMKeyboardShortcuts.swift; sourceTree = "<group>"; };
 		8471770527CC974F00D3A50B /* DefaultTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultTextField.swift; sourceTree = "<group>"; };
 		8471772727CD3CAB00D3A50B /* DetailedSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailedSection.swift; sourceTree = "<group>"; };
 		847BF9A92A49C783000BD9AA /* VMData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMData.swift; sourceTree = "<group>"; };
@@ -2477,6 +2485,7 @@
 				CEF7F65D2AEEDCC400E34952 /* gstcontroller-1.0.0.framework in Frameworks */,
 				CEF7F65E2AEEDCC400E34952 /* gstaudio-1.0.0.framework in Frameworks */,
 				CEF7F65F2AEEDCC400E34952 /* gpg-error.0.framework in Frameworks */,
+				CE68E5412E38A278006B3645 /* QEMUKit in Frameworks */,
 				CEF7F6602AEEDCC400E34952 /* gcrypt.20.framework in Frameworks */,
 				CEF7F6612AEEDCC400E34952 /* InAppSettingsKit in Frameworks */,
 				CEF7F6622AEEDCC400E34952 /* gobject-2.0.0.framework in Frameworks */,
@@ -2729,6 +2738,7 @@
 				CE928C3026ACCDEA0099F293 /* VMAppleRemovableDrivesView.swift */,
 				84C584E4268F8C65000FCABF /* VMAppleSettingsView.swift */,
 				845F1706289B5E2600944904 /* VMAppleSettingsAddDeviceMenuView.swift */,
+				846F8D572E3850620037162B /* VMKeyboardShortcutsView.swift */,
 				84C584E2268F8AE7000FCABF /* VMQEMUSettingsView.swift */,
 				CE2D953D24AD4F980059923A /* VMSettingsView.swift */,
 				CEF0300526A25A6900667B63 /* VMWizardView.swift */,
@@ -2873,6 +2883,7 @@
 				CE2D954624AD4F980059923A /* UTMExtensions.swift */,
 				CEB63A7924F469E300CAF323 /* UTMJailbreak.m */,
 				CEB63A7824F468BA00CAF323 /* UTMJailbreak.h */,
+				846F8D592E3891FE0037162B /* UTMKeyboardShortcuts.swift */,
 				CE059DC6243E9E3400338317 /* UTMLocationManager.h */,
 				CE059DC7243E9E3400338317 /* UTMLocationManager.m */,
 				CE6EDCE0241DA0E900A719DC /* UTMLogging.h */,
@@ -3310,6 +3321,7 @@
 				CE9B153B2B11A4B4003A32DD /* SwiftConnect */,
 				CE89CB112B8B1B7A006B2CC2 /* VisionKeyboardKit */,
 				CE231D592BE03791006D6DC3 /* SwiftCopyfile */,
+				CE68E5402E38A278006B3645 /* QEMUKit */,
 			);
 			productName = UTM;
 			productReference = CEF7F6D32AEEDCC400E34952 /* UTM Remote.app */;
@@ -3722,6 +3734,7 @@
 				84C2E8652AA429E800B17308 /* VMWizardContent.swift in Sources */,
 				841E58CB28937EE200137A20 /* UTMExternalSceneDelegate.swift in Sources */,
 				CEB63A7A24F469E300CAF323 /* UTMJailbreak.m in Sources */,
+				846F8D5D2E3891FE0037162B /* UTMKeyboardShortcuts.swift in Sources */,
 				841619B228431DA5000034B2 /* UTMQemuConfigurationQEMU.swift in Sources */,
 				843BF82428441EAD0029D60D /* UTMQemuConfigurationDisplay.swift in Sources */,
 				CE8011202AD4E9E8009001C2 /* UTMApp.swift in Sources */,
@@ -3880,6 +3893,7 @@
 				CE03D0D424DCF6DD00F76B84 /* VMMetalViewInputDelegate.swift in Sources */,
 				848F71EA277A2A4E006A0240 /* UTMSerialPort.swift in Sources */,
 				CE25124D29C55816000790AB /* UTMScriptingConfigImpl.swift in Sources */,
+				846F8D582E3850620037162B /* VMKeyboardShortcutsView.swift in Sources */,
 				CE0B6D0224AD56AE00FE012D /* UTMProcess.m in Sources */,
 				CEF0306026A2AFDF00667B63 /* VMWizardState.swift in Sources */,
 				CEF0300826A25A6900667B63 /* VMWizardView.swift in Sources */,
@@ -3964,6 +3978,7 @@
 				CE25124929BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift in Sources */,
 				848A98C8287206AE006F0550 /* VMConfigAppleVirtualizationView.swift in Sources */,
 				CE9B153F2B11A63E003A32DD /* UTMRemoteServer.swift in Sources */,
+				846F8D5A2E3891FE0037162B /* UTMKeyboardShortcuts.swift in Sources */,
 				847BF9AC2A49C783000BD9AA /* VMData.swift in Sources */,
 				CE25124729BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift in Sources */,
 				CE2D958824AD4F990059923A /* VMConfigPortForwardForm.swift in Sources */,
@@ -4010,6 +4025,7 @@
 				CEA45E43263519B5002FA97D /* UTMLegacyQemuConfigurationPortForward.m in Sources */,
 				843BF841284555E70029D60D /* UTMQemuConfigurationPortForward.swift in Sources */,
 				84A0A8842A47D52E0038F329 /* UTMQemuPort.swift in Sources */,
+				846F8D5B2E3891FE0037162B /* UTMKeyboardShortcuts.swift in Sources */,
 				848D99C528670F650055C215 /* UTMQemuConfiguration+Arguments.swift in Sources */,
 				CEA45E4B263519B5002FA97D /* BusyOverlay.swift in Sources */,
 				84937F032897451C003148F4 /* VMToolbarDriveMenuView.swift in Sources */,
@@ -4255,6 +4271,7 @@
 				CEF7F5EA2AEEDCC400E34952 /* VMConfigConstantPicker.swift in Sources */,
 				03FA9C742B9BBDB000C53A5A /* UTMConfigurationHostNetwork.swift in Sources */,
 				CEF7F5EC2AEEDCC400E34952 /* VMToolbarModifier.swift in Sources */,
+				846F8D5C2E3891FE0037162B /* UTMKeyboardShortcuts.swift in Sources */,
 				CEF7F5ED2AEEDCC400E34952 /* VMCursor.m in Sources */,
 				CEF7F5EE2AEEDCC400E34952 /* VMConfigDriveDetailsView.swift in Sources */,
 				CEF7F5F02AEEDCC400E34952 /* NumberTextField.swift in Sources */,
@@ -5586,6 +5603,11 @@
 			package = CE231D502BE03617006D6DC3 /* XCRemoteSwiftPackageReference "SwiftCopyfile" */;
 			productName = SwiftCopyfile;
 		};
+		CE68E5402E38A278006B3645 /* QEMUKit */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 84A0A8862A47D5C50038F329 /* XCRemoteSwiftPackageReference "QEMUKit" */;
+			productName = QEMUKit;
+		};
 		CE89CB0D2B8B1B5A006B2CC2 /* VisionKeyboardKit */ = {
 			isa = XCSwiftPackageProductDependency;
 			package = CE89CB0C2B8B1B49006B2CC2 /* XCRemoteSwiftPackageReference "VisionKeyboardKit" */;

+ 1 - 1
UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -52,7 +52,7 @@
       "location" : "https://github.com/utmapp/QEMUKit.git",
       "state" : {
         "branch" : "main",
-        "revision" : "6ddf970edaacf708aaf4483d153621921cbf737f"
+        "revision" : "7e6923686a722220917ca1ac9084793e1ae29ae0"
       }
     },
     {