Skip to content

Commit

Permalink
Merge pull request #131 from tauu/hardwarekeyboard-only
Browse files Browse the repository at this point in the history
feat: add hardwareKeyboardOnly flag to TerminalView
  • Loading branch information
xtyxtyx committed Sep 20, 2022
2 parents 07c1f17 + d397110 commit 0e10f22
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 28 deletions.
83 changes: 55 additions & 28 deletions lib/src/terminal_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<TerminalView> createState() => TerminalViewState();
}
Expand Down Expand Up @@ -216,33 +222,41 @@ class TerminalViewState extends State<TerminalView> {
},
);

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,
autofocus: widget.autofocus,
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,
autofocus: widget.autofocus,
onInsert: _onInsert,
onComposing: _onComposing,
onKey: _onKeyEvent,
);
}

child = TerminalActions(
terminal: widget.terminal,
Expand Down Expand Up @@ -299,7 +313,11 @@ class TerminalViewState extends State<TerminalView> {
if (_controller.selection != null) {
_controller.clearSelection();
} else {
_customTextEditKey.currentState?.requestKeyboard();
if (!widget.hardwareKeyboardOnly) {
_customTextEditKey.currentState?.requestKeyboard();
} else {
_focusNode.requestFocus();
}
}
}

Expand All @@ -317,6 +335,15 @@ class TerminalViewState extends State<TerminalView> {
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(
Expand Down
65 changes: 65 additions & 0 deletions lib/src/ui/keyboard_listener.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

class CustomKeyboardListener extends StatelessWidget {
final Widget child;

final FocusNode focusNode;

final bool autofocus;

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,
this.autofocus = false,
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,
autofocus: autofocus,
onKey: _onKey,
child: child,
);
}
}
67 changes: 67 additions & 0 deletions test/src/terminal_view_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:io';

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:xterm/xterm.dart';

Expand Down Expand Up @@ -239,4 +240,70 @@ void main() {
expect(output, isEmpty);
});
});

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 = <String>[];
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');
});
});
}

0 comments on commit 0e10f22

Please sign in to comment.