terminal_view.dart 12 KB

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