Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add hardwareKeyboardOnly flag to TerminalView #131

Merged
merged 5 commits into from
Sep 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
});
});
}