From 3a9d04287fdf14839981cb194f2016708f6f3045 Mon Sep 17 00:00:00 2001 From: Georg Wechslberger Date: Thu, 15 Sep 2022 04:25:42 +0200 Subject: [PATCH 1/3] feat: add hardwareKeyboardOnly --- lib/src/terminal_view.dart | 81 ++++++++++++++++++++----------- lib/src/ui/keyboard_listener.dart | 61 +++++++++++++++++++++++ 2 files changed, 114 insertions(+), 28 deletions(-) create mode 100644 lib/src/ui/keyboard_listener.dart diff --git a/lib/src/terminal_view.dart b/lib/src/terminal_view.dart index e4812a0d..d8e97a6f 100644 --- a/lib/src/terminal_view.dart +++ b/lib/src/terminal_view.dart @@ -11,6 +11,7 @@ import 'package:xterm/src/ui/cursor_type.dart'; import 'package:xterm/src/ui/custom_text_edit.dart'; import 'package:xterm/src/ui/gesture/gesture_handler.dart'; import 'package:xterm/src/ui/input_map.dart'; +import 'package:xterm/src/ui/keyboard_listener.dart'; import 'package:xterm/src/ui/keyboard_visibility.dart'; import 'package:xterm/src/ui/render.dart'; import 'package:xterm/src/ui/shortcut/actions.dart'; @@ -43,6 +44,7 @@ class TerminalView extends StatefulWidget { this.deleteDetection = false, this.shortcuts, this.readOnly = false, + this.hardwareKeyboardOnly = false, }) : super(key: key); /// The underlying terminal that this widget renders. @@ -119,6 +121,10 @@ class TerminalView extends StatefulWidget { /// True if no input should send to the terminal. final bool readOnly; + /// True if only hardware keyboard events should be used as input. This will + /// also prevent any on-screen keyboard to be shown. + final bool hardwareKeyboardOnly; + @override State createState() => TerminalViewState(); } @@ -204,33 +210,39 @@ class TerminalViewState extends State { }, ); - child = CustomTextEdit( - key: _customTextEditKey, - focusNode: _focusNode, - inputType: widget.keyboardType, - keyboardAppearance: widget.keyboardAppearance, - deleteDetection: widget.deleteDetection, - onInsert: (text) { - _scrollToBottom(); - widget.terminal.textInput(text); - }, - onDelete: () { - _scrollToBottom(); - widget.terminal.keyInput(TerminalKey.backspace); - }, - onComposing: (text) { - setState(() => _composingText = text); - }, - onAction: (action) { - _scrollToBottom(); - if (action == TextInputAction.done) { - widget.terminal.keyInput(TerminalKey.enter); - } - }, - onKey: _onKeyEvent, - readOnly: widget.readOnly, - child: child, - ); + if (!widget.hardwareKeyboardOnly) { + child = CustomTextEdit( + key: _customTextEditKey, + focusNode: _focusNode, + inputType: widget.keyboardType, + keyboardAppearance: widget.keyboardAppearance, + deleteDetection: widget.deleteDetection, + onInsert: _onInsert, + onDelete: () { + _scrollToBottom(); + widget.terminal.keyInput(TerminalKey.backspace); + }, + onComposing: _onComposing, + onAction: (action) { + _scrollToBottom(); + if (action == TextInputAction.done) { + widget.terminal.keyInput(TerminalKey.enter); + } + }, + onKey: _onKeyEvent, + readOnly: widget.readOnly, + child: child, + ); + } else if (!widget.readOnly) { + // Only listen for key input from a hardware keyboard. + child = CustomKeyboardListener( + child: child, + focusNode: _focusNode, + onInsert: _onInsert, + onComposing: _onComposing, + onKey: _onKeyEvent, + ); + } child = TerminalActions( terminal: widget.terminal, @@ -285,7 +297,11 @@ class TerminalViewState extends State { if (_controller.selection != null) { _controller.clearSelection(); } else { - _customTextEditKey.currentState?.requestKeyboard(); + if (!widget.hardwareKeyboardOnly) { + _customTextEditKey.currentState?.requestKeyboard(); + } else { + _focusNode.requestFocus(); + } } } @@ -303,6 +319,15 @@ class TerminalViewState extends State { return _customTextEditKey.currentState?.hasInputConnection == true; } + void _onInsert(String text) { + _scrollToBottom(); + widget.terminal.textInput(text); + } + + void _onComposing(String? text) { + setState(() => _composingText = text); + } + KeyEventResult _onKeyEvent(FocusNode focusNode, RawKeyEvent event) { // ignore: invalid_use_of_protected_member final shortcutResult = _shortcutManager.handleKeypress( diff --git a/lib/src/ui/keyboard_listener.dart b/lib/src/ui/keyboard_listener.dart new file mode 100644 index 00000000..ac090989 --- /dev/null +++ b/lib/src/ui/keyboard_listener.dart @@ -0,0 +1,61 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +class CustomKeyboardListener extends StatelessWidget { + final Widget child; + + final FocusNode focusNode; + + final void Function(String) onInsert; + + final void Function(String?) onComposing; + + final KeyEventResult Function(FocusNode, RawKeyEvent) onKey; + + const CustomKeyboardListener({ + Key? key, + required this.child, + required this.focusNode, + required this.onInsert, + required this.onComposing, + required this.onKey, + }) : super(key: key); + + KeyEventResult _onKey(FocusNode focusNode, RawKeyEvent keyEvent) { + // First try to handle the key event directly. + final handled = onKey(focusNode, keyEvent); + if (handled == KeyEventResult.ignored) { + // If it was not handled, but the key corresponds to a character, + // insert the character. + if (keyEvent.character != null && keyEvent.character != "") { + onInsert(keyEvent.character!); + return KeyEventResult.handled; + } else if (keyEvent.data is RawKeyEventDataIos && + keyEvent is RawKeyDownEvent) { + // On iOS keyEvent.character is always null. But data.characters + // contains the the character(s) corresponding to the input. + final data = keyEvent.data as RawKeyEventDataIos; + if (data.characters != "") { + onComposing(null); + onInsert(data.characters); + } else if (data.charactersIgnoringModifiers != "") { + // If characters is an empty string but charactersIgnoringModifiers is + // not an empty string, this indicates that the current characters is + // being composed. The current composing state is + // charactersIgnoringModifiers. + onComposing(data.charactersIgnoringModifiers); + } + } + } + return handled; + } + + @override + Widget build(BuildContext context) { + return Focus( + focusNode: focusNode, + onKey: _onKey, + child: child, + ); + } +} From 6cb23c89f516a2145f3cd285b8fa735dcb48aee2 Mon Sep 17 00:00:00 2001 From: xuty Date: Tue, 20 Sep 2022 08:55:19 +0800 Subject: [PATCH 2/3] Add autofocus in CustomKeyboardListener --- lib/src/terminal_view.dart | 2 ++ lib/src/ui/keyboard_listener.dart | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/lib/src/terminal_view.dart b/lib/src/terminal_view.dart index d8e97a6f..f53fa38a 100644 --- a/lib/src/terminal_view.dart +++ b/lib/src/terminal_view.dart @@ -214,6 +214,7 @@ class TerminalViewState extends State { child = CustomTextEdit( key: _customTextEditKey, focusNode: _focusNode, + autofocus: widget.autofocus, inputType: widget.keyboardType, keyboardAppearance: widget.keyboardAppearance, deleteDetection: widget.deleteDetection, @@ -238,6 +239,7 @@ class TerminalViewState extends State { child = CustomKeyboardListener( child: child, focusNode: _focusNode, + autofocus: widget.autofocus, onInsert: _onInsert, onComposing: _onComposing, onKey: _onKeyEvent, diff --git a/lib/src/ui/keyboard_listener.dart b/lib/src/ui/keyboard_listener.dart index ac090989..6a5e1aef 100644 --- a/lib/src/ui/keyboard_listener.dart +++ b/lib/src/ui/keyboard_listener.dart @@ -6,6 +6,8 @@ class CustomKeyboardListener extends StatelessWidget { final FocusNode focusNode; + final bool autofocus; + final void Function(String) onInsert; final void Function(String?) onComposing; @@ -16,6 +18,7 @@ class CustomKeyboardListener extends StatelessWidget { Key? key, required this.child, required this.focusNode, + this.autofocus = false, required this.onInsert, required this.onComposing, required this.onKey, @@ -54,6 +57,7 @@ class CustomKeyboardListener extends StatelessWidget { Widget build(BuildContext context) { return Focus( focusNode: focusNode, + autofocus: autofocus, onKey: _onKey, child: child, ); From 41e362acd90159d507d4571226e661c4a4d3537f Mon Sep 17 00:00:00 2001 From: xuty Date: Tue, 20 Sep 2022 08:55:37 +0800 Subject: [PATCH 3/3] Add tests for TerminalView.hardwareKeyboardOnly --- test/src/terminal_view_test.dart | 67 ++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/test/src/terminal_view_test.dart b/test/src/terminal_view_test.dart index 806c94ef..f4d4c7f3 100644 --- a/test/src/terminal_view_test.dart +++ b/test/src/terminal_view_test.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:xterm/xterm.dart'; @@ -172,4 +173,70 @@ void main() { }, ); }); + + group('TerminalView.autofocus', () { + testWidgets('works', (WidgetTester tester) async { + final terminal = Terminal(); + final focusNode = FocusNode(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TerminalView( + terminal, + autofocus: true, + focusNode: focusNode, + ), + ), + ), + ); + + expect(focusNode.hasFocus, isTrue); + }); + + testWidgets('works in hardwareKeyboardOnly mode', (tester) async { + final terminal = Terminal(); + final focusNode = FocusNode(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TerminalView( + terminal, + autofocus: true, + focusNode: focusNode, + hardwareKeyboardOnly: true, + ), + ), + ), + ); + + expect(focusNode.hasFocus, isTrue); + }); + }); + + group('TerminalView.hardwareKeyboardOnly', () { + testWidgets('works', (WidgetTester tester) async { + final output = []; + final terminal = Terminal(onOutput: output.add); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TerminalView( + terminal, + autofocus: true, + hardwareKeyboardOnly: true, + ), + ), + ), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.keyA); + await tester.sendKeyEvent(LogicalKeyboardKey.keyB); + await tester.sendKeyEvent(LogicalKeyboardKey.keyC); + + expect(output.join(), 'abc'); + }); + }); }