diff --git a/flutter_highlight/example/ios/Podfile.lock b/flutter_highlight/example/ios/Podfile.lock index 52e09e5..73e064b 100644 --- a/flutter_highlight/example/ios/Podfile.lock +++ b/flutter_highlight/example/ios/Podfile.lock @@ -2,12 +2,15 @@ PODS: - Flutter (1.0.0) - url_launcher (0.0.1): - Flutter + - url_launcher_macos (0.0.1): + - Flutter - url_launcher_web (0.0.1): - Flutter DEPENDENCIES: - Flutter (from `Flutter`) - url_launcher (from `.symlinks/plugins/url_launcher/ios`) + - url_launcher_macos (from `.symlinks/plugins/url_launcher_macos/ios`) - url_launcher_web (from `.symlinks/plugins/url_launcher_web/ios`) EXTERNAL SOURCES: @@ -15,14 +18,17 @@ EXTERNAL SOURCES: :path: Flutter url_launcher: :path: ".symlinks/plugins/url_launcher/ios" + url_launcher_macos: + :path: ".symlinks/plugins/url_launcher_macos/ios" url_launcher_web: :path: ".symlinks/plugins/url_launcher_web/ios" SPEC CHECKSUMS: Flutter: 0e3d915762c693b495b44d77113d4970485de6ec - url_launcher: a1c0cc845906122c4784c542523d8cacbded5626 + url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef + url_launcher_macos: fd7894421cd39320dce5f292fc99ea9270b2a313 url_launcher_web: e5527357f037c87560776e36436bf2b0288b965c PODFILE CHECKSUM: 1b66dae606f75376c5f2135a8290850eeb09ae83 -COCOAPODS: 1.8.4 +COCOAPODS: 1.9.1 diff --git a/flutter_highlight/example/lib/main.dart b/flutter_highlight/example/lib/main.dart index 74ada48..99654bc 100644 --- a/flutter_highlight/example/lib/main.dart +++ b/flutter_highlight/example/lib/main.dart @@ -41,6 +41,7 @@ class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomPadding: false, appBar: AppBar( title: Text(title), actions: <Widget>[ @@ -91,21 +92,37 @@ class _MyHomePageState extends State<MyHomePage> { ) ], ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: <Widget>[ - HighlightView( - exampleMap[language], - language: language, - theme: themeMap[theme], - padding: EdgeInsets.all(12), - textStyle: TextStyle( - fontFamily: - 'SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace'), - ) - ], - ), + body: LayoutBuilder( + builder: (context, constraints) { + final highlightView = HighlightView( + exampleMap[language], + readOnly: false, + language: language, + theme: themeMap[theme], + padding: EdgeInsets.all(12), + textStyle: TextStyle( + fontFamily: + 'SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace', + ), + ); + + if (constraints.maxWidth > 600) { + return Center( + child: Container( + constraints: BoxConstraints( + maxWidth: 600, + maxHeight: 800, + ), + child: highlightView, + ), + ); + } else { + return SizedBox( + height: MediaQuery.of(context).size.height, + child: highlightView, + ); + } + }, ), ); } diff --git a/flutter_highlight/lib/flutter_highlight.dart b/flutter_highlight/lib/flutter_highlight.dart index 43b7fa1..fcaa04e 100644 --- a/flutter_highlight/lib/flutter_highlight.dart +++ b/flutter_highlight/lib/flutter_highlight.dart @@ -1,9 +1,12 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:highlight/highlight.dart' show highlight, Node; +import 'package:linked_scroll_controller/linked_scroll_controller.dart'; /// Highlight Flutter Widget -class HighlightView extends StatelessWidget { +class HighlightView extends StatefulWidget { /// The original code to be highlighted final String source; @@ -27,6 +30,14 @@ class HighlightView extends StatelessWidget { /// Specify text styles such as font family and font size final TextStyle textStyle; + /// Code edition controller + /// + /// Required if property [readOnly] changed to false + final TextEditingController controller; + + /// Code edition availability + final bool readOnly; + HighlightView( String input, { this.language, @@ -34,8 +45,59 @@ class HighlightView extends StatelessWidget { this.padding, this.textStyle, int tabSize = 8, // TODO: https://github.com/flutter/flutter/issues/50087 + this.readOnly = true, + this.controller, }) : source = input.replaceAll('\t', ' ' * tabSize); + static const _rootKey = 'root'; + static const _defaultFontColor = Color(0xff000000); + static const _defaultBackgroundColor = Color(0xffffffff); + + // TODO: dart:io is not available at web platform currently + // See: https://github.com/flutter/flutter/issues/39998 + // So we just use monospace here for now + static const _defaultFontFamily = 'monospace'; + + @override + _HighlightViewState createState() => _HighlightViewState(); +} + +class _HighlightViewState extends State<HighlightView> { + LinkedScrollControllerGroup _controllers; + ScrollController _highlightScrollController, _editableScrollView; + TextEditingController _textController; + String _editableText; + + @override + void initState() { + _controllers = LinkedScrollControllerGroup(); + _highlightScrollController = _controllers.addAndGet(); + _editableScrollView = _controllers.addAndGet(); + _textController = widget.controller ?? TextEditingController(); + _textController.text = widget.source; + _editableText = widget.source; + super.initState(); + } + + @override + void dispose() { + _highlightScrollController.dispose(); + _editableScrollView.dispose(); + if (widget.controller == null) { + _textController.dispose(); + } + super.dispose(); + } + + @override + void didUpdateWidget(HighlightView oldWidget) { + if (oldWidget.source != widget.source) { + _textController.value = TextEditingValue(text: widget.source); + _editableText = widget.source; + } + super.didUpdateWidget(oldWidget); + } + List<TextSpan> _convert(List<Node> nodes) { List<TextSpan> spans = []; var currentSpans = spans; @@ -45,10 +107,11 @@ class HighlightView extends StatelessWidget { if (node.value != null) { currentSpans.add(node.className == null ? TextSpan(text: node.value) - : TextSpan(text: node.value, style: theme[node.className])); + : TextSpan(text: node.value, style: widget.theme[node.className])); } else if (node.children != null) { List<TextSpan> tmp = []; - currentSpans.add(TextSpan(children: tmp, style: theme[node.className])); + currentSpans + .add(TextSpan(children: tmp, style: widget.theme[node.className])); stack.add(currentSpans); currentSpans = tmp; @@ -68,32 +131,85 @@ class HighlightView extends StatelessWidget { return spans; } - static const _rootKey = 'root'; - static const _defaultFontColor = Color(0xff000000); - static const _defaultBackgroundColor = Color(0xffffffff); - - // TODO: dart:io is not available at web platform currently - // See: https://github.com/flutter/flutter/issues/39998 - // So we just use monospace here for now - static const _defaultFontFamily = 'monospace'; + int get _linesCount => '\n'.allMatches(_editableText).length; @override Widget build(BuildContext context) { + final media = MediaQuery.of(context); + var padding = widget.padding ?? EdgeInsets.zero; + // Keyboard height + if (!widget.readOnly) { + padding = padding.add(media.viewInsets); + } + var _textStyle = TextStyle( - fontFamily: _defaultFontFamily, - color: theme[_rootKey]?.color ?? _defaultFontColor, + letterSpacing: 0, + fontSize: 16, + fontFamily: HighlightView._defaultFontFamily, + color: widget.theme[HighlightView._rootKey]?.color ?? + HighlightView._defaultFontColor, ); - if (textStyle != null) { - _textStyle = _textStyle.merge(textStyle); + if (widget.textStyle != null) { + _textStyle = _textStyle.merge(widget.textStyle); } - return Container( - color: theme[_rootKey]?.backgroundColor ?? _defaultBackgroundColor, + Widget content = Padding( padding: padding, child: RichText( text: TextSpan( style: _textStyle, - children: _convert(highlight.parse(source, language: language).nodes), + children: _convert( + highlight.parse(_editableText, language: widget.language).nodes, + ), + ), + ), + ); + + if (!widget.readOnly) { + content = SingleChildScrollView( + controller: _highlightScrollController, + child: content, + ); + } + + return NotificationListener( + onNotification: (notification) { + if (notification is ScrollUpdateNotification) { + if (notification.scrollDelta.abs() > 10 && + notification.dragDetails != null) + SystemChannels.textInput.invokeMethod('TextInput.hide'); + } + return true; + }, + child: Container( + color: widget.theme[HighlightView._rootKey]?.backgroundColor ?? + HighlightView._defaultBackgroundColor, + child: Stack( + children: <Widget>[ + content, + if (!widget.readOnly) + CupertinoTextField( + scrollController: _editableScrollView, + scrollPadding: EdgeInsets.zero, + maxLines: _linesCount, + maxLengthEnforced: false, + keyboardType: TextInputType.multiline, + controller: _textController, + padding: padding, + style: _textStyle.copyWith( + color: Colors.transparent, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.zero, + color: Colors.transparent, + ), + onChanged: (value) { + setState(() { + _editableText = value; + }); + }, + ), + ], ), ), ); diff --git a/flutter_highlight/pubspec.yaml b/flutter_highlight/pubspec.yaml index ea30628..ecb72a6 100644 --- a/flutter_highlight/pubspec.yaml +++ b/flutter_highlight/pubspec.yaml @@ -11,6 +11,7 @@ dependencies: flutter: sdk: flutter highlight: ^0.6.0 + linked_scroll_controller: ^0.1.2 dev_dependencies: flutter_test: