From 8991827516a9fb08d80c7b67495f1c8782c04f4c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 23 Jun 2026 13:30:44 +0000 Subject: [PATCH 1/2] Hide iPad modifier bar when a hardware keyboard is connected The on-screen modifier-key bar exists for touch/on-screen-keyboard users. When a physical keyboard is attached its modifier keys are already available, so the bar is redundant. Add PhysicalKeyboardObserver (backed by GameController's GCKeyboard connect/disconnect notifications) and let it suppress the bar; the user toggle still controls visibility otherwise, and is disabled while a hardware keyboard is present. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01WaxWSS3sLMWVddxDtYxS4N --- .../Input/PhysicalKeyboardObserver.swift | 52 +++++++++++++++++++ KVMConsoleiPad/UI/ViewerView.swift | 15 +++++- KVMConsoleiPadTests/NanoKVMiPadTests.swift | 18 +++++++ 3 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 KVMConsoleiPad/Input/PhysicalKeyboardObserver.swift diff --git a/KVMConsoleiPad/Input/PhysicalKeyboardObserver.swift b/KVMConsoleiPad/Input/PhysicalKeyboardObserver.swift new file mode 100644 index 0000000..2e0efd1 --- /dev/null +++ b/KVMConsoleiPad/Input/PhysicalKeyboardObserver.swift @@ -0,0 +1,52 @@ +import Combine +import GameController +import SwiftUI + +/// Tracks whether a hardware keyboard is currently attached to the device. +/// +/// The on-screen modifier-key bar exists so that touch/on-screen-keyboard users +/// can send Ctrl/Alt/Cmd/etc. When a physical keyboard is connected those keys +/// are already available, so the bar is redundant and the viewer hides it. +@MainActor +final class PhysicalKeyboardObserver: ObservableObject { + @Published private(set) var isConnected: Bool + + private var observers: [NSObjectProtocol] = [] + + init() { + isConnected = GCKeyboard.coalesced != nil + + let center = NotificationCenter.default + observers.append(center.addObserver( + forName: .GCKeyboardDidConnect, + object: nil, + queue: .main + ) { [weak self] _ in + MainActor.assumeIsolated { self?.isConnected = true } + }) + observers.append(center.addObserver( + forName: .GCKeyboardDidDisconnect, + object: nil, + queue: .main + ) { [weak self] _ in + // A disconnect notification can fire while another keyboard is still + // attached, so re-read the coalesced keyboard rather than assuming none. + MainActor.assumeIsolated { self?.isConnected = GCKeyboard.coalesced != nil } + }) + } + + deinit { + for observer in observers { + NotificationCenter.default.removeObserver(observer) + } + } +} + +/// Resolves whether the modifier-key bar should be visible. Kept as a pure +/// function so the precedence rule (hardware keyboard wins over the user toggle) +/// is unit-testable without a live `GCKeyboard`. +enum ModifierBarVisibility { + static func shouldShow(userEnabled: Bool, physicalKeyboardConnected: Bool) -> Bool { + userEnabled && !physicalKeyboardConnected + } +} diff --git a/KVMConsoleiPad/UI/ViewerView.swift b/KVMConsoleiPad/UI/ViewerView.swift index 62486e6..dc8420d 100644 --- a/KVMConsoleiPad/UI/ViewerView.swift +++ b/KVMConsoleiPad/UI/ViewerView.swift @@ -33,6 +33,7 @@ struct ViewerHostView: View { struct ViewerView: View { @StateObject private var model: ViewerViewModel + @StateObject private var keyboardObserver = PhysicalKeyboardObserver() @State private var modifierState = ModifierKeyState() @State private var showModifierBar = true @State private var keyboardFocusToken = 0 @@ -54,7 +55,7 @@ struct ViewerView: View { errorOverlay } - if showModifierBar, model.passwordPrompt == nil, !showsErrorOverlay { + if modifierBarVisible, model.passwordPrompt == nil, !showsErrorOverlay { ModifierKeyBar(state: $modifierState) { usage, modifier in sendVirtualKey(usage: usage, modifier: modifier) } @@ -120,6 +121,13 @@ struct ViewerView: View { } } + private var modifierBarVisible: Bool { + ModifierBarVisibility.shouldShow( + userEnabled: showModifierBar, + physicalKeyboardConnected: keyboardObserver.isConnected + ) + } + private var showsLocalCursor: Bool { model.isStreaming && model.isMouseCaptureEnabled @@ -171,7 +179,10 @@ struct ViewerView: View { } .toggleStyle(.button) .labelStyle(.iconOnly) - .help(showModifierBar ? "Hide modifier-key bar" : "Show modifier-key bar") + .disabled(keyboardObserver.isConnected) + .help(keyboardObserver.isConnected + ? "Hardware keyboard connected — modifier-key bar hidden" + : (showModifierBar ? "Hide modifier-key bar" : "Show modifier-key bar")) Button { keyboardFocusToken += 1 diff --git a/KVMConsoleiPadTests/NanoKVMiPadTests.swift b/KVMConsoleiPadTests/NanoKVMiPadTests.swift index 5dbc546..2baf3ba 100644 --- a/KVMConsoleiPadTests/NanoKVMiPadTests.swift +++ b/KVMConsoleiPadTests/NanoKVMiPadTests.swift @@ -58,4 +58,22 @@ final class KVMConsoleiPadTests: XCTestCase { XCTAssertFalse(PointerScrollResolver.shouldEmitWheel(touchCount: 1)) XCTAssertTrue(PointerScrollResolver.shouldEmitWheel(touchCount: 2)) } + + func test_modifierBarHiddenWhenPhysicalKeyboardConnected() { + XCTAssertFalse( + ModifierBarVisibility.shouldShow(userEnabled: true, physicalKeyboardConnected: true) + ) + } + + func test_modifierBarVisibleWhenEnabledAndNoPhysicalKeyboard() { + XCTAssertTrue( + ModifierBarVisibility.shouldShow(userEnabled: true, physicalKeyboardConnected: false) + ) + } + + func test_modifierBarHiddenWhenUserDisablesIt() { + XCTAssertFalse( + ModifierBarVisibility.shouldShow(userEnabled: false, physicalKeyboardConnected: false) + ) + } } From 2056a88eaf0cccb41c83365ffc1abd0ab88575d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 23 Jun 2026 13:36:51 +0000 Subject: [PATCH 2/2] Drop unused SwiftUI import from PhysicalKeyboardObserver Combine already provides ObservableObject/@Published; the file references no SwiftUI symbols, so the import was unnecessary and coupled this input utility to the UI framework. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01WaxWSS3sLMWVddxDtYxS4N --- KVMConsoleiPad/Input/PhysicalKeyboardObserver.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/KVMConsoleiPad/Input/PhysicalKeyboardObserver.swift b/KVMConsoleiPad/Input/PhysicalKeyboardObserver.swift index 2e0efd1..37bf9af 100644 --- a/KVMConsoleiPad/Input/PhysicalKeyboardObserver.swift +++ b/KVMConsoleiPad/Input/PhysicalKeyboardObserver.swift @@ -1,6 +1,5 @@ import Combine import GameController -import SwiftUI /// Tracks whether a hardware keyboard is currently attached to the device. ///