diff --git a/Platform/iOS/VMKeyboardShortcutsView.swift b/Platform/iOS/VMKeyboardShortcutsView.swift new file mode 100644 index 00000000..acabaf1a --- /dev/null +++ b/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 } +} diff --git a/Platform/iOS/VMSessionState.swift b/Platform/iOS/VMSessionState.swift index 4581e231..8f250176 100644 --- a/Platform/iOS/VMSessionState.swift +++ b/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") diff --git a/Platform/iOS/VMToolbarView.swift b/Platform/iOS/VMToolbarView.swift index 7cf7da31..6bc5412a 100644 --- a/Platform/iOS/VMToolbarView.swift +++ b/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) } diff --git a/Platform/visionOS/VMToolbarOrnamentModifier.swift b/Platform/visionOS/VMToolbarOrnamentModifier.swift index 88caf586..ee7c5289 100644 --- a/Platform/visionOS/VMToolbarOrnamentModifier.swift +++ b/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 diff --git a/Services/UTMKeyboardShortcuts.swift b/Services/UTMKeyboardShortcuts.swift index 9744039c..21fb7edd 100644 --- a/Services/UTMKeyboardShortcuts.swift +++ b/Services/UTMKeyboardShortcuts.swift @@ -15,8 +15,6 @@ // import Foundation -import QEMUKit -import QEMUKitInternal final class UTMKeyboardShortcuts { static let shared = UTMKeyboardShortcuts() diff --git a/UTM.xcodeproj/project.pbxproj b/UTM.xcodeproj/project.pbxproj index 511a7bb2..864b4e0c 100644 --- a/UTM.xcodeproj/project.pbxproj +++ b/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 = ""; }; CE612AC524D3B50700FA6300 /* VMDisplayWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMDisplayWindowController.swift; sourceTree = ""; }; 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 = ""; }; CE6B240A25F1F3CE0020D43E /* main.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = main.c; sourceTree = ""; }; CE6B240F25F1F43A0020D43E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; CE6B241025F1F4B30020D43E /* QEMULauncher.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QEMULauncher.entitlements; sourceTree = ""; }; @@ -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 */,