Skip to content

Commit dce94a0

Browse files
authored
fix: Ensure the software keyboard doesn't cover focused text boxes (#447)
There's quite a bit more work to do here, but this is is a first-cut of the plumbing required to respond to the on-screen text field and cursor.
1 parent 2e34ad4 commit dce94a0

File tree

4 files changed

+119
-18
lines changed

4 files changed

+119
-18
lines changed

OpoLua/Model/Program.swift

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ protocol ProgramDelegate: AnyObject {
4040
func programDidRequestBackground(_ program: Program)
4141
func programDidRequestTaskList(_ program: Program)
4242
func program(_ program: Program, runApplication applicationIdentifier: ApplicationIdentifier, url: URL) -> Int32?
43+
func program(_ program: Program, didSetCursorPosition cursorPosition: CGPoint?)
4344

4445
}
4546

@@ -124,8 +125,8 @@ class Program {
124125
private var observers: [ProgramLifecycleObserver] = []
125126
weak var delegate: ProgramDelegate?
126127

127-
var title: String
128-
var icon: Icon
128+
var title: String // main
129+
var icon: Icon // main TODO: let?
129130

130131
var rootView: UIView {
131132
return windowServer.rootView
@@ -219,6 +220,7 @@ class Program {
219220
}
220221

221222
func toggleOnScreenKeyboard() {
223+
dispatchPrecondition(condition: .onQueue(.main))
222224
if windowServer.rootView.isFirstResponder {
223225
windowServer.rootView.resignFirstResponder()
224226
} else {
@@ -470,9 +472,21 @@ extension Program: OpoIoHandler {
470472
func printValue(_ val: String) {
471473
print(val)
472474
}
473-
475+
474476
func textEditor(_ info: TextFieldInfo?) {
475-
// print("textEditor: \(String(describing: info))")
477+
if let textFieldInfo = info {
478+
delegate?.program(self,
479+
didSetCursorPosition: CGPoint(x: CGFloat(textFieldInfo.cursorRect.midX),
480+
y: CGFloat(textFieldInfo.cursorRect.midY)))
481+
DispatchQueue.main.sync {
482+
_ = windowServer.rootView.becomeFirstResponder()
483+
}
484+
} else {
485+
delegate?.program(self, didSetCursorPosition: nil)
486+
DispatchQueue.main.sync {
487+
_ = windowServer.rootView.resignFirstResponder()
488+
}
489+
}
476490
}
477491

478492
func beep(frequency: Double, duration: Double) -> Error? {

OpoLua/View Controllers/ProgramViewController.swift

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,8 @@ class ProgramViewController: UIViewController {
243243

244244
return scaleView
245245
}()
246+
247+
var bottomConstraint: NSLayoutConstraint!
246248

247249
init(settings: Settings, taskManager: TaskManager, program: Program) {
248250
self.settings = settings
@@ -251,17 +253,21 @@ class ProgramViewController: UIViewController {
251253
super.init(nibName: nil, bundle: nil)
252254
program.delegate = self
253255
navigationItem.largeTitleDisplayMode = .never
256+
navigationItem.compactAppearance?.configureWithOpaqueBackground()
257+
navigationItem.standardAppearance?.configureWithOpaqueBackground()
254258
view.backgroundColor = UIColor(named: "ProgramBackground")
255259

256260
title = program.title
257261
view.clipsToBounds = true
262+
263+
bottomConstraint = scaleView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
258264

259265
view.addSubview(scaleView)
260266
NSLayoutConstraint.activate([
261267
scaleView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
262268
scaleView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
263269
scaleView.topAnchor.constraint(equalTo: view.topAnchor),
264-
scaleView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
270+
bottomConstraint,
265271
])
266272

267273
if traitCollection.horizontalSizeClass == .compact {
@@ -277,6 +283,7 @@ class ProgramViewController: UIViewController {
277283
}
278284

279285
observeGameControllers()
286+
observeKeyboard()
280287
}
281288

282289
required init?(coder: NSCoder) {
@@ -359,10 +366,34 @@ class ProgramViewController: UIViewController {
359366
notificationCenter.addObserver(forName: NSNotification.Name.GCControllerDidConnect,
360367
object: nil,
361368
queue: .main) { [weak self] notification in
362-
guard let self = self else {
363-
return
364-
}
365-
self.configureControllers()
369+
self?.configureControllers()
370+
}
371+
}
372+
373+
func observeKeyboard() {
374+
dispatchPrecondition(condition: .onQueue(.main))
375+
let notificationCenter = NotificationCenter.default
376+
notificationCenter.addObserver(forName: UIResponder.keyboardWillShowNotification,
377+
object: nil,
378+
queue: .main) { [weak self] notification in
379+
self?.adjustForKeyboard(notification: notification, keyboardShowing: true)
380+
}
381+
notificationCenter.addObserver(forName: UIResponder.keyboardWillHideNotification,
382+
object: nil,
383+
queue: .main) { [weak self] notification in
384+
self?.adjustForKeyboard(notification: notification, keyboardShowing: false)
385+
}
386+
}
387+
388+
func adjustForKeyboard(notification: Notification, keyboardShowing: Bool) {
389+
guard let userInfo = notification.userInfo,
390+
let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else {
391+
return
392+
}
393+
UIView.animate(withDuration: 0.3) {
394+
self.scaleView.isShowingKeyboard = keyboardShowing
395+
self.bottomConstraint.constant = -(keyboardShowing ? keyboardFrame.height : 0)
396+
self.view.layoutIfNeeded()
366397
}
367398
}
368399

@@ -565,7 +596,19 @@ extension ProgramViewController: ProgramDelegate {
565596
return AppDelegate.shared.runApplication(applicationIdentifier, url: url)
566597
}
567598
}
568-
599+
600+
func program(_ program: Program, didSetCursorPosition cursorPosition: CGPoint?) {
601+
DispatchQueue.main.async {
602+
UIView.animate(withDuration: 0.3) {
603+
if let cursorPosition {
604+
self.scaleView.focus = CGPoint(x: 0, y: cursorPosition.y)
605+
} else {
606+
self.scaleView.focus = nil
607+
}
608+
}
609+
}
610+
}
611+
569612
}
570613

571614
extension ProgramViewController: ProgramLifecycleObserver {

OpoLua/Views/AutoOrientView.swift

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@
2121
import UIKit
2222

2323
class AutoOrientView: UIView {
24+
25+
var focus: CGPoint? {
26+
didSet {
27+
layoutSubviews()
28+
}
29+
}
30+
31+
var isShowingKeyboard: Bool = false {
32+
didSet {
33+
layoutSubviews()
34+
}
35+
}
2436

2537
private var contentView: UIView
2638

@@ -62,20 +74,32 @@ class AutoOrientView: UIView {
6274
#else
6375
var transform = CGAffineTransform.identity
6476
var screenSize = contentView.systemLayoutSizeFitting(.zero)
65-
77+
let center = CGPoint(x: screenSize.width / 2, y: screenSize.height / 2)
78+
let focus = focus ?? center
79+
80+
if isShowingKeyboard {
81+
// If the keyboard is visible, never scale the view but instead prioritise the cursor position / focus.
82+
// TODO: Pick sensible scales and defaults based on the focus.
83+
transform = transform.concatenating(CGAffineTransform(translationX: 0, y: -(focus.y - center.y)))
84+
return transform
85+
}
86+
6687
if size.contains(size: screenSize) {
6788
// The screen fits and there's nothing to do.
6889
return transform
6990
}
70-
71-
if size.isPortrait {
91+
92+
// Only rotate the screen on the iPhone.
93+
if (UIDevice.current.userInterfaceIdiom == .phone &&
94+
UIDevice.current.orientation == .portrait &&
95+
!isShowingKeyboard) {
7296
// If our screen size is portrait, then we first apply rotation if the device screen doesn't fit.
7397
// We construct the transform, and then apply that effective transform to the screen size for further
7498
// layout operations.
7599
transform = transform.concatenating(CGAffineTransform(rotationAngle: -.pi / 2))
76100
screenSize = CGSize(width: screenSize.height, height: screenSize.width)
77101
}
78-
102+
79103
if !size.contains(size: screenSize) {
80104
// If the screen still doesn't fit then we need to scale it.
81105
let scale = screenSize.scaleThatFits(in: size)

swift/OpoIoHandler.swift

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,30 @@ public struct Graphics {
6060

6161
public let origin: Point
6262
public let size: Size
63-
public var minX: Int { return origin.x }
64-
public var minY: Int { return origin.y }
65-
public var width: Int { return size.width }
66-
public var height: Int { return size.height }
63+
64+
public var minX: Int {
65+
return origin.x
66+
}
67+
68+
public var minY: Int {
69+
return origin.y
70+
}
71+
72+
public var midX: Float {
73+
return Float(origin.x) + Float(width) / 2
74+
}
75+
76+
public var midY: Float {
77+
return Float(origin.y) + Float(height) / 2
78+
}
79+
80+
public var width: Int {
81+
return size.width
82+
}
83+
84+
public var height: Int {
85+
return size.height
86+
}
6787

6888
public init(origin: Point, size: Size) {
6989
self.origin = origin

0 commit comments

Comments
 (0)