Jelajahi Sumber

display(iOS): add keyboard shortcuts

osy 3 minggu lalu
induk
melakukan
c02cddc580

+ 113 - 0
Platform/iOS/VMKeyboardShortcutsView.swift

@@ -0,0 +1,113 @@
+//
+// 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 {
+    let onShortcut: ([QEMUKeyCode]) -> Void
+    @Environment(\.presentationMode) var presentationMode
+    @State private var keyboardShortcuts: [[QEMUKeyCode]] = []
+
+    var body: some View {
+        NavigationView {
+            List {
+                ForEach(keyboardShortcuts, id: \.self) { element in
+                    Button(element.title) {
+                        onShortcut(element)
+                        presentationMode.wrappedValue.dismiss()
+                    }
+                }.onDelete { indexSet in
+                    keyboardShortcuts.remove(atOffsets: indexSet)
+                }.onMove { indexSet, offset in
+                    keyboardShortcuts.move(fromOffsets: indexSet, toOffset: offset)
+                }
+                NavigationLink("Add…") {
+                    NewKeyboardShortcutView(keyboardShortcuts: $keyboardShortcuts)
+                }
+            }.navigationTitle("Keyboard Shortcut")
+            .toolbar {
+                ToolbarItemGroup(placement: .navigationBarTrailing) {
+                    EditButton()
+                    Button("Close") {
+                        presentationMode.wrappedValue.dismiss()
+                    }
+                }
+            }
+        }
+        .onAppear {
+            keyboardShortcuts = UTMKeyboardShortcuts.shared.loadKeyboardShortcuts()
+        }
+        .onChange(of: keyboardShortcuts) { newValue in
+            UTMKeyboardShortcuts.shared.saveKeyboardShortcuts(newValue)
+        }
+    }
+}
+
+private struct NewKeyboardShortcutView: View {
+    @Environment(\.presentationMode) var presentationMode
+    @Binding var keyboardShortcuts: [[QEMUKeyCode]]
+    @State private var newShortcut: [QEMUKeyCode] = []
+    @State private var newKey: QEMUKeyCode?
+
+    var body: some View {
+        List {
+            DetailedSection("Keys") {
+                ForEach(newShortcut, id: \.self) { element in
+                    Text(element.title)
+                }.onDelete { indexSet in
+                    newShortcut.remove(atOffsets: indexSet)
+                }.onMove { indexSet, offset in
+                    newShortcut.move(fromOffsets: indexSet, toOffset: offset)
+                }
+            }
+            DetailedSection("New Key") {
+                Picker("", selection: $newKey) {
+                    Text("").tag(nil as QEMUKeyCode?)
+                    ForEach(QEMUKeyCode.allCases) { keyCode in
+                        if !newShortcut.contains(keyCode) {
+                            Text(keyCode.title).tag(keyCode)
+                        }
+                    }
+                }.pickerStyle(.wheel)
+                Button("Add") {
+                    if let key = newKey {
+                        newShortcut.append(key)
+                    }
+                    newKey = nil
+                }.disabled(newKey == nil)
+            }
+        }.navigationTitle("New Keyboard Shortcut")
+        .toolbar {
+            ToolbarItemGroup(placement: .navigationBarTrailing) {
+                EditButton()
+                Button("Save") {
+                    if !newShortcut.isEmpty {
+                        keyboardShortcuts.append(newShortcut)
+                    }
+                    presentationMode.wrappedValue.dismiss()
+                }.disabled(newShortcut.isEmpty)
+            }
+        }
+        .onAppear {
+            newShortcut = []
+            newKey = nil
+        }
+    }
+}
+
+#Preview {
+    VMKeyboardShortcutsView() { _ in }
+}

+ 15 - 0
Platform/iOS/VMSessionState.swift

@@ -517,6 +517,21 @@ extension VMSessionState {
         vm.requestVmReset()
     }
     
+    #if !WITH_REMOTE
+    func sendKeys(keys: [QEMUKeyCode]) {
+        Task {
+            guard let monitor = await (vm as? UTMQemuVirtualMachine)?.monitor else {
+                return
+            }
+            do {
+                try await monitor.sendKeys(keys)
+            } catch {
+                self.nonfatalError = error.localizedDescription
+            }
+        }
+    }
+    #endif
+    
     func didReceiveMemoryWarning() {
         let shouldAutosave = UserDefaults.standard.bool(forKey: "AutosaveLowMemory")
         

+ 17 - 0
Platform/iOS/VMToolbarView.swift

@@ -25,6 +25,7 @@ struct VMToolbarView: View {
     @State private var isIdle: Bool = false
     @State private var dragOffset: CGSize = .zero
     @State private var shortIdleTask: DispatchWorkItem?
+    @State private var isKeyShortcutsShown: Bool = false
     
     @Environment(\.horizontalSizeClass) private var horizontalSizeClass
     @Environment(\.verticalSizeClass) private var verticalSizeClass
@@ -138,10 +139,26 @@ struct VMToolbarView: View {
                     VMToolbarDisplayMenuView(state: $state)
                         .animationUniqueID("display", in: namespace)
                     Button {
+                        // ignore if we are showing shortcuts
+                        guard !isKeyShortcutsShown else {
+                            return
+                        }
                         state.isKeyboardRequested = !state.isKeyboardShown
                     } label: {
                         Label("Keyboard", systemImage: "keyboard")
                     }.animationUniqueID("keyboard", in: namespace)
+                    #if !WITH_REMOTE
+                    .simultaneousGesture(
+                        LongPressGesture().onEnded { _ in
+                            isKeyShortcutsShown.toggle()
+                        }
+                    )
+                    .sheet(isPresented: $isKeyShortcutsShown) {
+                        VMKeyboardShortcutsView { keys in
+                            session.sendKeys(keys: keys)
+                        }
+                    }
+                    #endif
                 }.toolbarButtonStyle(horizontalSizeClass: horizontalSizeClass, verticalSizeClass: verticalSizeClass)
                 .disabled(state.isBusy)
             }

+ 13 - 0
Platform/visionOS/VMToolbarOrnamentModifier.swift

@@ -28,6 +28,7 @@ struct VMToolbarOrnamentModifier: ViewModifier {
     @AppStorage("ToolbarIsCollapsed") private var isCollapsed: Bool = false
     @Environment(\.openWindow) private var openWindow
     @Environment(\.dismissWindow) private var dismissWindow
+    @State private var isKeyShortcutsShown = false
 
     func body(content: Content) -> some View {
         content.ornament(visibility: isCollapsed ? .hidden : .visible, attachmentAnchor: .scene(.top)) {
@@ -112,6 +113,18 @@ struct VMToolbarOrnamentModifier: ViewModifier {
                         handleKeyEvent(keyCode, modifier: modifier, isKeyDown: true)
                     }
                 }
+                #if !WITH_REMOTE
+                .simultaneousGesture(
+                    LongPressGesture().onEnded { _ in
+                        isKeyShortcutsShown.toggle()
+                    }
+                )
+                .sheet(isPresented: $isKeyShortcutsShown) {
+                    VMKeyboardShortcutsView { keys in
+                        session.sendKeys(keys: keys)
+                    }
+                }
+                #endif
                 Divider()
                 Button {
                     isCollapsed = true

+ 0 - 2
Services/UTMKeyboardShortcuts.swift

@@ -15,8 +15,6 @@
 //
 
 import Foundation
-import QEMUKit
-import QEMUKitInternal
 
 final class UTMKeyboardShortcuts {
     static let shared = UTMKeyboardShortcuts()

+ 6 - 4
UTM.xcodeproj/project.pbxproj

@@ -152,7 +152,6 @@
 		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 */; };
@@ -647,7 +646,8 @@
 		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 */; };
+		CE68E5442E3912E0006B3645 /* VMKeyboardShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE68E5422E3912E0006B3645 /* VMKeyboardShortcutsView.swift */; };
+		CE68E5452E3912E0006B3645 /* VMKeyboardShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE68E5422E3912E0006B3645 /* VMKeyboardShortcutsView.swift */; };
 		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 */; };
@@ -1973,6 +1973,7 @@
 		CE611BEA29F50D3E001817BC /* VMReleaseNotesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMReleaseNotesView.swift; sourceTree = "<group>"; };
 		CE612AC524D3B50700FA6300 /* VMDisplayWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMDisplayWindowController.swift; sourceTree = "<group>"; };
 		CE66450C2269313200B0849A /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; };
+		CE68E5422E3912E0006B3645 /* VMKeyboardShortcutsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMKeyboardShortcutsView.swift; sourceTree = "<group>"; };
 		CE6B240A25F1F3CE0020D43E /* main.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = main.c; sourceTree = "<group>"; };
 		CE6B240F25F1F43A0020D43E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		CE6B241025F1F4B30020D43E /* QEMULauncher.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QEMULauncher.entitlements; sourceTree = "<group>"; };
@@ -2485,7 +2486,6 @@
 				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 */,
@@ -2775,6 +2775,7 @@
 				84CF5DD2288DCE6400D01721 /* VMDisplayHostedView.swift */,
 				84018685288A3B5B0050AC51 /* VMSessionState.swift */,
 				CE2D955124AD4F980059923A /* VMDrivesSettingsView.swift */,
+				CE68E5422E3912E0006B3645 /* VMKeyboardShortcutsView.swift */,
 				CE2D954C24AD4F980059923A /* VMSettingsView.swift */,
 				84C60FB62681A41B00B58C00 /* VMToolbarView.swift */,
 				84E6F6FC289319AE00080EEF /* VMToolbarDisplayMenuView.swift */,
@@ -3730,6 +3731,7 @@
 				CE2D92F224AD46670059923A /* VMKeyboardButton.m in Sources */,
 				84B36D2527B704C200C22685 /* UTMDownloadVMTask.swift in Sources */,
 				8432329828C3017F00CFBC97 /* GlobalFileImporter.swift in Sources */,
+				CE68E5442E3912E0006B3645 /* VMKeyboardShortcutsView.swift in Sources */,
 				84CE3DAE2904C17C00FF068B /* IASKAppSettings.swift in Sources */,
 				84C2E8652AA429E800B17308 /* VMWizardContent.swift in Sources */,
 				841E58CB28937EE200137A20 /* UTMExternalSceneDelegate.swift in Sources */,
@@ -4093,6 +4095,7 @@
 				CEA45EA8263519B5002FA97D /* VMDisplayMetalViewController+Gamepad.m in Sources */,
 				848D99C12866D9CE0055C215 /* QEMUArgumentBuilder.swift in Sources */,
 				CEA45EB3263519B5002FA97D /* UTMLogging.m in Sources */,
+				CE68E5452E3912E0006B3645 /* VMKeyboardShortcutsView.swift in Sources */,
 				848D99B928630A780055C215 /* VMConfigSerialView.swift in Sources */,
 				CEA45EB7263519B5002FA97D /* VMToolbarModifier.swift in Sources */,
 				CEA45EB9263519B5002FA97D /* VMCursor.m in Sources */,
@@ -4271,7 +4274,6 @@
 				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 */,