From e5446f6ded8e0edafe29cc94d2509066bc68fcc3 Mon Sep 17 00:00:00 2001 From: Alan Mantoux Date: Wed, 31 Aug 2022 22:43:52 +0200 Subject: [PATCH] Cannot make toolbar appear on empty document (immediately after gaining focus) (#28) --- .../fleather/lib/src/rendering/editor.dart | 25 ++++---- packages/fleather/lib/src/widgets/editor.dart | 6 +- .../lib/src/widgets/text_selection.dart | 2 +- .../fleather/test/widgets/editor_test.dart | 60 +++++++++++++++++-- 4 files changed, 75 insertions(+), 18 deletions(-) diff --git a/packages/fleather/lib/src/rendering/editor.dart b/packages/fleather/lib/src/rendering/editor.dart index b64bfa14..e8beb448 100644 --- a/packages/fleather/lib/src/rendering/editor.dart +++ b/packages/fleather/lib/src/rendering/editor.dart @@ -240,6 +240,7 @@ class RenderEditor extends RenderEditableContainerBox } double? _maxContentWidth; + set maxContentWidth(double? value) { if (_maxContentWidth == value) return; _maxContentWidth = value; @@ -601,20 +602,13 @@ class RenderEditor extends RenderEditableContainerBox TextSelection nextSelection, SelectionChangedCause cause, ) { + final bool selectionChanged = selection != nextSelection; // Changes made by the keyboard can sometimes be "out of band" for listening // components, so always send those events, even if we didn't think it - // changed. Also, focusing an empty field is sent as a selection change even - // if the selection offset didn't change. - final focusingEmpty = nextSelection.baseOffset == 0 && - nextSelection.extentOffset == 0 && - !hasFocus; - if (nextSelection == selection && - cause != SelectionChangedCause.keyboard && - !focusingEmpty) { - return; - } - if (onSelectionChanged != null) { - onSelectionChanged!(nextSelection, cause); + // changed. Also, the user long pressing should always send a selection change + // as well. + if (selectionChanged || cause.forcesSelectionChanged) { + onSelectionChanged?.call(nextSelection, cause); } } @@ -1025,3 +1019,10 @@ class FleatherVerticalCaretMovementRun return true; } } + +extension on SelectionChangedCause { + bool get forcesSelectionChanged => + this == SelectionChangedCause.longPress || + this == SelectionChangedCause.keyboard || + this == SelectionChangedCause.doubleTap; +} diff --git a/packages/fleather/lib/src/widgets/editor.dart b/packages/fleather/lib/src/widgets/editor.dart index e5a762fd..ca6fd660 100644 --- a/packages/fleather/lib/src/widgets/editor.dart +++ b/packages/fleather/lib/src/widgets/editor.dart @@ -901,7 +901,11 @@ class RawEditorState extends EditorState _replaceText( ReplaceTextIntent(textEditingValue, data.text!, selection, cause)); if (cause == SelectionChangedCause.toolbar) { - bringIntoView(textEditingValue.selection.extent); + SchedulerBinding.instance.addPostFrameCallback((_) { + if (mounted) { + bringIntoView(textEditingValue.selection.extent); + } + }); hideToolbar(); } } diff --git a/packages/fleather/lib/src/widgets/text_selection.dart b/packages/fleather/lib/src/widgets/text_selection.dart index 02ff1f62..9155ee41 100644 --- a/packages/fleather/lib/src/widgets/text_selection.dart +++ b/packages/fleather/lib/src/widgets/text_selection.dart @@ -850,7 +850,7 @@ class EditorTextSelectionGestureDetectorBuilder { @protected void onDoubleTapDown(TapDownDetails details) { if (delegate.selectionEnabled) { - renderEditor!.selectWord(cause: SelectionChangedCause.tap); + renderEditor!.selectWord(cause: SelectionChangedCause.doubleTap); if (shouldShowSelectionToolbar) editor!.showToolbar(); } } diff --git a/packages/fleather/test/widgets/editor_test.dart b/packages/fleather/test/widgets/editor_test.dart index cf338944..65eb0684 100644 --- a/packages/fleather/test/widgets/editor_test.dart +++ b/packages/fleather/test/widgets/editor_test.dart @@ -1,14 +1,13 @@ -// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +import 'package:fleather/fleather.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:quill_delta/quill_delta.dart'; -import 'package:fleather/fleather.dart'; import '../testing.dart'; void main() { + TestWidgetsFlutterBinding.ensureInitialized(); group('$RawEditor', () { testWidgets('allows merging attribute theme data', (tester) async { var delta = Delta() @@ -47,5 +46,58 @@ void main() { final widget = tester.widget(find.byType(FleatherField)) as FleatherField; expect(widget.readOnly, true); }); + + testWidgets('ability to paste upon long press on an empty document', + (tester) async { + // if Clipboard not initialize (status 'unknown'), an shrunken toolbar appears + prepareClipboard(); + + final editor = EditorSandBox( + tester: tester, document: ParchmentDocument(), autofocus: true); + await editor.pump(); + + expect(find.text('Paste'), findsNothing); + await tester.longPress(find.byType(FleatherEditor)); + await tester.pump(); + // Given current toolbar implementation in Flutter no other choice + // than to search for "Paste" text + final finder = find.text('Paste'); + expect(finder, findsOneWidget); + await tester.tap(finder); + await tester.pumpAndSettle(); + expect(editor.document.toPlainText(), '$clipboardText\n'); + }); + + testWidgets('ability to paste upon double-tap on an empty document', + (tester) async { + // if Clipboard not initialize (status 'unknown'), an shrunken toolbar appears + prepareClipboard(); + final editor = EditorSandBox( + tester: tester, document: ParchmentDocument(), autofocus: true); + await editor.pump(); + expect(find.text('Paste'), findsNothing); + await tester.tap(find.byType(FleatherEditor)); + await tester.tap(find.byType(FleatherEditor)); + await tester.pump(); + final finder = find.text('Paste'); + expect(finder, findsOneWidget); + await tester.tap(finder); + await tester.pumpAndSettle(); + expect(editor.document.toPlainText(), '$clipboardText\n'); + }); + }); +} + +const clipboardText = 'copied text'; + +void prepareClipboard() { + TestDefaultBinaryMessengerBinding.instance?.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, (message) { + if (message.method == 'Clipboard.getData') { + return Future.value({'text': clipboardText}); + } + if (message.method == 'Clipboard.hasStrings') { + return Future.value({'value': true}); + } }); }