import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:xterm/src/core/buffer/cell_offset.dart'; import 'package:xterm/src/core/input/keys.dart'; import 'package:xterm/src/terminal.dart'; import 'package:xterm/src/ui/controller.dart'; 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'; import 'package:xterm/src/ui/shortcut/shortcuts.dart'; import 'package:xterm/src/ui/terminal_text_style.dart'; import 'package:xterm/src/ui/terminal_theme.dart'; import 'package:xterm/src/ui/themes.dart'; class TerminalView extends StatefulWidget { const TerminalView( this.terminal, { Key? key, this.controller, this.theme = TerminalThemes.defaultTheme, this.textStyle = const TerminalStyle(), this.textScaleFactor, this.padding, this.scrollController, this.autoResize = true, this.backgroundOpacity = 1, this.focusNode, this.autofocus = false, this.onTapUp, this.onSecondaryTapDown, this.onSecondaryTapUp, this.mouseCursor = SystemMouseCursors.text, this.keyboardType = TextInputType.emailAddress, this.keyboardAppearance = Brightness.dark, this.cursorType = TerminalCursorType.block, this.alwaysShowCursor = false, this.deleteDetection = false, this.shortcuts, this.readOnly = false, this.hardwareKeyboardOnly = false, }) : super(key: key); /// The underlying terminal that this widget renders. final Terminal terminal; final TerminalController? controller; /// The theme to use for this terminal. final TerminalTheme theme; /// The style to use for painting characters. final TerminalStyle textStyle; /// The number of font pixels for each logical pixel. If null, will use the /// [MediaQueryData.textScaleFactor] obtained from [MediaQuery], or 1.0 if /// there is no [MediaQuery] in scope. final double? textScaleFactor; /// Padding around the inner [Scrollable] widget. final EdgeInsets? padding; /// Scroll controller for the inner [Scrollable] widget. final ScrollController? scrollController; /// Should this widget automatically notify the underlying terminal when its /// size changes. [true] by default. final bool autoResize; /// Opacity of the terminal background. Set to 0 to make the terminal /// background transparent. final double backgroundOpacity; /// An optional focus node to use as the focus node for this widget. final FocusNode? focusNode; /// True if this widget will be selected as the initial focus when no other /// node in its scope is currently focused. final bool autofocus; /// Callback for when the user taps on the terminal. final void Function(TapUpDetails, CellOffset)? onTapUp; /// Function called when the user taps on the terminal with a secondary /// button. final void Function(TapDownDetails, CellOffset)? onSecondaryTapDown; /// Function called when the user stops holding down a secondary button. final void Function(TapUpDetails, CellOffset)? onSecondaryTapUp; /// The mouse cursor for mouse pointers that are hovering over the terminal. /// [SystemMouseCursors.text] by default. final MouseCursor mouseCursor; /// The type of information for which to optimize the text input control. /// [TextInputType.emailAddress] by default. final TextInputType keyboardType; /// The appearance of the keyboard. [Brightness.dark] by default. /// /// This setting is only honored on iOS devices. final Brightness keyboardAppearance; /// The type of cursor to use. [TerminalCursorType.block] by default. final TerminalCursorType cursorType; /// Whether to always show the cursor. This is useful for debugging. /// [false] by default. final bool alwaysShowCursor; /// Workaround to detect delete key for platforms and IMEs that does not /// emit hardware delete event. Prefered on mobile platforms. [false] by /// default. final bool deleteDetection; /// Shortcuts for this terminal. This has higher priority than input handler /// of the terminal If not provided, [defaultTerminalShortcuts] will be used. final Map? shortcuts; /// 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 createState() => TerminalViewState(); } class TerminalViewState extends State { late FocusNode _focusNode; late final ShortcutManager _shortcutManager; final _customTextEditKey = GlobalKey(); final _scrollableKey = GlobalKey(); final _viewportKey = GlobalKey(); String? _composingText; late TerminalController _controller; late ScrollController _scrollController; RenderTerminal get renderTerminal => _viewportKey.currentContext!.findRenderObject() as RenderTerminal; @override void initState() { _focusNode = widget.focusNode ?? FocusNode(); _controller = widget.controller ?? TerminalController(); _scrollController = widget.scrollController ?? ScrollController(); _shortcutManager = ShortcutManager( shortcuts: widget.shortcuts ?? defaultTerminalShortcuts, ); super.initState(); } @override void didUpdateWidget(TerminalView oldWidget) { if (oldWidget.focusNode != widget.focusNode) { if (oldWidget.focusNode == null) { _focusNode.dispose(); } _focusNode = widget.focusNode ?? FocusNode(); } if (oldWidget.controller != widget.controller) { if (oldWidget.controller == null) { _controller.dispose(); } _controller = widget.controller ?? TerminalController(); } if (oldWidget.scrollController != widget.scrollController) { if (oldWidget.scrollController == null) { _scrollController.dispose(); } _scrollController = widget.scrollController ?? ScrollController(); } super.didUpdateWidget(oldWidget); } @override void dispose() { if (widget.focusNode == null) { _focusNode.dispose(); } if (widget.controller == null) { _controller.dispose(); } if (widget.scrollController == null) { _scrollController.dispose(); } _shortcutManager.dispose(); super.dispose(); } @override Widget build(BuildContext context) { Widget child = Scrollable( key: _scrollableKey, controller: _scrollController, viewportBuilder: (context, offset) { return _TerminalView( key: _viewportKey, terminal: widget.terminal, controller: _controller, offset: offset, padding: MediaQuery.of(context).padding, autoResize: widget.autoResize, textStyle: widget.textStyle, textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context), theme: widget.theme, focusNode: _focusNode, cursorType: widget.cursorType, alwaysShowCursor: widget.alwaysShowCursor, onEditableRect: _onEditableRect, composingText: _composingText, ); }, ); 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, controller: _controller, child: child, ); child = KeyboardVisibilty( onKeyboardShow: _onKeyboardShow, child: child, ); child = TerminalGestureHandler( terminalView: this, terminalController: _controller, onTapUp: _onTapUp, onTapDown: _onTapDown, onSecondaryTapDown: widget.onSecondaryTapDown != null ? _onSecondaryTapDown : null, onSecondaryTapUp: widget.onSecondaryTapUp != null ? _onSecondaryTapUp : null, readOnly: widget.readOnly, child: child, ); child = MouseRegion( cursor: widget.mouseCursor, child: child, ); child = Container( color: widget.theme.background.withOpacity(widget.backgroundOpacity), padding: widget.padding, child: child, ); return child; } void requestKeyboard() { _customTextEditKey.currentState?.requestKeyboard(); } void closeKeyboard() { _customTextEditKey.currentState?.closeKeyboard(); } void _onTapUp(TapUpDetails details) { final offset = renderTerminal.getCellOffset(details.localPosition); widget.onTapUp?.call(details, offset); } void _onTapDown(_) { if (_controller.selection != null) { _controller.clearSelection(); } else { if (!widget.hardwareKeyboardOnly) { _customTextEditKey.currentState?.requestKeyboard(); } else { _focusNode.requestFocus(); } } } void _onSecondaryTapDown(TapDownDetails details) { final offset = renderTerminal.getCellOffset(details.localPosition); widget.onSecondaryTapDown?.call(details, offset); } void _onSecondaryTapUp(TapUpDetails details) { final offset = renderTerminal.getCellOffset(details.localPosition); widget.onSecondaryTapUp?.call(details, offset); } bool get hasInputConnection { 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( focusNode.context!, event, ); if (shortcutResult != KeyEventResult.ignored) { return shortcutResult; } if (event is! RawKeyDownEvent) { return KeyEventResult.ignored; } final key = inputMap(event.logicalKey); if (key == null) { return KeyEventResult.ignored; } final handled = widget.terminal.keyInput( key, ctrl: event.isControlPressed, alt: event.isAltPressed, shift: event.isShiftPressed, ); if (handled) { _scrollToBottom(); } return handled ? KeyEventResult.handled : KeyEventResult.ignored; } void _onKeyboardShow() { if (_focusNode.hasFocus) { WidgetsBinding.instance.addPostFrameCallback((_) { _scrollToBottom(); }); } } void _onEditableRect(Rect rect, Rect caretRect) { _customTextEditKey.currentState?.setEditableRect(rect, caretRect); } void _scrollToBottom() { final position = _scrollableKey.currentState?.position; if (position != null) { position.jumpTo(position.maxScrollExtent); } } } class _TerminalView extends LeafRenderObjectWidget { const _TerminalView({ Key? key, required this.terminal, required this.controller, required this.offset, required this.padding, required this.autoResize, required this.textStyle, required this.textScaleFactor, required this.theme, required this.focusNode, required this.cursorType, required this.alwaysShowCursor, this.onEditableRect, this.composingText, }) : super(key: key); final Terminal terminal; final TerminalController controller; final ViewportOffset offset; final EdgeInsets padding; final bool autoResize; final TerminalStyle textStyle; final double textScaleFactor; final TerminalTheme theme; final FocusNode focusNode; final TerminalCursorType cursorType; final bool alwaysShowCursor; final EditableRectCallback? onEditableRect; final String? composingText; @override RenderTerminal createRenderObject(BuildContext context) { return RenderTerminal( terminal: terminal, controller: controller, offset: offset, padding: padding, autoResize: autoResize, textStyle: textStyle, textScaleFactor: textScaleFactor, theme: theme, focusNode: focusNode, cursorType: cursorType, alwaysShowCursor: alwaysShowCursor, onEditableRect: onEditableRect, composingText: composingText, ); } @override void updateRenderObject(BuildContext context, RenderTerminal renderObject) { renderObject ..terminal = terminal ..controller = controller ..offset = offset ..padding = padding ..autoResize = autoResize ..textStyle = textStyle ..textScaleFactor = textScaleFactor ..theme = theme ..focusNode = focusNode ..cursorType = cursorType ..alwaysShowCursor = alwaysShowCursor ..onEditableRect = onEditableRect ..composingText = composingText; } }