display(iOS): add keyboard shortcuts

This commit is contained in:
osy
2025-07-29 18:03:45 -07:00
parent 696772ac56
commit c02cddc580
6 changed files with 164 additions and 6 deletions
+113
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
@@ -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
-2
View File
@@ -15,8 +15,6 @@
//
import Foundation
import QEMUKit
import QEMUKitInternal
final class UTMKeyboardShortcuts {
static let shared = UTMKeyboardShortcuts()
+6 -4
View File
@@ -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 */,