mirror of
https://github.com/ogoguel/activegs-ios.git
synced 2024-06-02 16:41:34 +00:00
472 lines
18 KiB
Swift
472 lines
18 KiB
Swift
//
|
|
// EmulatorKeyboard.swift
|
|
// ActiveGS
|
|
//
|
|
// Created by Yoshi Sugawara on 7/30/20.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
|
|
class KeyboardButton: UIButton {
|
|
let key: KeyCoded
|
|
var toggleState = false
|
|
|
|
// MARK: - Functions
|
|
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
|
let newArea = CGRect(
|
|
x: self.bounds.origin.x - 5.0,
|
|
y: self.bounds.origin.y - 5.0,
|
|
width: self.bounds.size.width + 20.0,
|
|
height: self.bounds.size.height + 20.0
|
|
)
|
|
return newArea.contains(point)
|
|
}
|
|
|
|
override open var isHighlighted: Bool {
|
|
didSet {
|
|
// this is the problem - the isHighlighted sets to false for the touchesEnded callback
|
|
// need to look ask something else
|
|
// let shouldHighlight = isModifierToggle ? toggleState : isHighlighted
|
|
if !isHighlighted && toggleState {
|
|
// don't update the highlught
|
|
} else {
|
|
backgroundColor = isHighlighted ? .white : .black
|
|
}
|
|
}
|
|
}
|
|
|
|
override open var isSelected: Bool {
|
|
didSet {
|
|
let shouldHighlight = key.isModifier ? toggleState : isSelected
|
|
backgroundColor = shouldHighlight ? .red : .black
|
|
}
|
|
}
|
|
|
|
required init(key: KeyCoded) {
|
|
self.key = key
|
|
super.init(frame: .zero)
|
|
}
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
|
|
@objc protocol EmulatorKeyboardKeyPressedDelegate: class {
|
|
func keyDown(_ key: KeyCoded)
|
|
func keyUp(_ key: KeyCoded)
|
|
}
|
|
|
|
@objc protocol EmulatorKeyboardModifierPressedDelegate: class {
|
|
func modifierPressedWithKey(_ key: KeyCoded, enable: Bool)
|
|
func isModifierEnabled(key: KeyCoded) -> Bool
|
|
}
|
|
|
|
class EmulatorKeyboardView: UIView {
|
|
|
|
var viewModel = EmulatorKeyboardViewModel(keys: [[KeyCoded]]()) {
|
|
didSet {
|
|
setupWithModel(viewModel)
|
|
}
|
|
}
|
|
|
|
private lazy var keyRowsStackView: UIStackView = {
|
|
let stackView = UIStackView()
|
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
stackView.axis = .vertical
|
|
stackView.distribution = .equalCentering
|
|
stackView.spacing = 16
|
|
return stackView
|
|
}()
|
|
|
|
private lazy var alternateKeyRowsStackView: UIStackView = {
|
|
let stackView = UIStackView()
|
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
stackView.axis = .vertical
|
|
stackView.distribution = .equalCentering
|
|
stackView.spacing = 16
|
|
return stackView
|
|
}()
|
|
|
|
let dragMeView: UILabel = {
|
|
let label = UILabel(frame: .zero)
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
label.text = "DRAG ME!"
|
|
label.textColor = UIColor.systemRed
|
|
return label
|
|
}()
|
|
|
|
private var pressedKeyLabels = [String: UILabel]()
|
|
|
|
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 = .clear
|
|
layer.borderColor = UIColor.white.cgColor
|
|
layer.borderWidth = 1.0
|
|
layer.cornerRadius = 15.0
|
|
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).isActive = true
|
|
keyRowsStackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor).isActive = true
|
|
addSubview(dragMeView)
|
|
dragMeView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
|
|
dragMeView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0).isActive = true
|
|
}
|
|
|
|
|
|
@objc private func keyPressed(_ sender: KeyboardButton) {
|
|
if viewModel.shouldShowKeyPressedVisualFeedbackForKey(sender.key) {
|
|
let label = UILabel(frame: .zero)
|
|
label.text = sender.titleLabel?.text
|
|
// hmm need to convert frame
|
|
let converted = sender.convert(sender.bounds, to: self)
|
|
print("sender frame: \(sender.frame), bounds: \(sender.bounds), convertedBounds = \(converted)")
|
|
var labelFrame = converted.offsetBy(dx: 0, dy: -60)
|
|
labelFrame = CGRect(x: labelFrame.origin.x, y: labelFrame.origin.y, width: labelFrame.width * 2, height: labelFrame.height * 2)
|
|
label.backgroundColor = .purple
|
|
label.textColor = .green
|
|
label.frame = labelFrame
|
|
addSubview(label)
|
|
pressedKeyLabels[label.text ?? "😭"] = label
|
|
}
|
|
viewModel.keyPressed(sender.key)
|
|
}
|
|
|
|
@objc private func keyReleased(_ sender: KeyboardButton) {
|
|
let title = sender.titleLabel?.text ?? "😭"
|
|
if let label = pressedKeyLabels[title] {
|
|
label.removeFromSuperview()
|
|
pressedKeyLabels.removeValue(forKey: title)
|
|
}
|
|
let modifierState = viewModel.modifierKeyToggleStateForKey(sender.key)
|
|
sender.toggleState = modifierState
|
|
sender.isSelected = modifierState
|
|
viewModel.keyReleased(sender.key)
|
|
}
|
|
|
|
func setupWithModel(_ model: EmulatorKeyboardViewModel) {
|
|
for row in model.keys {
|
|
let keysInRow = createKeyRow(keys: row)
|
|
keyRowsStackView.addArrangedSubview(keysInRow)
|
|
}
|
|
}
|
|
|
|
private func createKey(_ keyCoded: KeyCoded) -> UIButton {
|
|
let testKey = KeyboardButton(key: keyCoded)
|
|
testKey.setTitle(keyCoded.keyLabel, for: .normal)
|
|
testKey.titleLabel?.font = UIFont.systemFont(ofSize: 12.0)
|
|
testKey.setTitleColor(.white, for: .normal)
|
|
testKey.setTitleColor(.black, for: .highlighted)
|
|
testKey.translatesAutoresizingMaskIntoConstraints = false
|
|
testKey.widthAnchor.constraint(equalToConstant: (25 * CGFloat(keyCoded.keySize.rawValue))).isActive = true
|
|
testKey.heightAnchor.constraint(equalToConstant: 25).isActive = true
|
|
testKey.layer.borderWidth = 1.0
|
|
testKey.layer.borderColor = UIColor.white.cgColor
|
|
testKey.addTarget(self, action: #selector(keyPressed(_:)), for: .touchDown)
|
|
testKey.addTarget(self, action: #selector(keyReleased(_:)), for: .touchUpInside)
|
|
testKey.addTarget(self, action: #selector(keyReleased(_:)), for: .touchUpOutside)
|
|
return testKey
|
|
}
|
|
|
|
private func createKeyRow(keys: [KeyCoded]) -> UIStackView {
|
|
let spacer = UIView()
|
|
var subviews: [UIView] = keys.enumerated().map { index, keyCoded -> UIView in
|
|
createKey(keyCoded)
|
|
}
|
|
subviews.append(spacer)
|
|
let stack = UIStackView(arrangedSubviews: subviews)
|
|
stack.axis = .horizontal
|
|
stack.distribution = .fill
|
|
stack.spacing = 8
|
|
return stack
|
|
}
|
|
}
|
|
|
|
// represents a key that has an underlying code that gets sent to the emulator
|
|
@objc enum KeySize: Int {
|
|
case standard = 1, wide, wider
|
|
}
|
|
|
|
@objc protocol KeyCoded: AnyObject {
|
|
var keyLabel: String { get }
|
|
var keyCode: Int { get }
|
|
var keySize: KeySize { get }
|
|
var isModifier: Bool { get }
|
|
}
|
|
|
|
protocol KeyRowsDataSource {
|
|
func keyForPositionAt(_ position: KeyPosition) -> KeyCoded?
|
|
}
|
|
|
|
@objc class AppleIIKey: NSObject, KeyCoded {
|
|
let keyLabel: String
|
|
let keyCode: Int
|
|
let keySize: KeySize
|
|
let isModifier: Bool
|
|
|
|
override var description: String {
|
|
return String(format: "\(keyLabel) (%02X)", keyCode)
|
|
}
|
|
init(label: String, code: Int, keySize: KeySize, isModifier: Bool = false) {
|
|
self.keyLabel = label
|
|
self.keyCode = code
|
|
self.keySize = keySize
|
|
self.isModifier = isModifier
|
|
}
|
|
convenience init(label: String, code: Int) {
|
|
self.init(label: label, code: code, keySize: .standard)
|
|
}
|
|
}
|
|
|
|
struct KeyPosition {
|
|
let row: Int
|
|
let column: Int
|
|
}
|
|
|
|
@objc class EmulatorKeyboardViewModel: NSObject, KeyRowsDataSource {
|
|
var keys = [[KeyCoded]]()
|
|
var alternateKeys: [[KeyCoded]]?
|
|
var modifierState: Int16 = 0
|
|
|
|
@objc weak var delegate: EmulatorKeyboardKeyPressedDelegate?
|
|
@objc weak var modifierDelegate: EmulatorKeyboardModifierPressedDelegate?
|
|
|
|
init(keys: [[KeyCoded]], alternateKeys: [[KeyCoded]]? = nil) {
|
|
self.keys = keys
|
|
self.alternateKeys = alternateKeys
|
|
}
|
|
|
|
func createView() -> EmulatorKeyboardView {
|
|
let view = EmulatorKeyboardView()
|
|
view.viewModel = self
|
|
return view
|
|
}
|
|
|
|
func keyForPositionAt(_ position: KeyPosition) -> KeyCoded? {
|
|
guard position.row < keys.count else {
|
|
return nil
|
|
}
|
|
let row = keys[position.row]
|
|
guard position.column < row.count else {
|
|
return nil
|
|
}
|
|
return row[position.column]
|
|
}
|
|
|
|
func shouldShowKeyPressedVisualFeedbackForKey(_ key: KeyCoded) -> Bool {
|
|
return !key.isModifier
|
|
}
|
|
|
|
func modifierKeyToggleStateForKey(_ key: KeyCoded) -> Bool {
|
|
return key.isModifier && (modifierDelegate?.isModifierEnabled(key: key) ?? false)
|
|
}
|
|
|
|
func keyPressed(_ key: KeyCoded) {
|
|
if key.isModifier {
|
|
let isPressed = modifierDelegate?.isModifierEnabled(key: key) ?? false
|
|
modifierDelegate?.modifierPressedWithKey(key, enable: !isPressed)
|
|
}
|
|
delegate?.keyDown(key)
|
|
}
|
|
|
|
func keyReleased(_ key: KeyCoded) {
|
|
delegate?.keyUp(key)
|
|
|
|
}
|
|
|
|
// KeyCoded can support a shifted key label
|
|
// view can update with shifted key labels?
|
|
// cluster can support alternate keys and view can swap them out?
|
|
}
|
|
|
|
@objc class EmulatorKeyboardController: UIViewController {
|
|
@objc lazy var leftKeyboardView: EmulatorKeyboardView = {
|
|
leftKeyboardModel.createView()
|
|
}()
|
|
@objc lazy var rightKeyboardView: EmulatorKeyboardView = {
|
|
rightKeyboardModel.createView()
|
|
}()
|
|
var keyboardConstraints = [NSLayoutConstraint]()
|
|
|
|
// uses bitwise masks for the state of shift keys, control, open-apple keys, etc
|
|
@objc var modifierState: Int16 = 0
|
|
|
|
@objc let leftKeyboardModel = EmulatorKeyboardViewModel(keys:
|
|
[
|
|
[
|
|
AppleIIKey(label: "q", code: AppleKeyboardKey.KEY_Q.rawValue),
|
|
AppleIIKey(label: "w", code: AppleKeyboardKey.KEY_W.rawValue),
|
|
AppleIIKey(label: "e", code: AppleKeyboardKey.KEY_E.rawValue),
|
|
AppleIIKey(label: "r", code: AppleKeyboardKey.KEY_R.rawValue),
|
|
AppleIIKey(label: "t", code: AppleKeyboardKey.KEY_T.rawValue)
|
|
],
|
|
[
|
|
AppleIIKey(label: "a", code: AppleKeyboardKey.KEY_A.rawValue),
|
|
AppleIIKey(label: "s", code: AppleKeyboardKey.KEY_S.rawValue),
|
|
AppleIIKey(label: "d", code: AppleKeyboardKey.KEY_D.rawValue),
|
|
AppleIIKey(label: "f", code: AppleKeyboardKey.KEY_F.rawValue),
|
|
AppleIIKey(label: "g", code: AppleKeyboardKey.KEY_G.rawValue)
|
|
],
|
|
[
|
|
AppleIIKey(label: "z", code: AppleKeyboardKey.KEY_Z.rawValue),
|
|
AppleIIKey(label: "x", code: AppleKeyboardKey.KEY_X.rawValue),
|
|
AppleIIKey(label: "c", code: AppleKeyboardKey.KEY_C.rawValue),
|
|
AppleIIKey(label: "v", code: AppleKeyboardKey.KEY_V.rawValue),
|
|
AppleIIKey(label: "b", code: AppleKeyboardKey.KEY_B.rawValue)
|
|
],
|
|
[
|
|
AppleIIKey(label: "SHIFT", code: AppleKeyboardKey.KEY_SHIFT.rawValue, keySize: .wide, isModifier: true),
|
|
AppleIIKey(label: "SPACE", code: AppleKeyboardKey.KEY_SPACE.rawValue, keySize: .wider)
|
|
]
|
|
]
|
|
)
|
|
@objc let rightKeyboardModel = EmulatorKeyboardViewModel(keys:
|
|
[
|
|
[
|
|
AppleIIKey(label: "y", code: AppleKeyboardKey.KEY_Y.rawValue),
|
|
AppleIIKey(label: "u", code: AppleKeyboardKey.KEY_U.rawValue),
|
|
AppleIIKey(label: "i", code: AppleKeyboardKey.KEY_I.rawValue),
|
|
AppleIIKey(label: "o", code: AppleKeyboardKey.KEY_O.rawValue),
|
|
AppleIIKey(label: "p", code: AppleKeyboardKey.KEY_P.rawValue)
|
|
],
|
|
[
|
|
AppleIIKey(label: "h", code: AppleKeyboardKey.KEY_H.rawValue),
|
|
AppleIIKey(label: "j", code: AppleKeyboardKey.KEY_J.rawValue),
|
|
AppleIIKey(label: "k", code: AppleKeyboardKey.KEY_K.rawValue),
|
|
AppleIIKey(label: "l", code: AppleKeyboardKey.KEY_L.rawValue),
|
|
AppleIIKey(label: "'", code: AppleKeyboardKey.KEY_SQUOTE.rawValue)
|
|
],
|
|
[
|
|
AppleIIKey(label: "n", code: AppleKeyboardKey.KEY_N.rawValue),
|
|
AppleIIKey(label: "m", code: AppleKeyboardKey.KEY_M.rawValue),
|
|
AppleIIKey(label: ",", code: AppleKeyboardKey.KEY_COMMA.rawValue),
|
|
AppleIIKey(label: ".", code: AppleKeyboardKey.KEY_PERIOD.rawValue),
|
|
AppleIIKey(label: "/", code: AppleKeyboardKey.KEY_FSLASH.rawValue)
|
|
],
|
|
[
|
|
AppleIIKey(label: "RETURN", code: AppleKeyboardKey.KEY_RETURN.rawValue, keySize: .wider)
|
|
]
|
|
]
|
|
)
|
|
var arrowKeysModel = [[AppleIIKey]]()
|
|
|
|
init() {
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
// setupView()
|
|
setupViewFrames()
|
|
// setupKeyModels()
|
|
|
|
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(draggedView(_:)))
|
|
leftKeyboardView.dragMeView.isUserInteractionEnabled = true
|
|
leftKeyboardView.dragMeView.addGestureRecognizer(panGesture)
|
|
let panGestureRightKeyboard = UIPanGestureRecognizer(target: self, action: #selector(draggedView(_:)))
|
|
rightKeyboardView.dragMeView.isUserInteractionEnabled = true
|
|
rightKeyboardView.dragMeView.addGestureRecognizer(panGestureRightKeyboard)
|
|
}
|
|
|
|
func setupView() {
|
|
NSLayoutConstraint.deactivate(keyboardConstraints)
|
|
keyboardConstraints.removeAll()
|
|
leftKeyboardView.translatesAutoresizingMaskIntoConstraints = false
|
|
view.addSubview(leftKeyboardView)
|
|
leftKeyboardView.heightAnchor.constraint(equalToConstant: 200).isActive = true
|
|
leftKeyboardView.widthAnchor.constraint(equalToConstant: 180).isActive = true
|
|
keyboardConstraints.append(contentsOf: [
|
|
leftKeyboardView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
|
leftKeyboardView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
|
|
])
|
|
rightKeyboardView.translatesAutoresizingMaskIntoConstraints = false
|
|
view.addSubview(rightKeyboardView)
|
|
keyboardConstraints.append(contentsOf: [
|
|
rightKeyboardView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
|
rightKeyboardView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
|
])
|
|
rightKeyboardView.heightAnchor.constraint(equalToConstant: 200).isActive = true
|
|
rightKeyboardView.widthAnchor.constraint(equalToConstant: 180).isActive = true
|
|
NSLayoutConstraint.activate(keyboardConstraints)
|
|
}
|
|
|
|
func setupViewFrames() {
|
|
// initial placement on the bottom corners
|
|
// assume a size of 200x180 for now
|
|
// since we don't know the frame of this view yet until layout time,
|
|
// assume it's taking the full screen
|
|
let screenFrame = UIScreen.main.bounds
|
|
let keyboardHeight: CGFloat = 180.0
|
|
let keyboardWidth: CGFloat = 200.0
|
|
let bottomLeftFrame = CGRect(
|
|
x: self.view.safeAreaInsets.left,
|
|
y: screenFrame.size.height - self.view.safeAreaInsets.bottom - keyboardHeight - 20,
|
|
width: keyboardWidth, height: keyboardHeight)
|
|
let bottomRightFrame = CGRect(
|
|
x: screenFrame.size.width - self.view.safeAreaInsets.right - keyboardWidth,
|
|
y:screenFrame.size.height - self.view.safeAreaInsets.bottom - keyboardHeight - 20,
|
|
width: keyboardWidth, height: keyboardHeight
|
|
)
|
|
view.addSubview(leftKeyboardView)
|
|
view.addSubview(rightKeyboardView)
|
|
leftKeyboardView.frame = bottomLeftFrame
|
|
rightKeyboardView.frame = bottomRightFrame
|
|
}
|
|
|
|
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
|
// get relative positions of frames to size
|
|
for v in [leftKeyboardView, rightKeyboardView] {
|
|
let xPercent = v.frame.origin.x / view.frame.size.width
|
|
let yPercent = v.frame.origin.y / view.frame.size.height
|
|
var newX = size.width * xPercent
|
|
var newY = size.height * yPercent
|
|
// mmm need to check if the views fit within the frame and adjust
|
|
if newX + v.bounds.size.width > size.width {
|
|
newX = size.width - v.bounds.size.width
|
|
} else if newX < 0 {
|
|
newX = 0
|
|
}
|
|
if newY + v.bounds.size.height > size.height {
|
|
newY = size.height - v.bounds.size.height
|
|
}
|
|
let newFrame = CGRect(x: newX, y: newY, width: v.bounds.size.width, height: v.bounds.size.height)
|
|
v.frame = newFrame
|
|
}
|
|
}
|
|
|
|
func setupKeyModels() {
|
|
leftKeyboardView.setupWithModel(leftKeyboardModel)
|
|
rightKeyboardView.setupWithModel(rightKeyboardModel)
|
|
}
|
|
|
|
@objc func draggedView(_ sender:UIPanGestureRecognizer){
|
|
guard let keyboardView = sender.view?.superview else {
|
|
return
|
|
}
|
|
// NSLayoutConstraint.deactivate(keyboardConstraints)
|
|
// self.view.bringSubviewToFront(keyboardView)
|
|
let translation = sender.translation(in: self.view)
|
|
keyboardView.center = CGPoint(x: keyboardView.center.x + translation.x, y: keyboardView.center.y + translation.y)
|
|
sender.setTranslation(CGPoint.zero, in: self.view)
|
|
}
|
|
}
|