UTM/Platform/macOS/Display/VMMetalView.swift

408 lines
14 KiB
Swift

//
// Copyright © 2020 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 Carbon.HIToolbox
class VMMetalView: MTKView {
weak var inputDelegate: VMMetalViewInputDelegate?
private var wholeTrackingArea: NSTrackingArea?
private var lastModifiers = NSEvent.ModifierFlags()
private var lastKeyDown: Int?
private(set) var isMouseCaptured = false
private(set) var isFirstResponder = false
private(set) var isMouseInWindow = false
/// On ISO keyboards we have to switch `kVK_ISO_Section` and `kVK_ANSI_Grave`
/// from: https://chromium.googlesource.com/chromium/src/+/lkgr/ui/events/keycodes/keyboard_code_conversion_mac.mm
private func convertToCurrentLayout(for keycode: Int) -> Int {
guard KBGetLayoutType(Int16(LMGetKbdType())) == kKeyboardISO else {
return keycode
}
switch keycode {
case kVK_ISO_Section:
return kVK_ANSI_Grave
case kVK_ANSI_Grave:
return kVK_ISO_Section
default:
return keycode
}
}
/// Returns the scan code for the key code in the `event`, or `0` if scan code is unknown.
private func getScanCodeForEvent(_ event: NSEvent) -> Int {
if event.type == .keyDown || event.type == .keyUp {
let keycode = convertToCurrentLayout(for: Int(event.keyCode))
/// see KeyCodeMap file for explaination why the .down scan code is used for both key down and up
return Int(KeyCodeMap.keyCodeToScanCodes[keycode]?.down ?? 0)
} else {
return 0
}
}
override var acceptsFirstResponder: Bool { true }
override func becomeFirstResponder() -> Bool {
isFirstResponder = true
if isMouseInWindow {
NSCursor.tryHide()
}
return super.becomeFirstResponder()
}
override func resignFirstResponder() -> Bool {
isFirstResponder = false
NSCursor.tryUnhide()
if let lastKeyDown = lastKeyDown {
inputDelegate?.keyUp(scanCode: lastKeyDown)
}
if lastModifiers.containsSpecialKeys {
sendModifiers(lastModifiers, press: false)
lastModifiers = []
}
return super.resignFirstResponder()
}
override func updateTrackingAreas() {
let trackingArea = NSTrackingArea(rect: CGRect(origin: .zero, size: frame.size), options: [.mouseMoved, .mouseEnteredAndExited, .activeInKeyWindow], owner: self, userInfo: nil)
logger.debug("update tracking area: \(trackingArea.rect)")
if let oldTrackingArea = wholeTrackingArea {
logger.debug("remove old tracking area: \(oldTrackingArea.rect)")
removeTrackingArea(oldTrackingArea)
NSCursor.tryUnhide()
}
wholeTrackingArea = trackingArea
addTrackingArea(trackingArea)
super.updateTrackingAreas()
}
override func mouseEntered(with event: NSEvent) {
logger.debug("mouse entered (first responder: \(isFirstResponder))")
isMouseInWindow = true
if isFirstResponder {
NSCursor.tryHide()
}
}
override func mouseExited(with event: NSEvent) {
logger.debug("mouse exited")
isMouseInWindow = false
NSCursor.tryUnhide()
}
override func mouseDown(with event: NSEvent) {
logger.trace("mouse down: \(event.buttonNumber)")
inputDelegate?.mouseDown(button: .left)
}
override func rightMouseDown(with event: NSEvent) {
logger.trace("right mouse down: \(event.buttonNumber)")
inputDelegate?.mouseDown(button: .right)
}
override func otherMouseDown(with event: NSEvent) {
logger.trace("other mouse down: \(event.buttonNumber)")
switch event.buttonNumber {
case 2: inputDelegate?.mouseDown(button: .middle)
case 3: inputDelegate?.mouseDown(button: .side)
case 4: inputDelegate?.mouseDown(button: .extra)
default: break
}
}
override func mouseUp(with event: NSEvent) {
logger.trace("mouse up: \(event.buttonNumber)")
inputDelegate?.mouseUp(button: .left)
}
override func rightMouseUp(with event: NSEvent) {
logger.trace("right mouse up: \(event.buttonNumber)")
inputDelegate?.mouseUp(button: .right)
}
override func otherMouseUp(with event: NSEvent) {
logger.trace("other mouse up: \(event.buttonNumber)")
switch event.buttonNumber {
case 2: inputDelegate?.mouseUp(button: .middle)
case 3: inputDelegate?.mouseUp(button: .side)
case 4: inputDelegate?.mouseUp(button: .extra)
default: break
}
}
override func keyDown(with event: NSEvent) {
guard !event.isARepeat else { return }
logger.trace("key down: \(event.keyCode)")
if event.modifierFlags.contains(.numericPad) {
inputDelegate?.didUseNumericPad()
}
lastKeyDown = getScanCodeForEvent(event)
inputDelegate?.keyDown(scanCode: lastKeyDown!)
}
override func keyUp(with event: NSEvent) {
logger.trace("key up: \(event.keyCode)")
lastKeyDown = nil
inputDelegate?.keyUp(scanCode: getScanCodeForEvent(event))
}
override func flagsChanged(with event: NSEvent) {
let modifiers = event.modifierFlags
logger.trace("modifers: \(modifiers)")
if let shouldUseCmdOptForCapture = inputDelegate?.shouldUseCmdOptForCapture {
let captureKeyPressed: Bool
if shouldUseCmdOptForCapture {
captureKeyPressed = modifiers.isSuperset(of: [.command, .option])
} else {
captureKeyPressed = modifiers.isSuperset(of: [.control, .option])
}
if captureKeyPressed {
if isMouseCaptured {
inputDelegate!.releaseMouse()
} else {
inputDelegate!.captureMouse()
}
}
}
sendModifiers(lastModifiers.subtracting(modifiers), press: false)
sendModifiers(modifiers.subtracting(lastModifiers), press: true)
inputDelegate?.syncCapsLock(with: modifiers)
lastModifiers = modifiers
if !isMouseCaptured {
super.flagsChanged(with: event)
}
}
private func sendModifiers(_ modifier: NSEvent.ModifierFlags, press: Bool) {
if modifier.contains(.capsLock) {
let sc = Int(KeyCodeMap.keyCodeToScanCodes[kVK_CapsLock]!.down)
if press {
inputDelegate?.keyDown(scanCode: sc)
} else {
inputDelegate?.keyUp(scanCode: sc)
}
}
if !modifier.isDisjoint(with: [.command, .leftCommand, .rightCommand]) {
let vk = modifier.contains(.rightCommand) ? kVK_RightCommand : kVK_Command
let sc = Int(KeyCodeMap.keyCodeToScanCodes[vk]!.down)
if press {
inputDelegate?.keyDown(scanCode: sc)
} else {
inputDelegate?.keyUp(scanCode: sc)
}
}
if !modifier.isDisjoint(with: [.control, .leftControl, .rightControl]) {
let vk = modifier.contains(.rightControl) ? kVK_RightControl : kVK_Control
let sc = Int(KeyCodeMap.keyCodeToScanCodes[vk]!.down)
if press {
inputDelegate?.keyDown(scanCode: sc)
} else {
inputDelegate?.keyUp(scanCode: sc)
}
}
if modifier.contains(.function) {
let sc = Int(KeyCodeMap.keyCodeToScanCodes[kVK_Function]!.down)
if press {
inputDelegate?.keyDown(scanCode: sc)
} else {
inputDelegate?.keyUp(scanCode: sc)
}
}
if !modifier.isDisjoint(with: [.option, .leftOption, .rightOption]) {
let vk = modifier.contains(.rightOption) ? kVK_RightOption : kVK_Option
let sc = Int(KeyCodeMap.keyCodeToScanCodes[vk]!.down)
if press {
inputDelegate?.keyDown(scanCode: sc)
} else {
inputDelegate?.keyUp(scanCode: sc)
}
}
if !modifier.isDisjoint(with: [.shift, .leftShift, .rightShift]) {
let vk = modifier.contains(.rightShift) ? kVK_RightShift : kVK_Shift
let sc = Int(KeyCodeMap.keyCodeToScanCodes[vk]!.down)
if press {
inputDelegate?.keyDown(scanCode: sc)
} else {
inputDelegate?.keyUp(scanCode: sc)
}
}
}
override func mouseDragged(with event: NSEvent) {
mouseMoved(with: event)
}
override func rightMouseDragged(with event: NSEvent) {
mouseMoved(with: event)
}
override func otherMouseDragged(with event: NSEvent) {
mouseMoved(with: event)
}
override func mouseMoved(with event: NSEvent) {
logger.trace("mouse moved: \(event.deltaX), \(event.deltaY)")
if isMouseCaptured {
inputDelegate?.mouseMove(relativePoint: CGPoint(x: event.deltaX, y: -event.deltaY),
button: NSEvent.pressedMouseButtons.inputButtons())
} else {
let location = event.locationInWindow
let converted = convert(location, from: nil)
inputDelegate?.mouseMove(absolutePoint: converted,
button: NSEvent.pressedMouseButtons.inputButtons())
}
}
override func scrollWheel(with event: NSEvent) {
guard event.deltaY != 0 else { return }
logger.trace("scroll: \(event.deltaY)")
inputDelegate?.mouseScroll(dy: event.deltaY,
button: NSEvent.pressedMouseButtons.inputButtons())
}
}
extension VMMetalView {
private var screenCenter: CGPoint? {
guard let window = self.window else { return nil }
guard let screen = window.screen else { return nil }
let centerView = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
let centerWindow = convert(centerView, to: nil)
var centerScreen = window.convertPoint(toScreen: centerWindow)
let screenHeight = screen.frame.height
centerScreen.y = screenHeight - centerScreen.y
logger.debug("screen \(centerScreen.x), \(centerScreen.y)")
return centerScreen
}
func captureMouse() {
logger.trace("capture cursor")
CGAssociateMouseAndMouseCursorPosition(0)
CGWarpMouseCursorPosition(screenCenter ?? .zero)
isMouseCaptured = true
NSCursor.tryHide()
CGSSetGlobalHotKeyOperatingMode(CGSMainConnectionID(), .disable)
}
func releaseMouse() {
logger.trace("release cursor")
CGAssociateMouseAndMouseCursorPosition(1)
isMouseCaptured = false
if !isMouseInWindow {
NSCursor.tryUnhide()
}
CGSSetGlobalHotKeyOperatingMode(CGSMainConnectionID(), .enable)
}
}
extension VMMetalView: NSAccessibilityGroup {
override func accessibilityRole() -> NSAccessibility.Role? {
.group
}
override func accessibilityRoleDescription() -> String? {
NSLocalizedString("Capture Input", comment: "VMMetalView")
}
override func accessibilityLabel() -> String? {
NSLocalizedString("Virtual Machine", comment: "VMMetalView")
}
override func accessibilityHelp() -> String? {
NSLocalizedString("To capture input or to release the capture, press Command and Option at the same time.", comment: "VMMetalView")
}
override func isAccessibilityElement() -> Bool {
true
}
override func isAccessibilityEnabled() -> Bool {
true
}
}
private extension NSCursor {
private static var isCursorHidden: Bool = false
static func tryHide() {
if !NSCursor.isCursorHidden {
NSCursor.hide()
NSCursor.isCursorHidden = true
}
}
static func tryUnhide() {
if NSCursor.isCursorHidden {
NSCursor.unhide()
NSCursor.isCursorHidden = false
}
}
}
private extension NSEvent.ModifierFlags {
var containsSpecialKeys: Bool {
!self.isDisjoint(with: [.capsLock, .command, .control, .function, .option, .shift])
}
}
private extension Int {
func inputButtons() -> CSInputButton {
var pressed = CSInputButton()
if self & (1 << 0) != 0 {
pressed.formUnion(.left)
}
if self & (1 << 1) != 0 {
pressed.formUnion(.right)
}
if self & (1 << 2) != 0 {
pressed.formUnion(.middle)
}
return pressed
}
}
private extension NSEvent.ModifierFlags {
static var leftCommand: NSEvent.ModifierFlags {
NSEvent.ModifierFlags(rawValue: 0x8)
}
static var rightCommand: NSEvent.ModifierFlags {
NSEvent.ModifierFlags(rawValue: 0x10)
}
static var leftControl: NSEvent.ModifierFlags {
NSEvent.ModifierFlags(rawValue: 0x1)
}
static var rightControl: NSEvent.ModifierFlags {
NSEvent.ModifierFlags(rawValue: 0x2000)
}
static var leftOption: NSEvent.ModifierFlags {
NSEvent.ModifierFlags(rawValue: 0x20)
}
static var rightOption: NSEvent.ModifierFlags {
NSEvent.ModifierFlags(rawValue: 0x40)
}
static var leftShift: NSEvent.ModifierFlags {
NSEvent.ModifierFlags(rawValue: 0x2)
}
static var rightShift: NSEvent.ModifierFlags {
NSEvent.ModifierFlags(rawValue: 0x4)
}
}