Skip to content

Commit c02cddc

Browse files
committed
display(iOS): add keyboard shortcuts
1 parent 696772a commit c02cddc

File tree

6 files changed

+164
-6
lines changed

6 files changed

+164
-6
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
//
2+
// Copyright © 2025 osy. All rights reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
import SwiftUI
18+
19+
struct VMKeyboardShortcutsView: View {
20+
let onShortcut: ([QEMUKeyCode]) -> Void
21+
@Environment(\.presentationMode) var presentationMode
22+
@State private var keyboardShortcuts: [[QEMUKeyCode]] = []
23+
24+
var body: some View {
25+
NavigationView {
26+
List {
27+
ForEach(keyboardShortcuts, id: \.self) { element in
28+
Button(element.title) {
29+
onShortcut(element)
30+
presentationMode.wrappedValue.dismiss()
31+
}
32+
}.onDelete { indexSet in
33+
keyboardShortcuts.remove(atOffsets: indexSet)
34+
}.onMove { indexSet, offset in
35+
keyboardShortcuts.move(fromOffsets: indexSet, toOffset: offset)
36+
}
37+
NavigationLink("Add…") {
38+
NewKeyboardShortcutView(keyboardShortcuts: $keyboardShortcuts)
39+
}
40+
}.navigationTitle("Keyboard Shortcut")
41+
.toolbar {
42+
ToolbarItemGroup(placement: .navigationBarTrailing) {
43+
EditButton()
44+
Button("Close") {
45+
presentationMode.wrappedValue.dismiss()
46+
}
47+
}
48+
}
49+
}
50+
.onAppear {
51+
keyboardShortcuts = UTMKeyboardShortcuts.shared.loadKeyboardShortcuts()
52+
}
53+
.onChange(of: keyboardShortcuts) { newValue in
54+
UTMKeyboardShortcuts.shared.saveKeyboardShortcuts(newValue)
55+
}
56+
}
57+
}
58+
59+
private struct NewKeyboardShortcutView: View {
60+
@Environment(\.presentationMode) var presentationMode
61+
@Binding var keyboardShortcuts: [[QEMUKeyCode]]
62+
@State private var newShortcut: [QEMUKeyCode] = []
63+
@State private var newKey: QEMUKeyCode?
64+
65+
var body: some View {
66+
List {
67+
DetailedSection("Keys") {
68+
ForEach(newShortcut, id: \.self) { element in
69+
Text(element.title)
70+
}.onDelete { indexSet in
71+
newShortcut.remove(atOffsets: indexSet)
72+
}.onMove { indexSet, offset in
73+
newShortcut.move(fromOffsets: indexSet, toOffset: offset)
74+
}
75+
}
76+
DetailedSection("New Key") {
77+
Picker("", selection: $newKey) {
78+
Text("").tag(nil as QEMUKeyCode?)
79+
ForEach(QEMUKeyCode.allCases) { keyCode in
80+
if !newShortcut.contains(keyCode) {
81+
Text(keyCode.title).tag(keyCode)
82+
}
83+
}
84+
}.pickerStyle(.wheel)
85+
Button("Add") {
86+
if let key = newKey {
87+
newShortcut.append(key)
88+
}
89+
newKey = nil
90+
}.disabled(newKey == nil)
91+
}
92+
}.navigationTitle("New Keyboard Shortcut")
93+
.toolbar {
94+
ToolbarItemGroup(placement: .navigationBarTrailing) {
95+
EditButton()
96+
Button("Save") {
97+
if !newShortcut.isEmpty {
98+
keyboardShortcuts.append(newShortcut)
99+
}
100+
presentationMode.wrappedValue.dismiss()
101+
}.disabled(newShortcut.isEmpty)
102+
}
103+
}
104+
.onAppear {
105+
newShortcut = []
106+
newKey = nil
107+
}
108+
}
109+
}
110+
111+
#Preview {
112+
VMKeyboardShortcutsView() { _ in }
113+
}

Platform/iOS/VMSessionState.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,21 @@ extension VMSessionState {
517517
vm.requestVmReset()
518518
}
519519

520+
#if !WITH_REMOTE
521+
func sendKeys(keys: [QEMUKeyCode]) {
522+
Task {
523+
guard let monitor = await (vm as? UTMQemuVirtualMachine)?.monitor else {
524+
return
525+
}
526+
do {
527+
try await monitor.sendKeys(keys)
528+
} catch {
529+
self.nonfatalError = error.localizedDescription
530+
}
531+
}
532+
}
533+
#endif
534+
520535
func didReceiveMemoryWarning() {
521536
let shouldAutosave = UserDefaults.standard.bool(forKey: "AutosaveLowMemory")
522537

Platform/iOS/VMToolbarView.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ struct VMToolbarView: View {
2525
@State private var isIdle: Bool = false
2626
@State private var dragOffset: CGSize = .zero
2727
@State private var shortIdleTask: DispatchWorkItem?
28+
@State private var isKeyShortcutsShown: Bool = false
2829

2930
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
3031
@Environment(\.verticalSizeClass) private var verticalSizeClass
@@ -138,10 +139,26 @@ struct VMToolbarView: View {
138139
VMToolbarDisplayMenuView(state: $state)
139140
.animationUniqueID("display", in: namespace)
140141
Button {
142+
// ignore if we are showing shortcuts
143+
guard !isKeyShortcutsShown else {
144+
return
145+
}
141146
state.isKeyboardRequested = !state.isKeyboardShown
142147
} label: {
143148
Label("Keyboard", systemImage: "keyboard")
144149
}.animationUniqueID("keyboard", in: namespace)
150+
#if !WITH_REMOTE
151+
.simultaneousGesture(
152+
LongPressGesture().onEnded { _ in
153+
isKeyShortcutsShown.toggle()
154+
}
155+
)
156+
.sheet(isPresented: $isKeyShortcutsShown) {
157+
VMKeyboardShortcutsView { keys in
158+
session.sendKeys(keys: keys)
159+
}
160+
}
161+
#endif
145162
}.toolbarButtonStyle(horizontalSizeClass: horizontalSizeClass, verticalSizeClass: verticalSizeClass)
146163
.disabled(state.isBusy)
147164
}

Platform/visionOS/VMToolbarOrnamentModifier.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ struct VMToolbarOrnamentModifier: ViewModifier {
2828
@AppStorage("ToolbarIsCollapsed") private var isCollapsed: Bool = false
2929
@Environment(\.openWindow) private var openWindow
3030
@Environment(\.dismissWindow) private var dismissWindow
31+
@State private var isKeyShortcutsShown = false
3132

3233
func body(content: Content) -> some View {
3334
content.ornament(visibility: isCollapsed ? .hidden : .visible, attachmentAnchor: .scene(.top)) {
@@ -112,6 +113,18 @@ struct VMToolbarOrnamentModifier: ViewModifier {
112113
handleKeyEvent(keyCode, modifier: modifier, isKeyDown: true)
113114
}
114115
}
116+
#if !WITH_REMOTE
117+
.simultaneousGesture(
118+
LongPressGesture().onEnded { _ in
119+
isKeyShortcutsShown.toggle()
120+
}
121+
)
122+
.sheet(isPresented: $isKeyShortcutsShown) {
123+
VMKeyboardShortcutsView { keys in
124+
session.sendKeys(keys: keys)
125+
}
126+
}
127+
#endif
115128
Divider()
116129
Button {
117130
isCollapsed = true

Services/UTMKeyboardShortcuts.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
//
1616

1717
import Foundation
18-
import QEMUKit
19-
import QEMUKitInternal
2018

2119
final class UTMKeyboardShortcuts {
2220
static let shared = UTMKeyboardShortcuts()

UTM.xcodeproj/project.pbxproj

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,6 @@
152152
846F8D582E3850620037162B /* VMKeyboardShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846F8D572E3850620037162B /* VMKeyboardShortcutsView.swift */; };
153153
846F8D5A2E3891FE0037162B /* UTMKeyboardShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846F8D592E3891FE0037162B /* UTMKeyboardShortcuts.swift */; };
154154
846F8D5B2E3891FE0037162B /* UTMKeyboardShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846F8D592E3891FE0037162B /* UTMKeyboardShortcuts.swift */; };
155-
846F8D5C2E3891FE0037162B /* UTMKeyboardShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846F8D592E3891FE0037162B /* UTMKeyboardShortcuts.swift */; };
156155
846F8D5D2E3891FE0037162B /* UTMKeyboardShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846F8D592E3891FE0037162B /* UTMKeyboardShortcuts.swift */; };
157156
8471770627CC974F00D3A50B /* DefaultTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471770527CC974F00D3A50B /* DefaultTextField.swift */; };
158157
8471770727CC974F00D3A50B /* DefaultTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471770527CC974F00D3A50B /* DefaultTextField.swift */; };
@@ -647,7 +646,8 @@
647646
CE612AC624D3B50700FA6300 /* VMDisplayWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE612AC524D3B50700FA6300 /* VMDisplayWindowController.swift */; };
648647
CE65BABF26A4D8DD0001BD6B /* VMConfigDisplayConsoleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8401FDA5269D44E400265F0D /* VMConfigDisplayConsoleView.swift */; };
649648
CE65BAC026A4D8DE0001BD6B /* VMConfigDisplayConsoleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8401FDA5269D44E400265F0D /* VMConfigDisplayConsoleView.swift */; };
650-
CE68E5412E38A278006B3645 /* QEMUKit in Frameworks */ = {isa = PBXBuildFile; productRef = CE68E5402E38A278006B3645 /* QEMUKit */; };
649+
CE68E5442E3912E0006B3645 /* VMKeyboardShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE68E5422E3912E0006B3645 /* VMKeyboardShortcutsView.swift */; };
650+
CE68E5452E3912E0006B3645 /* VMKeyboardShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE68E5422E3912E0006B3645 /* VMKeyboardShortcutsView.swift */; };
651651
CE6C13CA2B63610C003B7032 /* UTMRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6C13C92B63610C003B7032 /* UTMRemoteMessage.swift */; };
652652
CE6C13CB2B63610C003B7032 /* UTMRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6C13C92B63610C003B7032 /* UTMRemoteMessage.swift */; };
653653
CE6D21DC2553A6ED001D29C5 /* VMConfirmActionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6D21DB2553A6ED001D29C5 /* VMConfirmActionModifier.swift */; };
@@ -1973,6 +1973,7 @@
19731973
CE611BEA29F50D3E001817BC /* VMReleaseNotesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMReleaseNotesView.swift; sourceTree = "<group>"; };
19741974
CE612AC524D3B50700FA6300 /* VMDisplayWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMDisplayWindowController.swift; sourceTree = "<group>"; };
19751975
CE66450C2269313200B0849A /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; };
1976+
CE68E5422E3912E0006B3645 /* VMKeyboardShortcutsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMKeyboardShortcutsView.swift; sourceTree = "<group>"; };
19761977
CE6B240A25F1F3CE0020D43E /* main.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = main.c; sourceTree = "<group>"; };
19771978
CE6B240F25F1F43A0020D43E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
19781979
CE6B241025F1F4B30020D43E /* QEMULauncher.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QEMULauncher.entitlements; sourceTree = "<group>"; };
@@ -2485,7 +2486,6 @@
24852486
CEF7F65D2AEEDCC400E34952 /* gstcontroller-1.0.0.framework in Frameworks */,
24862487
CEF7F65E2AEEDCC400E34952 /* gstaudio-1.0.0.framework in Frameworks */,
24872488
CEF7F65F2AEEDCC400E34952 /* gpg-error.0.framework in Frameworks */,
2488-
CE68E5412E38A278006B3645 /* QEMUKit in Frameworks */,
24892489
CEF7F6602AEEDCC400E34952 /* gcrypt.20.framework in Frameworks */,
24902490
CEF7F6612AEEDCC400E34952 /* InAppSettingsKit in Frameworks */,
24912491
CEF7F6622AEEDCC400E34952 /* gobject-2.0.0.framework in Frameworks */,
@@ -2775,6 +2775,7 @@
27752775
84CF5DD2288DCE6400D01721 /* VMDisplayHostedView.swift */,
27762776
84018685288A3B5B0050AC51 /* VMSessionState.swift */,
27772777
CE2D955124AD4F980059923A /* VMDrivesSettingsView.swift */,
2778+
CE68E5422E3912E0006B3645 /* VMKeyboardShortcutsView.swift */,
27782779
CE2D954C24AD4F980059923A /* VMSettingsView.swift */,
27792780
84C60FB62681A41B00B58C00 /* VMToolbarView.swift */,
27802781
84E6F6FC289319AE00080EEF /* VMToolbarDisplayMenuView.swift */,
@@ -3730,6 +3731,7 @@
37303731
CE2D92F224AD46670059923A /* VMKeyboardButton.m in Sources */,
37313732
84B36D2527B704C200C22685 /* UTMDownloadVMTask.swift in Sources */,
37323733
8432329828C3017F00CFBC97 /* GlobalFileImporter.swift in Sources */,
3734+
CE68E5442E3912E0006B3645 /* VMKeyboardShortcutsView.swift in Sources */,
37333735
84CE3DAE2904C17C00FF068B /* IASKAppSettings.swift in Sources */,
37343736
84C2E8652AA429E800B17308 /* VMWizardContent.swift in Sources */,
37353737
841E58CB28937EE200137A20 /* UTMExternalSceneDelegate.swift in Sources */,
@@ -4093,6 +4095,7 @@
40934095
CEA45EA8263519B5002FA97D /* VMDisplayMetalViewController+Gamepad.m in Sources */,
40944096
848D99C12866D9CE0055C215 /* QEMUArgumentBuilder.swift in Sources */,
40954097
CEA45EB3263519B5002FA97D /* UTMLogging.m in Sources */,
4098+
CE68E5452E3912E0006B3645 /* VMKeyboardShortcutsView.swift in Sources */,
40964099
848D99B928630A780055C215 /* VMConfigSerialView.swift in Sources */,
40974100
CEA45EB7263519B5002FA97D /* VMToolbarModifier.swift in Sources */,
40984101
CEA45EB9263519B5002FA97D /* VMCursor.m in Sources */,
@@ -4271,7 +4274,6 @@
42714274
CEF7F5EA2AEEDCC400E34952 /* VMConfigConstantPicker.swift in Sources */,
42724275
03FA9C742B9BBDB000C53A5A /* UTMConfigurationHostNetwork.swift in Sources */,
42734276
CEF7F5EC2AEEDCC400E34952 /* VMToolbarModifier.swift in Sources */,
4274-
846F8D5C2E3891FE0037162B /* UTMKeyboardShortcuts.swift in Sources */,
42754277
CEF7F5ED2AEEDCC400E34952 /* VMCursor.m in Sources */,
42764278
CEF7F5EE2AEEDCC400E34952 /* VMConfigDriveDetailsView.swift in Sources */,
42774279
CEF7F5F02AEEDCC400E34952 /* NumberTextField.swift in Sources */,

0 commit comments

Comments
 (0)