diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e024d5d..0a840196 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to this project will be documented in this file. +## 8.5.6 +* Support [Flutter 3.16](https://medium.com/flutter/whats-new-in-flutter-3-16-dba6cb1015d1) + ## 8.5.5 * Now when opening dialogs by `QuillToolbar` you will not get an exception when you don't use `FlutterQuillLocalizations.delegate` in your `WidgetsApp`, `MaterialApp`, or `CupertinoApp`. The fix is for the `QuillToolbarSearchButton`, `QuillToolbarLinkStyleButton`, and `QuillToolbarColorButton` buttons diff --git a/README.md b/README.md index 3db5e322..0518cf2f 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,6 @@ dependencies: git: https://github.com/singerdmx/flutter-quill.git ``` - > > Note: At this time, we are making too many changes to the library, and you might see a new version almost every day > diff --git a/example/.metadata b/example/.metadata index a778330b..d22992ed 100644 --- a/example/.metadata +++ b/example/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "d211f42860350d914a5ad8102f9ec32764dc6d06" + revision: "db7ef5bf9f59442b0e200a90587e8fa5e0c6336a" channel: "stable" project_type: app @@ -13,26 +13,26 @@ project_type: app migration: platforms: - platform: root - create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 - base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a + base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - platform: android - create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 - base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a + base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - platform: ios - create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 - base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a + base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - platform: linux - create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 - base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a + base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - platform: macos - create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 - base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a + base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - platform: web - create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 - base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a + base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - platform: windows - create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 - base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a + base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a # User provided section diff --git a/example/windows/CMakeLists.txt b/example/windows/CMakeLists.txt index c09389c5..d960948a 100644 --- a/example/windows/CMakeLists.txt +++ b/example/windows/CMakeLists.txt @@ -87,6 +87,12 @@ if(PLUGIN_BUNDLED_LIBRARIES) COMPONENT Runtime) endif() +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") diff --git a/example/windows/flutter/CMakeLists.txt b/example/windows/flutter/CMakeLists.txt index 930d2071..903f4899 100644 --- a/example/windows/flutter/CMakeLists.txt +++ b/example/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/flutter_quill_extensions/CHANGELOG.md b/flutter_quill_extensions/CHANGELOG.md index a1a9dc3e..2c892264 100644 --- a/flutter_quill_extensions/CHANGELOG.md +++ b/flutter_quill_extensions/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to this project will be documented in this file. +## 0.6.11 +* Support [Flutter 3.16](https://medium.com/flutter/whats-new-in-flutter-3-16-dba6cb1015d1) + ## 0.6.10 * Update deprecated members from `flutter_quill` * Update doc and `README.md` diff --git a/lib/src/models/config/shared_configurations.dart b/lib/src/models/config/shared_configurations.dart index edad5404..81179402 100644 --- a/lib/src/models/config/shared_configurations.dart +++ b/lib/src/models/config/shared_configurations.dart @@ -1,9 +1,9 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart' show Color, Colors, Locale; +import '../themes/quill_dialog_theme.dart'; import './editor/configurations.dart' show QuillEditorConfigurations; import './toolbar/configurations.dart' show QuillToolbarConfigurations; -import '../themes/quill_dialog_theme.dart'; import 'others/animations.dart'; export './others/animations.dart'; diff --git a/lib/src/models/config/toolbar/buttons/color.dart b/lib/src/models/config/toolbar/buttons/color.dart index e6c2f9c5..ef242fa7 100644 --- a/lib/src/models/config/toolbar/buttons/color.dart +++ b/lib/src/models/config/toolbar/buttons/color.dart @@ -1,7 +1,7 @@ import 'package:flutter/widgets.dart' show Color; -import './../../shared_configurations.dart' show QuillSharedConfigurations; import '../../../../widgets/controller.dart'; +import './../../shared_configurations.dart' show QuillSharedConfigurations; import 'base.dart'; class QuillToolbarColorButtonExtraOptions diff --git a/lib/src/models/rules/delete.dart b/lib/src/models/rules/delete.dart index 9757bc45..a6718445 100644 --- a/lib/src/models/rules/delete.dart +++ b/lib/src/models/rules/delete.dart @@ -97,10 +97,8 @@ class PreserveLineStyleOnMergeRule extends DeleteRule { continue; } - var attributes = op.attributes == null - ? null - : op.attributes!.map( - (key, dynamic value) => MapEntry(key, null)); + var attributes = op.attributes?.map( + (key, dynamic value) => MapEntry(key, null)); if (isNotPlain) { attributes ??= {}; diff --git a/lib/src/widgets/proxy.dart b/lib/src/widgets/proxy.dart index e6475fdd..698497f4 100644 --- a/lib/src/widgets/proxy.dart +++ b/lib/src/widgets/proxy.dart @@ -129,22 +129,24 @@ class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox { class RichTextProxy extends SingleChildRenderObjectWidget { /// Child argument should be an instance of RichText widget. - const RichTextProxy( - {required RichText super.child, - required this.textStyle, - required this.textAlign, - required this.textDirection, - required this.locale, - required this.strutStyle, - this.textScaleFactor = 1.0, - this.textWidthBasis = TextWidthBasis.parent, - this.textHeightBehavior, - super.key}); + const RichTextProxy({ + required RichText super.child, + required this.textStyle, + required this.textAlign, + required this.textDirection, + required this.locale, + required this.strutStyle, + // TODO: This might needs to be updated, previous value was 1.0 using `textScaleFactor` + this.textScaler = const TextScaler.linear(1), + this.textWidthBasis = TextWidthBasis.parent, + this.textHeightBehavior, + super.key, + }); final TextStyle textStyle; final TextAlign textAlign; final TextDirection textDirection; - final double textScaleFactor; + final TextScaler textScaler; final Locale locale; final StrutStyle strutStyle; final TextWidthBasis textWidthBasis; @@ -152,16 +154,8 @@ class RichTextProxy extends SingleChildRenderObjectWidget { @override RenderParagraphProxy createRenderObject(BuildContext context) { - return RenderParagraphProxy( - null, - textStyle, - textAlign, - textDirection, - textScaleFactor, - strutStyle, - locale, - textWidthBasis, - textHeightBehavior); + return RenderParagraphProxy(null, textStyle, textAlign, textDirection, + textScaler, strutStyle, locale, textWidthBasis, textHeightBehavior); } @override @@ -171,7 +165,7 @@ class RichTextProxy extends SingleChildRenderObjectWidget { ..textStyle = textStyle ..textAlign = textAlign ..textDirection = textDirection - ..textScaleFactor = textScaleFactor + ..textScaler = textScaler ..locale = locale ..strutStyle = strutStyle ..textWidthBasis = textWidthBasis @@ -186,20 +180,21 @@ class RenderParagraphProxy extends RenderProxyBox TextStyle textStyle, TextAlign textAlign, TextDirection textDirection, - double textScaleFactor, + TextScaler textScaler, StrutStyle strutStyle, Locale locale, TextWidthBasis textWidthBasis, TextHeightBehavior? textHeightBehavior, ) : _prototypePainter = TextPainter( - text: TextSpan(text: ' ', style: textStyle), - textAlign: textAlign, - textDirection: textDirection, - textScaleFactor: textScaleFactor, - strutStyle: strutStyle, - locale: locale, - textWidthBasis: textWidthBasis, - textHeightBehavior: textHeightBehavior); + text: TextSpan(text: ' ', style: textStyle), + textAlign: textAlign, + textDirection: textDirection, + textScaler: textScaler, + strutStyle: strutStyle, + locale: locale, + textWidthBasis: textWidthBasis, + textHeightBehavior: textHeightBehavior, + ); final TextPainter _prototypePainter; @@ -227,11 +222,11 @@ class RenderParagraphProxy extends RenderProxyBox markNeedsLayout(); } - set textScaleFactor(double value) { - if (_prototypePainter.textScaleFactor == value) { + set textScaler(TextScaler value) { + if (_prototypePainter.textScaler == value) { return; } - _prototypePainter.textScaleFactor = value; + _prototypePainter.textScaler = value; markNeedsLayout(); } diff --git a/lib/src/widgets/raw_editor/raw_editor.dart b/lib/src/widgets/raw_editor/raw_editor.dart index dc13082f..c0ecf52d 100644 --- a/lib/src/widgets/raw_editor/raw_editor.dart +++ b/lib/src/widgets/raw_editor/raw_editor.dart @@ -1,1628 +1,51 @@ -import 'dart:async' show StreamSubscription; -import 'dart:convert' show jsonDecode; -import 'dart:math' as math; -import 'dart:ui' as ui hide TextStyle; - -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart' show defaultTargetPlatform; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart' show RenderAbstractViewport; -import 'package:flutter/scheduler.dart' show SchedulerBinding; -import 'package:flutter/services.dart' - show - LogicalKeyboardKey, - RawKeyDownEvent, - HardwareKeyboard, - Clipboard, - ClipboardData, - TextInputControl; -import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart' - show KeyboardVisibilityController; -import 'package:pasteboard/pasteboard.dart' show Pasteboard; - -import '../../models/documents/attribute.dart'; -import '../../models/documents/document.dart'; -import '../../models/documents/nodes/block.dart'; -import '../../models/documents/nodes/embeddable.dart'; -import '../../models/documents/nodes/leaf.dart' as leaf; -import '../../models/documents/nodes/line.dart'; -import '../../models/documents/nodes/node.dart'; -import '../../models/structs/offset_value.dart'; -import '../../models/structs/vertical_spacing.dart'; -import '../../utils/cast.dart'; -import '../../utils/delta.dart'; -import '../../utils/embeds.dart'; -import '../../utils/platform.dart'; -import '../controller.dart'; -import '../cursor.dart'; -import '../default_styles.dart'; -import '../editor/editor.dart'; -import '../keyboard_listener.dart'; -import '../link.dart'; -import '../proxy.dart'; -import '../quill_single_child_scroll_view.dart'; -import '../text_block.dart'; -import '../text_line.dart'; -import '../text_selection.dart'; -import 'raw_editor.dart'; -import 'raw_editor_actions.dart'; -import 'raw_editor_render_object.dart'; -import 'raw_editor_state_selection_delegate_mixin.dart'; -import 'raw_editor_state_text_input_client_mixin.dart'; -import 'raw_editor_text_boundaries.dart'; - -class QuillRawEditorState extends EditorState - with - AutomaticKeepAliveClientMixin, - WidgetsBindingObserver, - TickerProviderStateMixin, - RawEditorStateTextInputClientMixin, - RawEditorStateSelectionDelegateMixin { - final GlobalKey _editorKey = GlobalKey(); - - KeyboardVisibilityController? _keyboardVisibilityController; - StreamSubscription? _keyboardVisibilitySubscription; - bool _keyboardVisible = false; - - // Selection overlay - @override - EditorTextSelectionOverlay? get selectionOverlay => _selectionOverlay; - EditorTextSelectionOverlay? _selectionOverlay; - - @override - ScrollController get scrollController => _scrollController; - late ScrollController _scrollController; - - // Cursors - late CursorCont _cursorCont; - - QuillController get controller => widget.configurations.controller; - - // Focus - bool _didAutoFocus = false; - - bool get _hasFocus => widget.configurations.focusNode.hasFocus; - - // Theme - DefaultStyles? _styles; - - // for pasting style - @override - List get pasteStyleAndEmbed => _pasteStyleAndEmbed; - List _pasteStyleAndEmbed = []; - - @override - String get pastePlainText => _pastePlainText; - String _pastePlainText = ''; - - final ClipboardStatusNotifier _clipboardStatus = ClipboardStatusNotifier(); - final LayerLink _toolbarLayerLink = LayerLink(); - final LayerLink _startHandleLayerLink = LayerLink(); - final LayerLink _endHandleLayerLink = LayerLink(); - - TextDirection get _textDirection => Directionality.of(context); - - @override - bool get dirty => _dirty; - bool _dirty = false; - - @override - void insertContent(KeyboardInsertedContent content) { - assert(widget.configurations.contentInsertionConfiguration?.allowedMimeTypes - .contains(content.mimeType) ?? - false); - widget.configurations.contentInsertionConfiguration?.onContentInserted - .call(content); - } - - /// Returns the [ContextMenuButtonItem]s representing the buttons in this - /// platform's default selection menu for [QuillRawEditor]. - /// - /// Copied from [EditableTextState]. - List get contextMenuButtonItems { - return EditableText.getEditableButtonItems( - clipboardStatus: _clipboardStatus.value, - onLiveTextInput: null, - onCopy: copyEnabled - ? () => copySelection(SelectionChangedCause.toolbar) - : null, - onCut: - cutEnabled ? () => cutSelection(SelectionChangedCause.toolbar) : null, - onPaste: - pasteEnabled ? () => pasteText(SelectionChangedCause.toolbar) : null, - onSelectAll: selectAllEnabled - ? () => selectAll(SelectionChangedCause.toolbar) - : null, - onLookUp: null, - onSearchWeb: null, - onShare: null, - ); - } - - /// Returns the anchor points for the default context menu. - /// - /// Copied from [EditableTextState]. - TextSelectionToolbarAnchors get contextMenuAnchors { - final glyphHeights = _getGlyphHeights(); - final selection = textEditingValue.selection; - final points = renderEditor.getEndpointsForSelection(selection); - return TextSelectionToolbarAnchors.fromSelection( - renderBox: renderEditor, - startGlyphHeight: glyphHeights.startGlyphHeight, - endGlyphHeight: glyphHeights.endGlyphHeight, - selectionEndpoints: points, - ); - } - - /// Gets the line heights at the start and end of the selection for the given - /// [QuillRawEditorState]. - /// - /// Copied from [EditableTextState]. - QuillEditorGlyphHeights _getGlyphHeights() { - final selection = textEditingValue.selection; - - // Only calculate handle rects if the text in the previous frame - // is the same as the text in the current frame. This is done because - // widget.renderObject contains the renderEditable from the previous frame. - // If the text changed between the current and previous frames then - // widget.renderObject.getRectForComposingRange might fail. In cases where - // the current frame is different from the previous we fall back to - // renderObject.preferredLineHeight. - final prevText = renderEditor.document.toPlainText(); - final currText = textEditingValue.text; - if (prevText != currText || !selection.isValid || selection.isCollapsed) { - return QuillEditorGlyphHeights( - renderEditor.preferredLineHeight(selection.base), - renderEditor.preferredLineHeight(selection.base), - ); - } - - final startCharacterRect = - renderEditor.getLocalRectForCaret(selection.base); - final endCharacterRect = - renderEditor.getLocalRectForCaret(selection.extent); - return QuillEditorGlyphHeights( - startCharacterRect.height, - endCharacterRect.height, - ); - } - - void _defaultOnTapOutside(PointerDownEvent event) { - /// The focus dropping behavior is only present on desktop platforms - /// and mobile browsers. - switch (defaultTargetPlatform) { - case TargetPlatform.android: - case TargetPlatform.iOS: - case TargetPlatform.fuchsia: - // On mobile platforms, we don't unfocus on touch events unless they're - // in the web browser, but we do unfocus for all other kinds of events. - switch (event.kind) { - case ui.PointerDeviceKind.touch: - break; - case ui.PointerDeviceKind.mouse: - case ui.PointerDeviceKind.stylus: - case ui.PointerDeviceKind.invertedStylus: - case ui.PointerDeviceKind.unknown: - widget.configurations.focusNode.unfocus(); - break; - case ui.PointerDeviceKind.trackpad: - throw UnimplementedError( - 'Unexpected pointer down event for trackpad.', - ); - } - break; - case TargetPlatform.linux: - case TargetPlatform.macOS: - case TargetPlatform.windows: - widget.configurations.focusNode.unfocus(); - break; - default: - throw UnsupportedError( - 'The platform ${defaultTargetPlatform.name} is not supported in the' - ' _defaultOnTapOutside()', - ); - } - } - - @override - Widget build(BuildContext context) { - assert(debugCheckHasMediaQuery(context)); - super.build(context); - - var doc = controller.document; - if (doc.isEmpty() && widget.configurations.placeholder != null) { - final raw = widget.configurations.placeholder?.replaceAll(r'"', '\\"'); - doc = Document.fromJson( - jsonDecode( - '[{"attributes":{"placeholder":true},"insert":"$raw\\n"}]', - ), - ); - } - - Widget child = CompositedTransformTarget( - link: _toolbarLayerLink, - child: Semantics( - child: MouseRegion( - cursor: SystemMouseCursors.text, - child: QuilRawEditorMultiChildRenderObject( - key: _editorKey, - document: doc, - selection: controller.selection, - hasFocus: _hasFocus, - scrollable: widget.configurations.scrollable, - cursorController: _cursorCont, - textDirection: _textDirection, - startHandleLayerLink: _startHandleLayerLink, - endHandleLayerLink: _endHandleLayerLink, - onSelectionChanged: _handleSelectionChanged, - onSelectionCompleted: _handleSelectionCompleted, - scrollBottomInset: widget.configurations.scrollBottomInset, - padding: widget.configurations.padding, - maxContentWidth: widget.configurations.maxContentWidth, - floatingCursorDisabled: - widget.configurations.floatingCursorDisabled, - children: _buildChildren(doc, context), - ), - ), - ), - ); - - if (widget.configurations.scrollable) { - /// Since [SingleChildScrollView] does not implement - /// `computeDistanceToActualBaseline` it prevents the editor from - /// providing its baseline metrics. To address this issue we wrap - /// the scroll view with [BaselineProxy] which mimics the editor's - /// baseline. - // This implies that the first line has no styles applied to it. - final baselinePadding = - EdgeInsets.only(top: _styles!.paragraph!.verticalSpacing.top); - child = BaselineProxy( - textStyle: _styles!.paragraph!.style, - padding: baselinePadding, - child: QuillSingleChildScrollView( - controller: _scrollController, - physics: widget.configurations.scrollPhysics, - viewportBuilder: (_, offset) => CompositedTransformTarget( - link: _toolbarLayerLink, - child: MouseRegion( - cursor: SystemMouseCursors.text, - child: QuilRawEditorMultiChildRenderObject( - key: _editorKey, - offset: offset, - document: doc, - selection: controller.selection, - hasFocus: _hasFocus, - scrollable: widget.configurations.scrollable, - textDirection: _textDirection, - startHandleLayerLink: _startHandleLayerLink, - endHandleLayerLink: _endHandleLayerLink, - onSelectionChanged: _handleSelectionChanged, - onSelectionCompleted: _handleSelectionCompleted, - scrollBottomInset: widget.configurations.scrollBottomInset, - padding: widget.configurations.padding, - maxContentWidth: widget.configurations.maxContentWidth, - cursorController: _cursorCont, - floatingCursorDisabled: - widget.configurations.floatingCursorDisabled, - children: _buildChildren(doc, context), - ), - ), - ), - ), - ); - } - - final constraints = widget.configurations.expands - ? const BoxConstraints.expand() - : BoxConstraints( - minHeight: widget.configurations.minHeight ?? 0.0, - maxHeight: widget.configurations.maxHeight ?? double.infinity, - ); - - // Please notice that this change will make the check fixed - // so if we ovveride the platform in material app theme data - // it will not depend on it and doesn't change here but I don't think - // we need to - final isDesktopMacOS = isMacOS(supportWeb: true); - - return TextFieldTapRegion( - enabled: widget.configurations.isOnTapOutsideEnabled, - onTapOutside: (event) { - final onTapOutside = widget.configurations.onTapOutside; - if (onTapOutside != null) { - onTapOutside.call(event, widget.configurations.focusNode); - return; - } - _defaultOnTapOutside(event); - }, - child: QuillStyles( - data: _styles!, - child: Shortcuts( - shortcuts: mergeMaps({ - // shortcuts added for Desktop platforms. - const SingleActivator( - LogicalKeyboardKey.escape, - ): const HideSelectionToolbarIntent(), - SingleActivator( - LogicalKeyboardKey.keyZ, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const UndoTextIntent(SelectionChangedCause.keyboard), - SingleActivator( - LogicalKeyboardKey.keyY, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const RedoTextIntent(SelectionChangedCause.keyboard), - - // Selection formatting. - SingleActivator( - LogicalKeyboardKey.keyB, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const ToggleTextStyleIntent(Attribute.bold), - SingleActivator( - LogicalKeyboardKey.keyU, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const ToggleTextStyleIntent(Attribute.underline), - SingleActivator( - LogicalKeyboardKey.keyI, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const ToggleTextStyleIntent(Attribute.italic), - SingleActivator( - LogicalKeyboardKey.keyS, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - shift: true, - ): const ToggleTextStyleIntent(Attribute.strikeThrough), - SingleActivator( - LogicalKeyboardKey.backquote, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const ToggleTextStyleIntent(Attribute.inlineCode), - SingleActivator( - LogicalKeyboardKey.tilde, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - shift: true, - ): const ToggleTextStyleIntent(Attribute.codeBlock), - SingleActivator( - LogicalKeyboardKey.keyB, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - shift: true, - ): const ToggleTextStyleIntent(Attribute.blockQuote), - SingleActivator( - LogicalKeyboardKey.keyK, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const QuillEditorApplyLinkIntent(), - - // Lists - SingleActivator( - LogicalKeyboardKey.keyL, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - shift: true, - ): const ToggleTextStyleIntent(Attribute.ul), - SingleActivator( - LogicalKeyboardKey.keyO, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - shift: true, - ): const ToggleTextStyleIntent(Attribute.ol), - SingleActivator( - LogicalKeyboardKey.keyC, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - shift: true, - ): const QuillEditorApplyCheckListIntent(), - - // Indents - SingleActivator( - LogicalKeyboardKey.keyM, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const IndentSelectionIntent(true), - SingleActivator( - LogicalKeyboardKey.keyM, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - shift: true, - ): const IndentSelectionIntent(false), - - // Headers - SingleActivator( - LogicalKeyboardKey.digit1, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const QuillEditorApplyHeaderIntent(Attribute.h1), - SingleActivator( - LogicalKeyboardKey.digit2, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const QuillEditorApplyHeaderIntent(Attribute.h2), - SingleActivator( - LogicalKeyboardKey.digit3, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const QuillEditorApplyHeaderIntent(Attribute.h3), - SingleActivator( - LogicalKeyboardKey.digit0, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const QuillEditorApplyHeaderIntent(Attribute.header), - - SingleActivator( - LogicalKeyboardKey.keyG, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const QuillEditorInsertEmbedIntent(Attribute.image), - - SingleActivator( - LogicalKeyboardKey.keyF, - control: !isDesktopMacOS, - meta: isDesktopMacOS, - ): const OpenSearchIntent(), - }, { - ...?widget.configurations.customShortcuts - }), - child: Actions( - actions: mergeMaps>(_actions, { - ...?widget.configurations.customActions, - }), - child: Focus( - focusNode: widget.configurations.focusNode, - onKey: _onKey, - child: QuillKeyboardListener( - child: Container( - constraints: constraints, - child: child, - ), - ), - ), - ), - ), - ), - ); - } - - KeyEventResult _onKey(node, RawKeyEvent event) { - // Don't handle key if there is a meta key pressed. - if (event.isAltPressed || event.isControlPressed || event.isMetaPressed) { - return KeyEventResult.ignored; - } - - if (event is! RawKeyDownEvent) { - return KeyEventResult.ignored; - } - // Handle indenting blocks when pressing the tab key. - if (event.logicalKey == LogicalKeyboardKey.tab) { - return _handleTabKey(event); - } - - // Don't handle key if there is an active selection. - if (controller.selection.baseOffset != controller.selection.extentOffset) { - return KeyEventResult.ignored; - } - - // Handle inserting lists when space is pressed following - // a list initiating phrase. - if (event.logicalKey == LogicalKeyboardKey.space) { - return _handleSpaceKey(event); - } - - return KeyEventResult.ignored; - } - - KeyEventResult _handleSpaceKey(RawKeyEvent event) { - final child = - controller.document.queryChild(controller.selection.baseOffset); - if (child.node == null) { - return KeyEventResult.ignored; - } - - final line = child.node as Line?; - if (line == null) { - return KeyEventResult.ignored; - } - - final text = castOrNull(line.first); - if (text == null) { - return KeyEventResult.ignored; - } - - const olKeyPhrase = '1.'; - const ulKeyPhrase = '-'; - - if (text.value == olKeyPhrase) { - _updateSelectionForKeyPhrase(olKeyPhrase, Attribute.ol); - } else if (text.value == ulKeyPhrase) { - _updateSelectionForKeyPhrase(ulKeyPhrase, Attribute.ul); - } else { - return KeyEventResult.ignored; - } - - return KeyEventResult.handled; - } - - KeyEventResult _handleTabKey(RawKeyEvent event) { - final child = - controller.document.queryChild(controller.selection.baseOffset); - - KeyEventResult insertTabCharacter() { - if (widget.configurations.isReadOnly) { - return KeyEventResult.ignored; - } - controller.replaceText(controller.selection.baseOffset, 0, '\t', null); - _moveCursor(1); - return KeyEventResult.handled; - } - - if (controller.selection.baseOffset != controller.selection.extentOffset) { - if (child.node == null || child.node!.parent == null) { - return KeyEventResult.handled; - } - final parentBlock = child.node!.parent!; - if (parentBlock.style.containsKey(Attribute.ol.key) || - parentBlock.style.containsKey(Attribute.ul.key) || - parentBlock.style.containsKey(Attribute.checked.key)) { - controller.indentSelection(!event.isShiftPressed); - } - return KeyEventResult.handled; - } - - if (child.node == null) { - return insertTabCharacter(); - } - - final node = child.node!; - - final parent = node.parent; - if (parent == null || parent is! Block) { - return insertTabCharacter(); - } - - if (node is! Line || (node.isNotEmpty && node.first is! leaf.QuillText)) { - return insertTabCharacter(); - } - - final parentBlock = parent; - if (parentBlock.style.containsKey(Attribute.ol.key) || - parentBlock.style.containsKey(Attribute.ul.key) || - parentBlock.style.containsKey(Attribute.checked.key)) { - if (node.isNotEmpty && - (node.first as leaf.QuillText).value.isNotEmpty && - controller.selection.base.offset > node.documentOffset) { - return insertTabCharacter(); - } - controller.indentSelection(!event.isShiftPressed); - return KeyEventResult.handled; - } - - if (node.isNotEmpty && (node.first as leaf.QuillText).value.isNotEmpty) { - return insertTabCharacter(); - } - - return insertTabCharacter(); - } - - void _moveCursor(int chars) { - final selection = controller.selection; - controller.updateSelection( - controller.selection.copyWith( - baseOffset: selection.baseOffset + chars, - extentOffset: selection.baseOffset + chars), - ChangeSource.local); - } - - void _updateSelectionForKeyPhrase(String phrase, Attribute attribute) { - controller.replaceText(controller.selection.baseOffset - phrase.length, - phrase.length, '\n', null); - _moveCursor(-phrase.length); - controller - ..formatSelection(attribute) - // Remove the added newline. - ..replaceText(controller.selection.baseOffset + 1, 1, '', null); - } - - void _handleSelectionChanged( - TextSelection selection, - SelectionChangedCause cause, - ) { - final oldSelection = controller.selection; - controller.updateSelection(selection, ChangeSource.local); - - _selectionOverlay?.handlesVisible = _shouldShowSelectionHandles(); - - if (!_keyboardVisible) { - // This will show the keyboard for all selection changes on the - // editor, not just changes triggered by user gestures. - requestKeyboard(); - } - - if (cause == SelectionChangedCause.drag) { - // When user updates the selection while dragging make sure to - // bring the updated position (base or extent) into view. - if (oldSelection.baseOffset != selection.baseOffset) { - bringIntoView(selection.base); - } else if (oldSelection.extentOffset != selection.extentOffset) { - bringIntoView(selection.extent); - } - } - } - - void _handleSelectionCompleted() { - controller.onSelectionCompleted?.call(); - } - - /// Updates the checkbox positioned at [offset] in document - /// by changing its attribute according to [value]. - void _handleCheckboxTap(int offset, bool value) { - final requestKeyboardFocusOnCheckListChanged = - widget.configurations.requestKeyboardFocusOnCheckListChanged; - if (!widget.configurations.isReadOnly) { - _disableScrollControllerAnimateOnce = true; - final currentSelection = controller.selection.copyWith(); - final attribute = value ? Attribute.checked : Attribute.unchecked; - - _markNeedsBuild(); - controller - ..ignoreFocusOnTextChange = true - ..skipRequestKeyboard = !requestKeyboardFocusOnCheckListChanged - ..formatText(offset, 0, attribute) - - // Checkbox tapping causes controller.selection to go to offset 0 - // Stop toggling those two toolbar buttons - ..toolbarButtonToggler = { - Attribute.list.key: attribute, - Attribute.header.key: Attribute.header - }; - - // Go back from offset 0 to current selection - SchedulerBinding.instance.addPostFrameCallback((_) { - controller - ..ignoreFocusOnTextChange = false - ..skipRequestKeyboard = !requestKeyboardFocusOnCheckListChanged - ..updateSelection(currentSelection, ChangeSource.local); - }); - } - } - - List _buildChildren(Document doc, BuildContext context) { - final result = []; - final indentLevelCounts = {}; - // this need for several ordered list in document - // we need to reset indents Map, if list finished - // List finished when there is node without Attribute.ol in styles - // So in this case we set clearIndents=true and send it - // to the next EditableTextBlock - var prevNodeOl = false; - var clearIndents = false; - - for (final node in doc.root.children) { - final attrs = node.style.attributes; - - if (prevNodeOl && attrs[Attribute.list.key] != Attribute.ol) { - clearIndents = true; - } - - prevNodeOl = attrs[Attribute.list.key] == Attribute.ol; - - if (node is Line) { - final editableTextLine = _getEditableTextLineFromNode(node, context); - result.add(Directionality( - textDirection: getDirectionOfNode(node), child: editableTextLine)); - } else if (node is Block) { - final editableTextBlock = EditableTextBlock( - block: node, - controller: controller, - textDirection: getDirectionOfNode(node), - scrollBottomInset: widget.configurations.scrollBottomInset, - verticalSpacing: _getVerticalSpacingForBlock(node, _styles), - textSelection: controller.selection, - color: widget.configurations.selectionColor, - styles: _styles, - enableInteractiveSelection: - widget.configurations.enableInteractiveSelection, - hasFocus: _hasFocus, - contentPadding: attrs.containsKey(Attribute.codeBlock.key) - ? const EdgeInsets.all(16) - : null, - embedBuilder: widget.configurations.embedBuilder, - linkActionPicker: _linkActionPicker, - onLaunchUrl: widget.configurations.onLaunchUrl, - cursorCont: _cursorCont, - indentLevelCounts: indentLevelCounts, - clearIndents: clearIndents, - onCheckboxTap: _handleCheckboxTap, - readOnly: widget.configurations.isReadOnly, - customStyleBuilder: widget.configurations.customStyleBuilder, - customLinkPrefixes: widget.configurations.customLinkPrefixes, - ); - result.add( - Directionality( - textDirection: getDirectionOfNode(node), - child: editableTextBlock, - ), - ); - - clearIndents = false; - } else { - _dirty = false; - throw StateError('Unreachable.'); - } - } - _dirty = false; - return result; - } - - EditableTextLine _getEditableTextLineFromNode( - Line node, BuildContext context) { - final textLine = TextLine( - line: node, - textDirection: _textDirection, - embedBuilder: widget.configurations.embedBuilder, - customStyleBuilder: widget.configurations.customStyleBuilder, - customRecognizerBuilder: widget.configurations.customRecognizerBuilder, - styles: _styles!, - readOnly: widget.configurations.isReadOnly, - controller: controller, - linkActionPicker: _linkActionPicker, - onLaunchUrl: widget.configurations.onLaunchUrl, - customLinkPrefixes: widget.configurations.customLinkPrefixes, - ); - final editableTextLine = EditableTextLine( - node, - null, - textLine, - 0, - _getVerticalSpacingForLine(node, _styles), - _textDirection, - controller.selection, - widget.configurations.selectionColor, - widget.configurations.enableInteractiveSelection, - _hasFocus, - MediaQuery.devicePixelRatioOf(context), - _cursorCont); - return editableTextLine; - } - - VerticalSpacing _getVerticalSpacingForLine( - Line line, - DefaultStyles? defaultStyles, - ) { - final attrs = line.style.attributes; - if (attrs.containsKey(Attribute.header.key)) { - int level; - if (attrs[Attribute.header.key]!.value is double) { - level = attrs[Attribute.header.key]!.value.toInt(); - } else { - level = attrs[Attribute.header.key]!.value; - } - switch (level) { - case 1: - return defaultStyles!.h1!.verticalSpacing; - case 2: - return defaultStyles!.h2!.verticalSpacing; - case 3: - return defaultStyles!.h3!.verticalSpacing; - default: - throw ArgumentError('Invalid level $level'); - } - } - - return defaultStyles!.paragraph!.verticalSpacing; - } - - VerticalSpacing _getVerticalSpacingForBlock( - Block node, DefaultStyles? defaultStyles) { - final attrs = node.style.attributes; - if (attrs.containsKey(Attribute.blockQuote.key)) { - return defaultStyles!.quote!.verticalSpacing; - } else if (attrs.containsKey(Attribute.codeBlock.key)) { - return defaultStyles!.code!.verticalSpacing; - } else if (attrs.containsKey(Attribute.indent.key)) { - return defaultStyles!.indent!.verticalSpacing; - } else if (attrs.containsKey(Attribute.list.key)) { - return defaultStyles!.lists!.verticalSpacing; - } else if (attrs.containsKey(Attribute.align.key)) { - return defaultStyles!.align!.verticalSpacing; - } - return const VerticalSpacing(0, 0); - } - - void _didChangeTextEditingValueListener() { - _didChangeTextEditingValue(controller.ignoreFocusOnTextChange); - } - - @override - void initState() { - super.initState(); - - _clipboardStatus.addListener(_onChangedClipboardStatus); - - controller.addListener(_didChangeTextEditingValueListener); - - _scrollController = widget.configurations.scrollController; - _scrollController.addListener(_updateSelectionOverlayForScroll); - - _cursorCont = CursorCont( - show: ValueNotifier(widget.configurations.showCursor), - style: widget.configurations.cursorStyle, - tickerProvider: this, - ); - - // Floating cursor - _floatingCursorResetController = AnimationController(vsync: this); - _floatingCursorResetController.addListener(onFloatingCursorResetTick); - - if (isKeyboardOS(supportWeb: true)) { - _keyboardVisible = true; - } else if (!isWeb() && isFlutterTest()) { - // treat tests like a keyboard OS - _keyboardVisible = true; - } else { - // treat iOS Simulator like a keyboard OS - isIOSSimulator().then((isIosSimulator) { - if (isIosSimulator) { - _keyboardVisible = true; - } else { - _keyboardVisibilityController = KeyboardVisibilityController(); - _keyboardVisible = _keyboardVisibilityController!.isVisible; - _keyboardVisibilitySubscription = - _keyboardVisibilityController?.onChange.listen((visible) { - _keyboardVisible = visible; - if (visible) { - _onChangeTextEditingValue(!_hasFocus); - } - }); - - HardwareKeyboard.instance.addHandler(_hardwareKeyboardEvent); - } - }); - } - - // Focus - widget.configurations.focusNode.addListener(_handleFocusChanged); - } - - // KeyboardVisibilityController only checks for keyboards that - // adjust the screen size. Also watch for hardware keyboards - // that don't alter the screen (i.e. Chromebook, Android tablet - // and any hardware keyboards from an OS not listed in isKeyboardOS()) - bool _hardwareKeyboardEvent(KeyEvent _) { - if (!_keyboardVisible) { - // hardware keyboard key pressed. Set visibility to true - _keyboardVisible = true; - // update the editor - _onChangeTextEditingValue(!_hasFocus); - } - - // remove the key handler - it's no longer needed. If - // KeyboardVisibilityController clears visibility, it wil - // also enable it when appropriate. - HardwareKeyboard.instance.removeHandler(_hardwareKeyboardEvent); - - // we didn't handle the event, just needed to know a key was pressed - return false; - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final parentStyles = QuillStyles.getStyles(context, true); - final defaultStyles = DefaultStyles.getInstance(context); - _styles = (parentStyles != null) - ? defaultStyles.merge(parentStyles) - : defaultStyles; - - if (widget.configurations.customStyles != null) { - _styles = _styles!.merge(widget.configurations.customStyles!); - } - - _requestAutoFocusIfShould(); - } - - Future _requestAutoFocusIfShould() async { - final focusManager = FocusScope.of(context); - if (!_didAutoFocus && widget.configurations.autoFocus) { - await Future.delayed(Duration.zero); // To avoid exceptions - focusManager.autofocus(widget.configurations.focusNode); - _didAutoFocus = true; - } - } - - @override - void didUpdateWidget(QuillRawEditor oldWidget) { - super.didUpdateWidget(oldWidget); - - _cursorCont.show.value = widget.configurations.showCursor; - _cursorCont.style = widget.configurations.cursorStyle; - - if (controller != oldWidget.configurations.controller) { - oldWidget.configurations.controller - .removeListener(_didChangeTextEditingValue); - controller.addListener(_didChangeTextEditingValue); - updateRemoteValueIfNeeded(); - } - - if (widget.configurations.scrollController != _scrollController) { - _scrollController.removeListener(_updateSelectionOverlayForScroll); - _scrollController = widget.configurations.scrollController; - _scrollController.addListener(_updateSelectionOverlayForScroll); - } - - if (widget.configurations.focusNode != oldWidget.configurations.focusNode) { - oldWidget.configurations.focusNode.removeListener(_handleFocusChanged); - widget.configurations.focusNode.addListener(_handleFocusChanged); - updateKeepAlive(); - } - - if (controller.selection != oldWidget.configurations.controller.selection) { - _selectionOverlay?.update(textEditingValue); - } - - _selectionOverlay?.handlesVisible = _shouldShowSelectionHandles(); - if (!shouldCreateInputConnection) { - closeConnectionIfNeeded(); - } else { - if (oldWidget.configurations.isReadOnly && _hasFocus) { - openConnectionIfNeeded(); - } - } - - // in case customStyles changed in new widget - if (widget.configurations.customStyles != null) { - _styles = _styles!.merge(widget.configurations.customStyles!); - } - } - - bool _shouldShowSelectionHandles() { - return widget.configurations.showSelectionHandles && - !controller.selection.isCollapsed; - } - - @override - void dispose() { - closeConnectionIfNeeded(); - _keyboardVisibilitySubscription?.cancel(); - HardwareKeyboard.instance.removeHandler(_hardwareKeyboardEvent); - assert(!hasConnection); - _selectionOverlay?.dispose(); - _selectionOverlay = null; - controller.removeListener(_didChangeTextEditingValueListener); - widget.configurations.focusNode.removeListener(_handleFocusChanged); - _cursorCont.dispose(); - _clipboardStatus - ..removeListener(_onChangedClipboardStatus) - ..dispose(); - super.dispose(); - } - - void _updateSelectionOverlayForScroll() { - _selectionOverlay?.updateForScroll(); - } - - /// Marks the editor as dirty and trigger a rebuild. - /// - /// When the editor is dirty methods that depend on the editor - /// state being in sync with the controller know they may be - /// operating on stale data. - void _markNeedsBuild() { - if (_dirty) { - // No need to rebuilt if it already darty - return; - } - setState(() { - _dirty = true; - }); - } - - void _didChangeTextEditingValue([bool ignoreFocus = false]) { - if (isWeb()) { - _onChangeTextEditingValue(ignoreFocus); - if (!ignoreFocus) { - requestKeyboard(); - } - return; - } - - if (ignoreFocus || _keyboardVisible) { - _onChangeTextEditingValue(ignoreFocus); - } else { - requestKeyboard(); - if (mounted) { - // Use controller.value in build() - // Mark widget as dirty and trigger build and updateChildren - _markNeedsBuild(); - } - } - - _adjacentLineAction.stopCurrentVerticalRunIfSelectionChanges(); - } - - void _onChangeTextEditingValue([bool ignoreCaret = false]) { - updateRemoteValueIfNeeded(); - if (ignoreCaret) { - return; - } - _showCaretOnScreen(); - _cursorCont.startOrStopCursorTimerIfNeeded(_hasFocus, controller.selection); - if (hasConnection) { - // To keep the cursor from blinking while typing, we want to restart the - // cursor timer every time a new character is typed. - _cursorCont - ..stopCursorTimer(resetCharTicks: false) - ..startCursorTimer(); - } - - // Refresh selection overlay after the build step had a chance to - // update and register all children of RenderEditor. Otherwise this will - // fail in situations where a new line of text is entered, which adds - // a new RenderEditableBox child. If we try to update selection overlay - // immediately it'll not be able to find the new child since it hasn't been - // built yet. - SchedulerBinding.instance.addPostFrameCallback((_) { - if (!mounted) { - return; - } - _updateOrDisposeSelectionOverlayIfNeeded(); - }); - if (mounted) { - // Use controller.value in build() - // Mark widget as dirty and trigger build and updateChildren - _markNeedsBuild(); - } - } - - void _updateOrDisposeSelectionOverlayIfNeeded() { - if (_selectionOverlay != null) { - if (!_hasFocus || textEditingValue.selection.isCollapsed) { - _selectionOverlay!.dispose(); - _selectionOverlay = null; - } else { - _selectionOverlay!.update(textEditingValue); - } - } else if (_hasFocus) { - _selectionOverlay = EditorTextSelectionOverlay( - value: textEditingValue, - context: context, - debugRequiredFor: widget, - startHandleLayerLink: _startHandleLayerLink, - endHandleLayerLink: _endHandleLayerLink, - renderObject: renderEditor, - selectionCtrls: widget.configurations.selectionCtrls, - selectionDelegate: this, - clipboardStatus: _clipboardStatus, - contextMenuBuilder: widget.configurations.contextMenuBuilder == null - ? null - : (context) => - widget.configurations.contextMenuBuilder!(context, this), - ); - _selectionOverlay!.handlesVisible = _shouldShowSelectionHandles(); - _selectionOverlay!.showHandles(); - } - } - - void _handleFocusChanged() { - if (dirty) { - SchedulerBinding.instance - .addPostFrameCallback((_) => _handleFocusChanged()); - return; - } - openOrCloseConnection(); - _cursorCont.startOrStopCursorTimerIfNeeded(_hasFocus, controller.selection); - _updateOrDisposeSelectionOverlayIfNeeded(); - if (_hasFocus) { - WidgetsBinding.instance.addObserver(this); - _showCaretOnScreen(); - } else { - WidgetsBinding.instance.removeObserver(this); - } - updateKeepAlive(); - } - - void _onChangedClipboardStatus() { - if (!mounted) return; - // Inform the widget that the value of clipboardStatus has changed. - // Trigger build and updateChildren - _markNeedsBuild(); - } - - Future _linkActionPicker(Node linkNode) async { - final link = linkNode.style.attributes[Attribute.link.key]!.value!; - return widget.configurations - .linkActionPickerDelegate(context, link, linkNode); - } - - bool _showCaretOnScreenScheduled = false; - - // This is a workaround for checkbox tapping issue - // https://github.com/singerdmx/flutter-quill/issues/619 - // We cannot treat {"list": "checked"} and {"list": "unchecked"} as - // block of the same style - // This causes controller.selection to go to offset 0 - bool _disableScrollControllerAnimateOnce = false; - - void _showCaretOnScreen() { - if (!widget.configurations.showCursor || _showCaretOnScreenScheduled) { - return; - } - - _showCaretOnScreenScheduled = true; - SchedulerBinding.instance.addPostFrameCallback((_) { - if (widget.configurations.scrollable || _scrollController.hasClients) { - _showCaretOnScreenScheduled = false; - - if (!mounted) { - return; - } - - final viewport = RenderAbstractViewport.of(renderEditor); - final editorOffset = - renderEditor.localToGlobal(const Offset(0, 0), ancestor: viewport); - final offsetInViewport = _scrollController.offset + editorOffset.dy; - - final offset = renderEditor.getOffsetToRevealCursor( - _scrollController.position.viewportDimension, - _scrollController.offset, - offsetInViewport, - ); - - if (offset != null) { - if (_disableScrollControllerAnimateOnce) { - _disableScrollControllerAnimateOnce = false; - return; - } - _scrollController.animateTo( - math.min(offset, _scrollController.position.maxScrollExtent), - duration: const Duration(milliseconds: 100), - curve: Curves.fastOutSlowIn, - ); - } - } - }); - } - - /// The renderer for this widget's editor descendant. - /// - /// This property is typically used to notify the renderer of input gestures. - @override - RenderEditor get renderEditor => - _editorKey.currentContext!.findRenderObject() as RenderEditor; - - /// Express interest in interacting with the keyboard. - /// - /// If this control is already attached to the keyboard, this function will - /// request that the keyboard become visible. Otherwise, this function will - /// ask the focus system that it become focused. If successful in acquiring - /// focus, the control will then attach to the keyboard and request that the - /// keyboard become visible. - @override - void requestKeyboard() { - if (controller.skipRequestKeyboard) { - // and that just by one simple change - controller.skipRequestKeyboard = false; - return; - } - if (_hasFocus) { - final keyboardAlreadyShown = _keyboardVisible; - openConnectionIfNeeded(); - if (!keyboardAlreadyShown) { - /// delay 500 milliseconds for waiting keyboard show up - Future.delayed( - const Duration(milliseconds: 500), - _showCaretOnScreen, - ); - } else { - _showCaretOnScreen(); - } - } else { - widget.configurations.focusNode.requestFocus(); - } - } - - /// Shows the selection toolbar at the location of the current cursor. - /// - /// Returns `false` if a toolbar couldn't be shown, such as when the toolbar - /// is already shown, or when no text selection currently exists. - @override - bool showToolbar() { - // Web is using native dom elements to enable clipboard functionality of the - // toolbar: copy, paste, select, cut. It might also provide additional - // functionality depending on the browser (such as translate). Due to this - // we should not show a Flutter toolbar for the editable text elements. - if (isWeb()) { - return false; - } - - // selectionOverlay is aggressively released when selection is collapsed - // to remove unnecessary handles. Since a toolbar is requested here, - // attempt to create the selectionOverlay if it's not already created. - if (_selectionOverlay == null) { - _updateOrDisposeSelectionOverlayIfNeeded(); - } - - if (_selectionOverlay == null || _selectionOverlay!.toolbar != null) { - return false; - } - - _selectionOverlay!.update(textEditingValue); - _selectionOverlay!.showToolbar(); - return true; - } - - void _replaceText(ReplaceTextIntent intent) { - userUpdateTextEditingValue( - intent.currentTextEditingValue - .replaced(intent.replacementRange, intent.replacementText), - intent.cause, - ); - } - - /// Copy current selection to [Clipboard]. - @override - void copySelection(SelectionChangedCause cause) { - controller.copiedImageUrl = null; - _pastePlainText = controller.getPlainText(); - _pasteStyleAndEmbed = controller.getAllIndividualSelectionStylesAndEmbed(); - - final selection = textEditingValue.selection; - final text = textEditingValue.text; - if (selection.isCollapsed) { - return; - } - Clipboard.setData(ClipboardData(text: selection.textInside(text))); - - if (cause == SelectionChangedCause.toolbar) { - bringIntoView(textEditingValue.selection.extent); - - // Collapse the selection and hide the toolbar and handles. - userUpdateTextEditingValue( - TextEditingValue( - text: textEditingValue.text, - selection: - TextSelection.collapsed(offset: textEditingValue.selection.end), - ), - SelectionChangedCause.toolbar, - ); - } - } - - /// Cut current selection to [Clipboard]. - @override - void cutSelection(SelectionChangedCause cause) { - controller.copiedImageUrl = null; - _pastePlainText = controller.getPlainText(); - _pasteStyleAndEmbed = controller.getAllIndividualSelectionStylesAndEmbed(); - - if (widget.configurations.isReadOnly) { - return; - } - final selection = textEditingValue.selection; - final text = textEditingValue.text; - if (selection.isCollapsed) { - return; - } - Clipboard.setData(ClipboardData(text: selection.textInside(text))); - _replaceText(ReplaceTextIntent(textEditingValue, '', selection, cause)); - - if (cause == SelectionChangedCause.toolbar) { - bringIntoView(textEditingValue.selection.extent); - hideToolbar(); - } - } - - /// Paste text from [Clipboard]. - @override - Future pasteText(SelectionChangedCause cause) async { - if (widget.configurations.isReadOnly) { - return; - } - - if (controller.copiedImageUrl != null) { - final index = textEditingValue.selection.baseOffset; - final length = textEditingValue.selection.extentOffset - index; - final copied = controller.copiedImageUrl!; - controller.replaceText( - index, - length, - BlockEmbed.image(copied.url), - null, - ); - if (copied.styleString.isNotEmpty) { - controller.formatText( - getEmbedNode(controller, index + 1).offset, - 1, - StyleAttribute(copied.styleString), - ); - } - controller.copiedImageUrl = null; - await Clipboard.setData( - const ClipboardData(text: ''), - ); - return; - } - - final selection = textEditingValue.selection; - if (!selection.isValid) { - return; - } - // Snapshot the input before using `await`. - // See https://github.com/flutter/flutter/issues/11427 - final text = await Clipboard.getData(Clipboard.kTextPlain); - if (text != null) { - _replaceText( - ReplaceTextIntent( - textEditingValue, - text.text!, - selection, - cause, - ), - ); - - bringIntoView(textEditingValue.selection.extent); - - // Collapse the selection and hide the toolbar and handles. - userUpdateTextEditingValue( - TextEditingValue( - text: textEditingValue.text, - selection: TextSelection.collapsed( - offset: textEditingValue.selection.end, - ), - ), - cause, - ); - - return; - } - - final onImagePaste = widget.configurations.onImagePaste; - if (onImagePaste != null) { - final image = await Pasteboard.image; - - if (image == null) { - return; - } - - final imageUrl = await onImagePaste(image); - if (imageUrl == null) { - return; - } - - controller.replaceText( - textEditingValue.selection.end, - 0, - BlockEmbed.image(imageUrl), - null, - ); - } - } - - /// Select the entire text value. - @override - void selectAll(SelectionChangedCause cause) { - userUpdateTextEditingValue( - textEditingValue.copyWith( - selection: TextSelection( - baseOffset: 0, extentOffset: textEditingValue.text.length), - ), - cause, - ); - - if (cause == SelectionChangedCause.toolbar) { - bringIntoView(textEditingValue.selection.extent); - } - } - - @override - bool get wantKeepAlive => widget.configurations.focusNode.hasFocus; - - @override - AnimationController get floatingCursorResetController => - _floatingCursorResetController; - - late AnimationController _floatingCursorResetController; - - // --------------------------- Text Editing Actions -------------------------- - - QuillEditorTextBoundary _characterBoundary( - DirectionalTextEditingIntent intent) { - final atomicTextBoundary = QuillEditorCharacterBoundary(textEditingValue); - return QuillEditorCollapsedSelectionBoundary( - atomicTextBoundary, intent.forward); - } - - QuillEditorTextBoundary _nextWordBoundary( - DirectionalTextEditingIntent intent) { - final QuillEditorTextBoundary atomicTextBoundary; - final QuillEditorTextBoundary boundary; - - // final TextEditingValue textEditingValue = - // _textEditingValueforTextLayoutMetrics; - atomicTextBoundary = QuillEditorCharacterBoundary(textEditingValue); - // This isn't enough. Newline characters. - boundary = QuillEditorExpandedTextBoundary( - QuillEditorWhitespaceBoundary(textEditingValue), - QuillEditorWordBoundary(renderEditor, textEditingValue)); - - final mixedBoundary = intent.forward - ? QuillEditorMixedBoundary(atomicTextBoundary, boundary) - : QuillEditorMixedBoundary(boundary, atomicTextBoundary); - // Use a _MixedBoundary to make sure we don't leave invalid codepoints in - // the field after deletion. - return QuillEditorCollapsedSelectionBoundary(mixedBoundary, intent.forward); - } - - QuillEditorTextBoundary _linebreak(DirectionalTextEditingIntent intent) { - final QuillEditorTextBoundary atomicTextBoundary; - final QuillEditorTextBoundary boundary; - - // final TextEditingValue textEditingValue = - // _textEditingValueforTextLayoutMetrics; - atomicTextBoundary = QuillEditorCharacterBoundary(textEditingValue); - boundary = QuillEditorLineBreak(renderEditor, textEditingValue); - - // The _MixedBoundary is to make sure we don't leave invalid code units in - // the field after deletion. - // `boundary` doesn't need to be wrapped in a _CollapsedSelectionBoundary, - // since the document boundary is unique and the linebreak boundary is - // already caret-location based. - return intent.forward - ? QuillEditorMixedBoundary( - QuillEditorCollapsedSelectionBoundary(atomicTextBoundary, true), - boundary) - : QuillEditorMixedBoundary( - boundary, - QuillEditorCollapsedSelectionBoundary(atomicTextBoundary, false), - ); - } - - QuillEditorTextBoundary _documentBoundary( - DirectionalTextEditingIntent intent) => - QuillEditorDocumentBoundary(textEditingValue); - - Action _makeOverridable(Action defaultAction) { - return Action.overridable( - context: context, defaultAction: defaultAction); - } - - late final Action _replaceTextAction = - CallbackAction(onInvoke: _replaceText); - - void _updateSelection(UpdateSelectionIntent intent) { - userUpdateTextEditingValue( - intent.currentTextEditingValue.copyWith(selection: intent.newSelection), - intent.cause, - ); - } - - late final Action _updateSelectionAction = - CallbackAction(onInvoke: _updateSelection); - - late final QuillEditorUpdateTextSelectionToAdjacentLineAction< - ExtendSelectionVerticallyToAdjacentLineIntent> _adjacentLineAction = - QuillEditorUpdateTextSelectionToAdjacentLineAction< - ExtendSelectionVerticallyToAdjacentLineIntent>(this); - - late final QuillEditorToggleTextStyleAction _formatSelectionAction = - QuillEditorToggleTextStyleAction(this); - - late final QuillEditorIndentSelectionAction _indentSelectionAction = - QuillEditorIndentSelectionAction(this); - - late final QuillEditorOpenSearchAction _openSearchAction = - QuillEditorOpenSearchAction(this); - late final QuillEditorApplyHeaderAction _applyHeaderAction = - QuillEditorApplyHeaderAction(this); - late final QuillEditorApplyCheckListAction _applyCheckListAction = - QuillEditorApplyCheckListAction(this); - - late final Map> _actions = >{ - DoNothingAndStopPropagationTextIntent: DoNothingAction(consumesKey: false), - ReplaceTextIntent: _replaceTextAction, - UpdateSelectionIntent: _updateSelectionAction, - DirectionalFocusIntent: DirectionalFocusAction.forTextField(), - - // Delete - DeleteCharacterIntent: _makeOverridable( - QuillEditorDeleteTextAction( - this, _characterBoundary)), - DeleteToNextWordBoundaryIntent: _makeOverridable( - QuillEditorDeleteTextAction( - this, _nextWordBoundary)), - DeleteToLineBreakIntent: _makeOverridable( - QuillEditorDeleteTextAction(this, _linebreak)), - - // Extend/Move Selection - ExtendSelectionByCharacterIntent: _makeOverridable( - QuillEditorUpdateTextSelectionAction( - this, - false, - _characterBoundary, - )), - ExtendSelectionToNextWordBoundaryIntent: _makeOverridable( - QuillEditorUpdateTextSelectionAction< - ExtendSelectionToNextWordBoundaryIntent>( - this, true, _nextWordBoundary)), - ExtendSelectionToLineBreakIntent: _makeOverridable( - QuillEditorUpdateTextSelectionAction( - this, true, _linebreak)), - ExtendSelectionVerticallyToAdjacentLineIntent: - _makeOverridable(_adjacentLineAction), - ExtendSelectionToDocumentBoundaryIntent: _makeOverridable( - QuillEditorUpdateTextSelectionAction< - ExtendSelectionToDocumentBoundaryIntent>( - this, true, _documentBoundary)), - ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable( - QuillEditorExtendSelectionOrCaretPositionAction( - this, _nextWordBoundary)), - - // Copy Paste - SelectAllTextIntent: _makeOverridable(QuillEditorSelectAllAction(this)), - CopySelectionTextIntent: - _makeOverridable(QuillEditorCopySelectionAction(this)), - PasteTextIntent: _makeOverridable(CallbackAction( - onInvoke: (intent) => pasteText(intent.cause))), - - HideSelectionToolbarIntent: - _makeOverridable(QuillEditorHideSelectionToolbarAction(this)), - UndoTextIntent: _makeOverridable(QuillEditorUndoKeyboardAction(this)), - RedoTextIntent: _makeOverridable(QuillEditorRedoKeyboardAction(this)), - - OpenSearchIntent: _openSearchAction, - - // Selection Formatting - ToggleTextStyleIntent: _formatSelectionAction, - IndentSelectionIntent: _indentSelectionAction, - QuillEditorApplyHeaderIntent: _applyHeaderAction, - QuillEditorApplyCheckListIntent: _applyCheckListAction, - QuillEditorApplyLinkIntent: QuillEditorApplyLinkAction(this) - }; - - @override - void insertTextPlaceholder(Size size) { - // this is needed for Scribble (Stylus input) in Apple platforms - // and this package does not implement this feature - } - - @override - void removeTextPlaceholder() { - // this is needed for Scribble (Stylus input) in Apple platforms - // and this package does not implement this feature - } - - @override - void didChangeInputControl( - TextInputControl? oldControl, - TextInputControl? newControl, - ) { - // TODO: implement didChangeInputControl - } - - @override - void performSelector(String selectorName) { - final intent = intentForMacOSSelector(selectorName); - - if (intent != null) { - final primaryContext = primaryFocus?.context; - if (primaryContext != null) { - Actions.invoke(primaryContext, intent); - } - } - } - - @override - bool get liveTextInputEnabled => false; - - @override - bool get lookUpEnabled => false; - - @override - bool get searchWebEnabled => false; +import 'package:flutter/widgets.dart' + show BuildContext, State, StatefulWidget, Widget; +import 'package:meta/meta.dart' show immutable; + +import '../../models/config/raw_editor/configurations.dart'; +import 'raw_editor_state.dart'; + +class QuillRawEditor extends StatefulWidget { + QuillRawEditor({ + required this.configurations, + super.key, + }) : assert( + configurations.maxHeight == null || configurations.maxHeight! > 0, + 'maxHeight cannot be null'), + assert( + configurations.minHeight == null || configurations.minHeight! >= 0, + 'minHeight cannot be null'), + assert( + configurations.maxHeight == null || + configurations.minHeight == null || + configurations.maxHeight! >= configurations.minHeight!, + 'maxHeight cannot be null'); + + final QuillRawEditorConfigurations configurations; + + @override + State createState() => QuillRawEditorState(); +} - @override - bool get shareEnabled => false; +/// Signature for a widget builder that builds a context menu for the given +/// [QuillRawEditorState]. +/// +/// See also: +/// +/// * [EditableTextContextMenuBuilder], which performs the same role for +/// [EditableText] +typedef QuillEditorContextMenuBuilder = Widget Function( + BuildContext context, + QuillRawEditorState rawEditorState, +); + +@immutable +class QuillEditorGlyphHeights { + const QuillEditorGlyphHeights( + this.startGlyphHeight, + this.endGlyphHeight, + ); + + final double startGlyphHeight; + final double endGlyphHeight; } diff --git a/lib/src/widgets/raw_editor/raw_editor_state.dart b/lib/src/widgets/raw_editor/raw_editor_state.dart index 0c6d6ee6..dc13082f 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state.dart @@ -133,6 +133,9 @@ class QuillRawEditorState extends EditorState onSelectAll: selectAllEnabled ? () => selectAll(SelectionChangedCause.toolbar) : null, + onLookUp: null, + onSearchWeb: null, + onShare: null, ); } @@ -1612,6 +1615,14 @@ class QuillRawEditorState extends EditorState } @override - // TODO: implement liveTextInputEnabled bool get liveTextInputEnabled => false; + + @override + bool get lookUpEnabled => false; + + @override + bool get searchWebEnabled => false; + + @override + bool get shareEnabled => false; } diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index 5f095dc7..c79e8da3 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -172,7 +172,7 @@ class _TextLineState extends State { textAlign: textAlign, textDirection: widget.textDirection, strutStyle: strutStyle, - textScaleFactor: MediaQuery.textScaleFactorOf(context), + textScaler: MediaQuery.textScalerOf(context), ); return RichTextProxy( textStyle: textSpan.style!, diff --git a/pubspec.yaml b/pubspec.yaml index fce4127e..86cac33d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor built for the modern Android, iOS, web and desktop platforms. It is the WYSIWYG editor and a Quill component for Flutter. -version: 8.5.5 +version: 8.5.6 homepage: https://1o24bbs.com/c/bulletjournal/108 repository: https://github.com/singerdmx/flutter-quill