terminal_view.dart 12 KB

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