terminal_view.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. import 'package:flutter/cupertino.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/rendering.dart';
  4. import 'package:flutter/services.dart';
  5. import 'package:xterm/src/core/buffer/cell_offset.dart';
  6. import 'package:xterm/src/core/input/keys.dart';
  7. import 'package:xterm/src/terminal.dart';
  8. import 'package:xterm/src/ui/controller.dart';
  9. import 'package:xterm/src/ui/cursor_type.dart';
  10. import 'package:xterm/src/ui/custom_text_edit.dart';
  11. import 'package:xterm/src/ui/gesture/gesture_handler.dart';
  12. import 'package:xterm/src/ui/input_map.dart';
  13. import 'package:xterm/src/ui/keyboard_listener.dart';
  14. import 'package:xterm/src/ui/keyboard_visibility.dart';
  15. import 'package:xterm/src/ui/render.dart';
  16. import 'package:xterm/src/ui/shortcut/actions.dart';
  17. import 'package:xterm/src/ui/shortcut/shortcuts.dart';
  18. import 'package:xterm/src/ui/terminal_text_style.dart';
  19. import 'package:xterm/src/ui/terminal_theme.dart';
  20. import 'package:xterm/src/ui/themes.dart';
  21. class TerminalView extends StatefulWidget {
  22. const TerminalView(
  23. this.terminal, {
  24. Key? key,
  25. this.controller,
  26. this.theme = TerminalThemes.defaultTheme,
  27. this.textStyle = const TerminalStyle(),
  28. this.padding,
  29. this.scrollController,
  30. this.autoResize = true,
  31. this.backgroundOpacity = 1,
  32. this.focusNode,
  33. this.autofocus = false,
  34. this.onTapUp,
  35. this.onSecondaryTapDown,
  36. this.onSecondaryTapUp,
  37. this.mouseCursor = SystemMouseCursors.text,
  38. this.keyboardType = TextInputType.emailAddress,
  39. this.keyboardAppearance = Brightness.dark,
  40. this.cursorType = TerminalCursorType.block,
  41. this.alwaysShowCursor = false,
  42. this.deleteDetection = false,
  43. this.shortcuts,
  44. this.readOnly = false,
  45. this.hardwareKeyboardOnly = false,
  46. }) : super(key: key);
  47. /// The underlying terminal that this widget renders.
  48. final Terminal terminal;
  49. final TerminalController? controller;
  50. /// The theme to use for this terminal.
  51. final TerminalTheme theme;
  52. /// The style to use for painting characters.
  53. final TerminalStyle textStyle;
  54. /// Padding around the inner [Scrollable] widget.
  55. final EdgeInsets? padding;
  56. /// Scroll controller for the inner [Scrollable] widget.
  57. final ScrollController? scrollController;
  58. /// Should this widget automatically notify the underlying terminal when its
  59. /// size changes. [true] by default.
  60. final bool autoResize;
  61. /// Opacity of the terminal background. Set to 0 to make the terminal
  62. /// background transparent.
  63. final double backgroundOpacity;
  64. /// An optional focus node to use as the focus node for this widget.
  65. final FocusNode? focusNode;
  66. /// True if this widget will be selected as the initial focus when no other
  67. /// node in its scope is currently focused.
  68. final bool autofocus;
  69. /// Callback for when the user taps on the terminal.
  70. final void Function(TapUpDetails, CellOffset)? onTapUp;
  71. /// Function called when the user taps on the terminal with a secondary
  72. /// button.
  73. final void Function(TapDownDetails, CellOffset)? onSecondaryTapDown;
  74. /// Function called when the user stops holding down a secondary button.
  75. final void Function(TapUpDetails, CellOffset)? onSecondaryTapUp;
  76. /// The mouse cursor for mouse pointers that are hovering over the terminal.
  77. /// [SystemMouseCursors.text] by default.
  78. final MouseCursor mouseCursor;
  79. /// The type of information for which to optimize the text input control.
  80. /// [TextInputType.emailAddress] by default.
  81. final TextInputType keyboardType;
  82. /// The appearance of the keyboard. [Brightness.dark] by default.
  83. ///
  84. /// This setting is only honored on iOS devices.
  85. final Brightness keyboardAppearance;
  86. /// The type of cursor to use. [TerminalCursorType.block] by default.
  87. final TerminalCursorType cursorType;
  88. /// Whether to always show the cursor. This is useful for debugging.
  89. /// [false] by default.
  90. final bool alwaysShowCursor;
  91. /// Workaround to detect delete key for platforms and IMEs that does not
  92. /// emit hardware delete event. Prefered on mobile platforms. [false] by
  93. /// default.
  94. final bool deleteDetection;
  95. /// Shortcuts for this terminal. This has higher priority than input handler
  96. /// of the terminal If not provided, [defaultTerminalShortcuts] will be used.
  97. final Map<ShortcutActivator, Intent>? shortcuts;
  98. /// True if no input should send to the terminal.
  99. final bool readOnly;
  100. /// True if only hardware keyboard events should be used as input. This will
  101. /// also prevent any on-screen keyboard to be shown.
  102. final bool hardwareKeyboardOnly;
  103. @override
  104. State<TerminalView> createState() => TerminalViewState();
  105. }
  106. class TerminalViewState extends State<TerminalView> {
  107. late final FocusNode _focusNode;
  108. late final ShortcutManager _shortcutManager;
  109. final _customTextEditKey = GlobalKey<CustomTextEditState>();
  110. final _scrollableKey = GlobalKey<ScrollableState>();
  111. final _viewportKey = GlobalKey();
  112. String? _composingText;
  113. late TerminalController _controller;
  114. late ScrollController _scrollController;
  115. RenderTerminal get renderTerminal =>
  116. _viewportKey.currentContext!.findRenderObject() as RenderTerminal;
  117. @override
  118. void initState() {
  119. _focusNode = widget.focusNode ?? FocusNode();
  120. _controller = widget.controller ?? TerminalController();
  121. _scrollController = widget.scrollController ?? ScrollController();
  122. _shortcutManager = ShortcutManager(
  123. shortcuts: widget.shortcuts ?? defaultTerminalShortcuts,
  124. );
  125. super.initState();
  126. }
  127. @override
  128. void didUpdateWidget(TerminalView oldWidget) {
  129. if (oldWidget.focusNode != widget.focusNode) {
  130. if (oldWidget.focusNode == null) {
  131. _focusNode.dispose();
  132. }
  133. _focusNode = widget.focusNode ?? FocusNode();
  134. }
  135. if (oldWidget.controller != widget.controller) {
  136. _controller = widget.controller ?? TerminalController();
  137. }
  138. if (oldWidget.scrollController != widget.scrollController) {
  139. _scrollController = widget.scrollController ?? ScrollController();
  140. }
  141. super.didUpdateWidget(oldWidget);
  142. }
  143. @override
  144. void dispose() {
  145. if (widget.focusNode == null) {
  146. _focusNode.dispose();
  147. }
  148. _shortcutManager.dispose();
  149. super.dispose();
  150. }
  151. @override
  152. Widget build(BuildContext context) {
  153. Widget child = Scrollable(
  154. key: _scrollableKey,
  155. controller: _scrollController,
  156. viewportBuilder: (context, offset) {
  157. return _TerminalView(
  158. key: _viewportKey,
  159. terminal: widget.terminal,
  160. controller: _controller,
  161. offset: offset,
  162. padding: MediaQuery.of(context).padding,
  163. autoResize: widget.autoResize,
  164. textStyle: widget.textStyle,
  165. theme: widget.theme,
  166. focusNode: _focusNode,
  167. cursorType: widget.cursorType,
  168. alwaysShowCursor: widget.alwaysShowCursor,
  169. onEditableRect: _onEditableRect,
  170. composingText: _composingText,
  171. );
  172. },
  173. );
  174. if (!widget.hardwareKeyboardOnly) {
  175. child = CustomTextEdit(
  176. key: _customTextEditKey,
  177. focusNode: _focusNode,
  178. autofocus: widget.autofocus,
  179. inputType: widget.keyboardType,
  180. keyboardAppearance: widget.keyboardAppearance,
  181. deleteDetection: widget.deleteDetection,
  182. onInsert: _onInsert,
  183. onDelete: () {
  184. _scrollToBottom();
  185. widget.terminal.keyInput(TerminalKey.backspace);
  186. },
  187. onComposing: _onComposing,
  188. onAction: (action) {
  189. _scrollToBottom();
  190. if (action == TextInputAction.done) {
  191. widget.terminal.keyInput(TerminalKey.enter);
  192. }
  193. },
  194. onKey: _onKeyEvent,
  195. readOnly: widget.readOnly,
  196. child: child,
  197. );
  198. } else if (!widget.readOnly) {
  199. // Only listen for key input from a hardware keyboard.
  200. child = CustomKeyboardListener(
  201. child: child,
  202. focusNode: _focusNode,
  203. autofocus: widget.autofocus,
  204. onInsert: _onInsert,
  205. onComposing: _onComposing,
  206. onKey: _onKeyEvent,
  207. );
  208. }
  209. child = TerminalActions(
  210. terminal: widget.terminal,
  211. controller: _controller,
  212. child: child,
  213. );
  214. child = KeyboardVisibilty(
  215. onKeyboardShow: _onKeyboardShow,
  216. child: child,
  217. );
  218. child = TerminalGestureHandler(
  219. terminalView: this,
  220. onTapUp: _onTapUp,
  221. onTapDown: _onTapDown,
  222. onSecondaryTapDown:
  223. widget.onSecondaryTapDown != null ? _onSecondaryTapDown : null,
  224. onSecondaryTapUp:
  225. widget.onSecondaryTapUp != null ? _onSecondaryTapUp : null,
  226. child: child,
  227. );
  228. child = MouseRegion(
  229. cursor: widget.mouseCursor,
  230. child: child,
  231. );
  232. child = Container(
  233. color: widget.theme.background.withOpacity(widget.backgroundOpacity),
  234. padding: widget.padding,
  235. child: child,
  236. );
  237. return child;
  238. }
  239. void requestKeyboard() {
  240. _customTextEditKey.currentState?.requestKeyboard();
  241. }
  242. void closeKeyboard() {
  243. _customTextEditKey.currentState?.closeKeyboard();
  244. }
  245. void _onTapUp(TapUpDetails details) {
  246. final offset = renderTerminal.getCellOffset(details.localPosition);
  247. widget.onTapUp?.call(details, offset);
  248. }
  249. void _onTapDown(_) {
  250. if (_controller.selection != null) {
  251. _controller.clearSelection();
  252. } else {
  253. if (!widget.hardwareKeyboardOnly) {
  254. _customTextEditKey.currentState?.requestKeyboard();
  255. } else {
  256. _focusNode.requestFocus();
  257. }
  258. }
  259. }
  260. void _onSecondaryTapDown(TapDownDetails details) {
  261. final offset = renderTerminal.getCellOffset(details.localPosition);
  262. widget.onSecondaryTapDown?.call(details, offset);
  263. }
  264. void _onSecondaryTapUp(TapUpDetails details) {
  265. final offset = renderTerminal.getCellOffset(details.localPosition);
  266. widget.onSecondaryTapUp?.call(details, offset);
  267. }
  268. bool get hasInputConnection {
  269. return _customTextEditKey.currentState?.hasInputConnection == true;
  270. }
  271. void _onInsert(String text) {
  272. _scrollToBottom();
  273. widget.terminal.textInput(text);
  274. }
  275. void _onComposing(String? text) {
  276. setState(() => _composingText = text);
  277. }
  278. KeyEventResult _onKeyEvent(FocusNode focusNode, RawKeyEvent event) {
  279. // ignore: invalid_use_of_protected_member
  280. final shortcutResult = _shortcutManager.handleKeypress(
  281. focusNode.context!,
  282. event,
  283. );
  284. if (shortcutResult != KeyEventResult.ignored) {
  285. return shortcutResult;
  286. }
  287. if (event is! RawKeyDownEvent) {
  288. return KeyEventResult.ignored;
  289. }
  290. final key = inputMap(event.logicalKey);
  291. if (key == null) {
  292. return KeyEventResult.ignored;
  293. }
  294. final handled = widget.terminal.keyInput(
  295. key,
  296. ctrl: event.isControlPressed,
  297. alt: event.isAltPressed,
  298. shift: event.isShiftPressed,
  299. );
  300. if (handled) {
  301. _scrollToBottom();
  302. }
  303. return handled ? KeyEventResult.handled : KeyEventResult.ignored;
  304. }
  305. void _onKeyboardShow() {
  306. if (_focusNode.hasFocus) {
  307. WidgetsBinding.instance.addPostFrameCallback((_) {
  308. _scrollToBottom();
  309. });
  310. }
  311. }
  312. void _onEditableRect(Rect rect, Rect caretRect) {
  313. _customTextEditKey.currentState?.setEditableRect(rect, caretRect);
  314. }
  315. void _scrollToBottom() {
  316. final position = _scrollableKey.currentState?.position;
  317. if (position != null) {
  318. position.jumpTo(position.maxScrollExtent);
  319. }
  320. }
  321. }
  322. class _TerminalView extends LeafRenderObjectWidget {
  323. const _TerminalView({
  324. Key? key,
  325. required this.terminal,
  326. required this.controller,
  327. required this.offset,
  328. required this.padding,
  329. required this.autoResize,
  330. required this.textStyle,
  331. required this.theme,
  332. required this.focusNode,
  333. required this.cursorType,
  334. required this.alwaysShowCursor,
  335. this.onEditableRect,
  336. this.composingText,
  337. }) : super(key: key);
  338. final Terminal terminal;
  339. final TerminalController controller;
  340. final ViewportOffset offset;
  341. final EdgeInsets padding;
  342. final bool autoResize;
  343. final TerminalStyle textStyle;
  344. final TerminalTheme theme;
  345. final FocusNode focusNode;
  346. final TerminalCursorType cursorType;
  347. final bool alwaysShowCursor;
  348. final EditableRectCallback? onEditableRect;
  349. final String? composingText;
  350. @override
  351. RenderTerminal createRenderObject(BuildContext context) {
  352. return RenderTerminal(
  353. terminal: terminal,
  354. controller: controller,
  355. offset: offset,
  356. padding: padding,
  357. autoResize: autoResize,
  358. textStyle: textStyle,
  359. theme: theme,
  360. focusNode: focusNode,
  361. cursorType: cursorType,
  362. alwaysShowCursor: alwaysShowCursor,
  363. onEditableRect: onEditableRect,
  364. composingText: composingText,
  365. );
  366. }
  367. @override
  368. void updateRenderObject(BuildContext context, RenderTerminal renderObject) {
  369. renderObject
  370. ..terminal = terminal
  371. ..controller = controller
  372. ..offset = offset
  373. ..padding = padding
  374. ..autoResize = autoResize
  375. ..textStyle = textStyle
  376. ..theme = theme
  377. ..focusNode = focusNode
  378. ..cursorType = cursorType
  379. ..alwaysShowCursor = alwaysShowCursor
  380. ..onEditableRect = onEditableRect
  381. ..composingText = composingText;
  382. }
  383. }