RetroArch/pkg/apple/OnScreenKeyboard/EmulatorKeyboardView.swift
yoshisuga 01cb10d8b3
[iOS 13+] Support a toolbar that allows toggling of onscreen keyboard and touch mouse (#13700)
* Fetch translations from Crowdin

* Support for showing and hiding indicator and navigation bar

* Refactored to use a view model

* Support defining helper bar items and support showing/hiding keyboard

* reorganized source files into separate logical files

* Moved mouse support to swift (except for delegate implementation), added support for enabling touch mouse in helper bar; reorganized swift source files

* Reorganized keyboard files; added the touch mouse messages to the RA localization files; use the RA notification system

* change keyboard letters to uppercase for clarity

Co-authored-by: github-actions <github-actions@github.com>
2022-03-07 19:09:49 +01:00

284 lines
11 KiB
Swift

//
// EmulatorKeyboard.swift
//
// Created by Yoshi Sugawara on 7/30/20.
//
// TODO: shift key should change the label of the keys to uppercase (need callback mechanism?)
// pan gesture to outer edges of keyboard view for better dragging
@objc protocol EmulatorKeyboardKeyPressedDelegate: AnyObject {
func keyDown(_ key: KeyCoded)
func keyUp(_ key: KeyCoded)
}
@objc protocol EmulatorKeyboardModifierPressedDelegate: AnyObject {
func modifierPressedWithKey(_ key: KeyCoded, enable: Bool)
func isModifierEnabled(key: KeyCoded) -> Bool
}
protocol EmulatorKeyboardViewDelegate: AnyObject {
func toggleAlternateKeys()
func refreshModifierStates()
func updateTransparency(toAlpha alpha: Float)
}
class EmulatorKeyboardView: UIView {
static var keyboardBackgroundColor = UIColor.systemGray6.withAlphaComponent(0.5)
static var keyboardCornerRadius = 6.0
static var keyboardDragColor = UIColor.systemGray
static var keyCornerRadius = 6.0
static var keyBorderWidth = 1.0
static var rowSpacing = 12.0
static var keySpacing = 8.0
static var keyNormalFont = UIFont.systemFont(ofSize: 12)
static var keyPressedFont = UIFont.boldSystemFont(ofSize: 24)
static var keyNormalBackgroundColor = UIColor.systemGray4.withAlphaComponent(0.5)
static var keyNormalBorderColor = keyNormalBackgroundColor
static var keyNormalTextColor = UIColor.label
static var keyPressedBackgroundColor = UIColor.systemGray2
static var keyPressedBorderColor = keyPressedBackgroundColor
static var keyPressedTextColor = UIColor.label
static var keySelectedBackgroundColor = UIColor.systemGray2.withAlphaComponent(0.8)
static var keySelectedBorderColor = keySelectedBackgroundColor
static var keySelectedTextColor = UIColor.label
var viewModel = EmulatorKeyboardViewModel(keys: [[KeyCoded]]()) {
didSet {
setupWithModel(viewModel)
}
}
var modifierButtons = Set<EmulatorKeyboardButton>()
weak var delegate: EmulatorKeyboardViewDelegate?
private lazy var keyRowsStackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.distribution = .equalCentering
stackView.spacing = Self.rowSpacing
return stackView
}()
private lazy var alternateKeyRowsStackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.distribution = .equalCentering
stackView.spacing = Self.rowSpacing
stackView.isHidden = true
return stackView
}()
let dragMeView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = EmulatorKeyboardView.keyboardDragColor
view.translatesAutoresizingMaskIntoConstraints = false
view.widthAnchor.constraint(equalToConstant: 80).isActive = true
view.heightAnchor.constraint(equalToConstant: 2).isActive = true
let outerView = UIView(frame: .zero)
outerView.backgroundColor = .clear
outerView.translatesAutoresizingMaskIntoConstraints = false
outerView.addSubview(view)
view.centerXAnchor.constraint(equalTo: outerView.centerXAnchor).isActive = true
view.centerYAnchor.constraint(equalTo: outerView.centerYAnchor).isActive = true
outerView.heightAnchor.constraint(equalToConstant: 20).isActive = true
outerView.widthAnchor.constraint(equalToConstant: 100).isActive = true
return outerView
}()
private var pressedKeyViews = [UIControl: UIView]()
convenience init() {
self.init(frame: CGRect.zero)
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
backgroundColor = Self.keyboardBackgroundColor
layer.cornerRadius = Self.keyboardCornerRadius
layoutMargins = UIEdgeInsets(top: 16, left: 4, bottom: 16, right: 4)
insetsLayoutMarginsFromSafeArea = false
addSubview(keyRowsStackView)
keyRowsStackView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor).isActive = true
keyRowsStackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor, constant: 4.0).isActive = true
keyRowsStackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor, constant: -4.0).isActive = true
addSubview(alternateKeyRowsStackView)
alternateKeyRowsStackView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor).isActive = true
alternateKeyRowsStackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor, constant: 4.0).isActive = true
alternateKeyRowsStackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor, constant: -4.0).isActive = true
addSubview(dragMeView)
dragMeView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
dragMeView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
@objc private func keyPressed(_ sender: EmulatorKeyboardButton) {
if sender.key.keyCode == 9000 { // hack for now
return
}
if !sender.key.isModifier {
// make a "stand-in" for our key, and scale up key
let view = UIView()
view.backgroundColor = EmulatorKeyboardView.keyPressedBackgroundColor
view.layer.cornerRadius = EmulatorKeyboardView.keyCornerRadius
view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
view.frame = sender.convert(sender.bounds, to: self)
addSubview(view)
var tx = 0.0
let ty = sender.bounds.height * -1.20
if let window = self.window {
let rect = sender.convert(sender.bounds, to:window)
if rect.maxX > window.bounds.width * 0.9 {
tx = sender.bounds.width * -0.5
}
if rect.minX < window.bounds.width * 0.1 {
tx = sender.bounds.width * 0.5
}
}
sender.superview!.bringSubviewToFront(sender)
sender.transform = CGAffineTransform(translationX:tx, y:ty).scaledBy(x:2, y:2)
pressedKeyViews[sender] = view
}
viewModel.keyPressed(sender.key)
}
@objc private func keyCancelled(_ sender: EmulatorKeyboardButton) {
sender.transform = .identity
if let view = pressedKeyViews[sender] {
view.removeFromSuperview()
pressedKeyViews.removeValue(forKey: sender)
}
}
@objc private func keyReleased(_ sender: EmulatorKeyboardButton) {
sender.transform = .identity
if sender.key.keyCode == 9000 {
delegate?.toggleAlternateKeys()
return
}
if let view = pressedKeyViews[sender] {
view.removeFromSuperview()
pressedKeyViews.removeValue(forKey: sender)
}
sender.isSelected = viewModel.modifierKeyToggleStateForKey(sender.key)
viewModel.keyReleased(sender.key)
self.delegate?.refreshModifierStates()
}
func setupWithModel(_ model: EmulatorKeyboardViewModel) {
for row in model.keys {
let keysInRow = createKeyRow(keys: row)
keyRowsStackView.addArrangedSubview(keysInRow)
}
if let altKeys = model.alternateKeys {
for row in altKeys {
let keysInRow = createKeyRow(keys: row)
alternateKeyRowsStackView.addArrangedSubview(keysInRow)
}
}
if !model.isDraggable {
dragMeView.isHidden = true
}
}
func toggleKeysStackView() {
if viewModel.alternateKeys != nil {
keyRowsStackView.isHidden.toggle()
alternateKeyRowsStackView.isHidden.toggle()
refreshModifierStates()
}
}
func refreshModifierStates() {
modifierButtons.forEach{ button in
button.isSelected = viewModel.modifierKeyToggleStateForKey(button.key)
}
}
private func createKey(_ keyCoded: KeyCoded) -> UIButton {
let key = EmulatorKeyboardButton(key: keyCoded)
if let imageName = keyCoded.keyImageName {
key.tintColor = EmulatorKeyboardView.keyNormalTextColor
key.setImage(UIImage(systemName: imageName), for: .normal)
if let highlightedImageName = keyCoded.keyImageNameHighlighted {
key.setImage(UIImage(systemName: highlightedImageName), for: .highlighted)
key.setImage(UIImage(systemName: highlightedImageName), for: .selected)
}
} else {
key.setTitle(keyCoded.keyLabel, for: .normal)
key.titleLabel?.font = EmulatorKeyboardView.keyNormalFont
key.setTitleColor(EmulatorKeyboardView.keyNormalTextColor, for: .normal)
key.setTitleColor(EmulatorKeyboardView.keySelectedTextColor, for: .selected)
key.setTitleColor(EmulatorKeyboardView.keyPressedTextColor, for: .highlighted)
}
key.translatesAutoresizingMaskIntoConstraints = false
key.widthAnchor.constraint(equalToConstant: (25 * CGFloat(keyCoded.keySize.rawValue))).isActive = true
key.heightAnchor.constraint(equalToConstant: 35).isActive = true
key.backgroundColor = EmulatorKeyboardView.keyNormalBackgroundColor
key.layer.borderWidth = EmulatorKeyboardView.keyBorderWidth
key.layer.borderColor = EmulatorKeyboardView.keyNormalBorderColor.cgColor
key.layer.cornerRadius = EmulatorKeyboardView.keyCornerRadius
key.addTarget(self, action: #selector(keyPressed(_:)), for: .touchDown)
key.addTarget(self, action: #selector(keyReleased(_:)), for: .touchUpInside)
key.addTarget(self, action: #selector(keyReleased(_:)), for: .touchUpOutside)
key.addTarget(self, action: #selector(keyCancelled(_:)), for: .touchCancel)
if keyCoded.isModifier {
modifierButtons.update(with: key)
}
return key
}
private func createKeyRow(keys: [KeyCoded]) -> UIStackView {
let subviews: [UIView] = keys.enumerated().map { index, keyCoded -> UIView in
if keyCoded is SpacerKey {
let spacer = UIView()
spacer.widthAnchor.constraint(equalToConstant: 25.0 * CGFloat(keyCoded.keySize.rawValue)).isActive = true
spacer.heightAnchor.constraint(equalToConstant: 25.0).isActive = true
return spacer
} else if let sliderKey = keyCoded as? SliderKey {
sliderKey.keyboardView = self
return sliderKey.createView()
}
return createKey(keyCoded)
}
let stack = UIStackView(arrangedSubviews: subviews)
stack.axis = .horizontal
stack.distribution = .fill
stack.spacing = 8
return stack
}
}
extension UIImage {
static func dot(size:CGSize, color:UIColor) -> UIImage {
return UIGraphicsImageRenderer(size: size).image { context in
context.cgContext.setFillColor(color.cgColor)
context.cgContext.fillEllipse(in: CGRect(origin:.zero, size:size))
}
}
}