Make the keyboard button actually present a keyboard
The toolbar's keyboard icon used to toggle a custom function-key bar — it never opened the iOS system keyboard, so users couldn't type into the remote. Fix: FramebufferUIView now conforms to UIKeyInput + UITextInputTraits, so becoming first responder presents the iOS keyboard. Tapping the keyboard button toggles software-keyboard visibility on the framebuffer view via a SwiftUI binding. While the keyboard is up, an inputAccessoryView toolbar (esc / tab / ctrl / ⌘ / ⌥ / ←↓↑→ / dismiss) sits directly above it, with each button forwarded to the existing controller.send… APIs. The standalone SoftKeyboardBar overlay is no longer used. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,11 @@ final class FramebufferUIView: UIView,
|
||||
// Indirect pointer (trackpad/mouse via UIPointerInteraction)
|
||||
private var indirectPointerNormalized: CGPoint?
|
||||
|
||||
// On-screen keyboard handling
|
||||
private var keyboardActive = false
|
||||
var onKeyboardDismissed: (() -> Void)?
|
||||
private lazy var functionAccessoryView: UIView = makeFunctionAccessoryView()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
isOpaque = true
|
||||
@@ -69,6 +74,32 @@ final class FramebufferUIView: UIView,
|
||||
if window != nil { _ = becomeFirstResponder() }
|
||||
}
|
||||
|
||||
override var inputAccessoryView: UIView? {
|
||||
keyboardActive ? functionAccessoryView : nil
|
||||
}
|
||||
|
||||
/// Show or hide the iOS on-screen keyboard.
|
||||
func setSoftwareKeyboardVisible(_ visible: Bool) {
|
||||
guard visible != keyboardActive else { return }
|
||||
keyboardActive = visible
|
||||
// Force inputAccessoryView re-read by toggling first responder
|
||||
_ = resignFirstResponder()
|
||||
_ = becomeFirstResponder()
|
||||
if visible {
|
||||
reloadInputViews()
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
override func resignFirstResponder() -> Bool {
|
||||
let result = super.resignFirstResponder()
|
||||
if keyboardActive && result {
|
||||
keyboardActive = false
|
||||
onKeyboardDismissed?()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: Layout / image
|
||||
|
||||
override func layoutSubviews() {
|
||||
@@ -468,5 +499,106 @@ final class FramebufferUIView: UIView,
|
||||
for sym in modifiers.reversed() { controller.keyUp(keysym: sym) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Software keyboard accessory
|
||||
|
||||
private func makeFunctionAccessoryView() -> UIView {
|
||||
let bar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 44))
|
||||
bar.barStyle = .black
|
||||
bar.tintColor = .white
|
||||
bar.isTranslucent = true
|
||||
|
||||
func barButton(title: String?, image: String?, action: Selector) -> UIBarButtonItem {
|
||||
if let title {
|
||||
return UIBarButtonItem(title: title, style: .plain, target: self, action: action)
|
||||
}
|
||||
return UIBarButtonItem(image: UIImage(systemName: image ?? ""),
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: action)
|
||||
}
|
||||
|
||||
let esc = barButton(title: "esc", image: nil, action: #selector(accessoryEsc))
|
||||
let tab = barButton(title: "tab", image: nil, action: #selector(accessoryTab))
|
||||
let ctrl = barButton(title: "ctrl", image: nil, action: #selector(accessoryControl))
|
||||
let cmd = barButton(title: "⌘", image: nil, action: #selector(accessoryCommand))
|
||||
let opt = barButton(title: "⌥", image: nil, action: #selector(accessoryOption))
|
||||
let left = barButton(title: nil, image: "arrow.left", action: #selector(accessoryLeft))
|
||||
let down = barButton(title: nil, image: "arrow.down", action: #selector(accessoryDown))
|
||||
let up = barButton(title: nil, image: "arrow.up", action: #selector(accessoryUp))
|
||||
let right = barButton(title: nil, image: "arrow.right", action: #selector(accessoryRight))
|
||||
let spacer = UIBarButtonItem.flexibleSpace()
|
||||
let dismiss = barButton(title: nil,
|
||||
image: "keyboard.chevron.compact.down",
|
||||
action: #selector(accessoryDismiss))
|
||||
bar.items = [esc, tab, ctrl, cmd, opt, spacer, left, down, up, right, spacer, dismiss]
|
||||
return bar
|
||||
}
|
||||
|
||||
@objc private func accessoryEsc() { controller?.sendEscape() }
|
||||
@objc private func accessoryTab() { controller?.sendTab() }
|
||||
@objc private func accessoryControl() {
|
||||
controller?.pressKeyCombo([.control])
|
||||
}
|
||||
@objc private func accessoryCommand() {
|
||||
controller?.pressKeyCombo([.command])
|
||||
}
|
||||
@objc private func accessoryOption() {
|
||||
controller?.pressKeyCombo([.option])
|
||||
}
|
||||
@objc private func accessoryLeft() { controller?.sendArrow(.left) }
|
||||
@objc private func accessoryDown() { controller?.sendArrow(.down) }
|
||||
@objc private func accessoryUp() { controller?.sendArrow(.up) }
|
||||
@objc private func accessoryRight() { controller?.sendArrow(.right) }
|
||||
@objc private func accessoryDismiss() {
|
||||
setSoftwareKeyboardVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIKeyInput (accept on-screen keyboard input)
|
||||
|
||||
extension FramebufferUIView: UIKeyInput {
|
||||
var hasText: Bool { true }
|
||||
|
||||
func insertText(_ text: String) {
|
||||
guard let controller else { return }
|
||||
controller.type(text)
|
||||
}
|
||||
|
||||
func deleteBackward() {
|
||||
controller?.sendBackspace()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITextInputTraits (sane keyboard appearance)
|
||||
|
||||
extension FramebufferUIView: UITextInputTraits {
|
||||
var autocorrectionType: UITextAutocorrectionType {
|
||||
get { .no } set { _ = newValue }
|
||||
}
|
||||
var autocapitalizationType: UITextAutocapitalizationType {
|
||||
get { .none } set { _ = newValue }
|
||||
}
|
||||
var spellCheckingType: UITextSpellCheckingType {
|
||||
get { .no } set { _ = newValue }
|
||||
}
|
||||
var smartQuotesType: UITextSmartQuotesType {
|
||||
get { .no } set { _ = newValue }
|
||||
}
|
||||
var smartDashesType: UITextSmartDashesType {
|
||||
get { .no } set { _ = newValue }
|
||||
}
|
||||
var smartInsertDeleteType: UITextSmartInsertDeleteType {
|
||||
get { .no } set { _ = newValue }
|
||||
}
|
||||
var keyboardType: UIKeyboardType {
|
||||
get { .asciiCapable } set { _ = newValue }
|
||||
}
|
||||
var keyboardAppearance: UIKeyboardAppearance {
|
||||
get { .dark } set { _ = newValue }
|
||||
}
|
||||
var returnKeyType: UIReturnKeyType {
|
||||
get { .default } set { _ = newValue }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -8,6 +8,7 @@ struct FramebufferView: UIViewRepresentable {
|
||||
let inputMode: InputMode
|
||||
let selectedScreen: RemoteScreen?
|
||||
@Binding var trackpadCursor: CGPoint
|
||||
@Binding var showSoftwareKeyboard: Bool
|
||||
|
||||
func makeUIView(context: Context) -> FramebufferUIView {
|
||||
let view = FramebufferUIView()
|
||||
@@ -18,6 +19,9 @@ struct FramebufferView: UIViewRepresentable {
|
||||
view.onTrackpadCursorChanged = { [binding = $trackpadCursor] new in
|
||||
binding.wrappedValue = new
|
||||
}
|
||||
view.onKeyboardDismissed = { [binding = $showSoftwareKeyboard] in
|
||||
if binding.wrappedValue { binding.wrappedValue = false }
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
@@ -30,6 +34,7 @@ struct FramebufferView: UIViewRepresentable {
|
||||
}
|
||||
uiView.apply(image: controller.currentImage,
|
||||
framebufferSize: framebufferSize)
|
||||
uiView.setSoftwareKeyboardVisible(showSoftwareKeyboard)
|
||||
// Touch the revision so SwiftUI re-runs us when frames arrive
|
||||
_ = controller.imageRevision
|
||||
}
|
||||
@@ -51,6 +56,7 @@ struct FramebufferView: View {
|
||||
let inputMode: InputMode
|
||||
let selectedScreen: RemoteScreen?
|
||||
@Binding var trackpadCursor: CGPoint
|
||||
@Binding var showSoftwareKeyboard: Bool
|
||||
|
||||
var body: some View { Color.black }
|
||||
}
|
||||
|
||||
@@ -16,8 +16,7 @@ public struct SessionView: View {
|
||||
|
||||
@State private var controller: SessionController?
|
||||
@State private var inputMode: InputMode = .touch
|
||||
@State private var showKeyboardBar = false
|
||||
@State private var showFunctionRow = false
|
||||
@State private var showSoftwareKeyboard = false
|
||||
@State private var trackpadCursor: CGPoint = CGPoint(x: 0.5, y: 0.5)
|
||||
@State private var selectedScreenID: UInt32?
|
||||
@State private var screenshotItem: ScreenshotShareItem?
|
||||
@@ -36,7 +35,8 @@ public struct SessionView: View {
|
||||
controller: controller,
|
||||
inputMode: inputMode,
|
||||
selectedScreen: selectedScreen(for: controller),
|
||||
trackpadCursor: $trackpadCursor
|
||||
trackpadCursor: $trackpadCursor,
|
||||
showSoftwareKeyboard: $showSoftwareKeyboard
|
||||
)
|
||||
.ignoresSafeArea()
|
||||
.onTapGesture(count: 3) {
|
||||
@@ -113,19 +113,13 @@ public struct SessionView: View {
|
||||
SessionToolbar(
|
||||
controller: controller,
|
||||
inputMode: $inputMode,
|
||||
showKeyboardBar: $showKeyboardBar,
|
||||
showKeyboardBar: $showSoftwareKeyboard,
|
||||
selectedScreenID: $selectedScreenID,
|
||||
onScreenshot: { takeScreenshot(controller: controller) },
|
||||
onDisconnect: { stopAndDismiss() }
|
||||
)
|
||||
|
||||
Spacer()
|
||||
|
||||
if showKeyboardBar {
|
||||
SoftKeyboardBar(controller: controller, isExpanded: $showFunctionRow)
|
||||
.padding(.bottom, 12)
|
||||
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user