fix: selection is wrong after pressing arrow left / right

This commit is contained in:
Lucas.Xu 2022-08-18 21:09:30 +08:00
parent 28d9b8ee3a
commit 45a120fe88
10 changed files with 146 additions and 43 deletions

View File

@ -5,18 +5,23 @@ class FlowySvg extends StatelessWidget {
const FlowySvg({ const FlowySvg({
Key? key, Key? key,
this.name, this.name,
this.size = const Size(20, 20), this.width,
this.height,
this.color, this.color,
this.number, this.number,
this.padding, this.padding,
}) : super(key: key); }) : super(key: key);
final String? name; final String? name;
final Size size; final double? width;
final double? height;
final Color? color; final Color? color;
final int? number; final int? number;
final EdgeInsets? padding; final EdgeInsets? padding;
final _defaultWidth = 20.0;
final _defaultHeight = 20.0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
@ -27,22 +32,21 @@ class FlowySvg extends StatelessWidget {
Widget _buildSvg() { Widget _buildSvg() {
if (name != null) { if (name != null) {
return SizedBox.fromSize( return SvgPicture.asset(
size: size, 'assets/images/$name.svg',
child: SvgPicture.asset( color: color,
'assets/images/$name.svg', fit: BoxFit.fill,
color: color, height: height,
package: 'appflowy_editor', width: width,
fit: BoxFit.fill, package: 'appflowy_editor',
),
); );
} else if (number != null) { } else if (number != null) {
final numberText = final numberText =
'<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg"><text x="30" y="150" fill="black" font-size="160">$number.</text></svg>'; '<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg"><text x="30" y="150" fill="black" font-size="160">$number.</text></svg>';
return SvgPicture.string( return SvgPicture.string(
numberText, numberText,
width: size.width, width: width ?? _defaultWidth,
height: size.width, height: height ?? _defaultHeight,
); );
} }
return Container(); return Container();

View File

@ -47,7 +47,7 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
final iconKey = GlobalKey(); final iconKey = GlobalKey();
final _richTextKey = GlobalKey(debugLabel: 'bulleted_list_text'); final _richTextKey = GlobalKey(debugLabel: 'bulleted_list_text');
final _iconSize = 20.0; final _iconWidth = 20.0;
final _iconRightPadding = 5.0; final _iconRightPadding = 5.0;
@override @override
@ -67,7 +67,8 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
children: [ children: [
FlowySvg( FlowySvg(
key: iconKey, key: iconKey,
size: Size.square(_iconSize), width: _iconWidth,
height: _iconWidth,
padding: padding:
EdgeInsets.only(top: topPadding, right: _iconRightPadding), EdgeInsets.only(top: topPadding, right: _iconRightPadding),
name: 'point', name: 'point',

View File

@ -45,7 +45,7 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
final iconKey = GlobalKey(); final iconKey = GlobalKey();
final _richTextKey = GlobalKey(debugLabel: 'checkbox_text'); final _richTextKey = GlobalKey(debugLabel: 'checkbox_text');
final _iconSize = 20.0; final _iconWidth = 20.0;
final _iconRightPadding = 5.0; final _iconRightPadding = 5.0;
@override @override
@ -74,7 +74,8 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
GestureDetector( GestureDetector(
key: iconKey, key: iconKey,
child: FlowySvg( child: FlowySvg(
size: Size.square(_iconSize), width: _iconWidth,
height: _iconWidth,
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: topPadding, top: topPadding,
right: _iconRightPadding, right: _iconRightPadding,

View File

@ -47,7 +47,7 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
final iconKey = GlobalKey(); final iconKey = GlobalKey();
final _richTextKey = GlobalKey(debugLabel: 'number_list_text'); final _richTextKey = GlobalKey(debugLabel: 'number_list_text');
final _iconSize = 20.0; final _iconWidth = 20.0;
final _iconRightPadding = 5.0; final _iconRightPadding = 5.0;
@override @override
@ -66,7 +66,8 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
children: [ children: [
FlowySvg( FlowySvg(
key: iconKey, key: iconKey,
size: Size.square(_iconSize), width: _iconWidth,
height: _iconWidth,
padding: padding:
EdgeInsets.only(top: topPadding, right: _iconRightPadding), EdgeInsets.only(top: topPadding, right: _iconRightPadding),
number: widget.textNode.attributes.number, number: widget.textNode.attributes.number,

View File

@ -46,7 +46,7 @@ class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
final iconKey = GlobalKey(); final iconKey = GlobalKey();
final _richTextKey = GlobalKey(debugLabel: 'quoted_text'); final _richTextKey = GlobalKey(debugLabel: 'quoted_text');
final _iconSize = 20.0; final _iconWidth = 20.0;
final _iconRightPadding = 5.0; final _iconRightPadding = 5.0;
@override @override
@ -60,25 +60,27 @@ class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
width: defaultMaxTextNodeWidth, width: defaultMaxTextNodeWidth,
child: Padding( child: Padding(
padding: EdgeInsets.only(bottom: defaultLinePadding), padding: EdgeInsets.only(bottom: defaultLinePadding),
child: Row( child: IntrinsicHeight(
crossAxisAlignment: CrossAxisAlignment.start, child: Row(
children: [ crossAxisAlignment: CrossAxisAlignment.stretch,
FlowySvg( children: [
key: iconKey, FlowySvg(
size: Size(_iconSize, _quoteHeight), key: iconKey,
padding: width: _iconWidth,
EdgeInsets.only(top: topPadding, right: _iconRightPadding), padding: EdgeInsets.only(
name: 'quote', top: topPadding, right: _iconRightPadding),
), name: 'quote',
Expanded(
child: FlowyRichText(
key: _richTextKey,
placeholderText: 'Quote',
textNode: widget.textNode,
editorState: widget.editorState,
), ),
), Expanded(
], child: FlowyRichText(
key: _richTextKey,
placeholderText: 'Quote',
textNode: widget.textNode,
editorState: widget.editorState,
),
),
],
),
), ),
)); ));
} }
@ -86,6 +88,6 @@ class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
double get _quoteHeight { double get _quoteHeight {
final lines = final lines =
widget.textNode.toRawString().characters.where((c) => c == '\n').length; widget.textNode.toRawString().characters.where((c) => c == '\n').length;
return (lines + 1) * _iconSize; return (lines + 1) * _iconWidth;
} }
} }

View File

@ -141,7 +141,7 @@ class _ToolbarWidgetState extends State<ToolbarWidget> with ToolbarMixin {
Size(toolbarHeight - (width != null ? 20 : 0), toolbarHeight), Size(toolbarHeight - (width != null ? 20 : 0), toolbarHeight),
child: Center( child: Center(
child: FlowySvg( child: FlowySvg(
size: Size(width ?? 20, 20), width: width ?? 20,
name: 'toolbar/$name', name: 'toolbar/$name',
), ),
), ),

View File

@ -120,8 +120,9 @@ AppFlowyKeyEventHandler arrowKeysHandler = (editorState, event) {
editorState.updateCursorSelection(Selection.collapsed(leftPosition)); editorState.updateCursorSelection(Selection.collapsed(leftPosition));
} }
} else { } else {
editorState editorState.updateCursorSelection(
.updateCursorSelection(currentSelection.collapse(atStart: true)); currentSelection.collapse(atStart: currentSelection.isBackward),
);
} }
return KeyEventResult.handled; return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
@ -131,7 +132,9 @@ AppFlowyKeyEventHandler arrowKeysHandler = (editorState, event) {
editorState.updateCursorSelection(Selection.collapsed(rightPosition)); editorState.updateCursorSelection(Selection.collapsed(rightPosition));
} }
} else { } else {
editorState.updateCursorSelection(currentSelection.collapse()); editorState.updateCursorSelection(
currentSelection.collapse(atStart: !currentSelection.isBackward),
);
} }
return KeyEventResult.handled; return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {

View File

@ -448,5 +448,6 @@ class PopupListItem {
Widget _popupListIcon(String name) => FlowySvg( Widget _popupListIcon(String name) => FlowySvg(
name: 'popup_list/$name', name: 'popup_list/$name',
color: Colors.black, color: Colors.black,
size: const Size.square(18.0), width: 18.0,
height: 18.0,
); );

View File

@ -88,6 +88,12 @@ extension on LogicalKeyboardKey {
if (this == LogicalKeyboardKey.delete) { if (this == LogicalKeyboardKey.delete) {
return PhysicalKeyboardKey.delete; return PhysicalKeyboardKey.delete;
} }
if (this == LogicalKeyboardKey.arrowRight) {
return PhysicalKeyboardKey.arrowRight;
}
if (this == LogicalKeyboardKey.arrowLeft) {
return PhysicalKeyboardKey.arrowLeft;
}
if (this == LogicalKeyboardKey.pageDown) { if (this == LogicalKeyboardKey.pageDown) {
return PhysicalKeyboardKey.pageDown; return PhysicalKeyboardKey.pageDown;
} }

View File

@ -0,0 +1,84 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../infra/test_editor.dart';
void main() async {
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
group('arrow_keys_handler.dart', () {
testWidgets('Presses arrow right key, move the cursor from left to right',
(tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor
..insertTextNode(text)
..insertTextNode(text);
await editor.startTesting();
await editor.updateSelection(
Selection.single(path: [0], startOffset: 0),
);
final textNode = editor.nodeAtPath([0]) as TextNode;
for (var i = 0; i < text.length; i++) {
await editor.pressLogicKey(LogicalKeyboardKey.arrowRight);
if (i == text.length - 1) {
// Wrap to next node if the cursor is at the end of the current node.
expect(
editor.documentSelection,
Selection.single(
path: [1],
startOffset: 0,
),
);
} else {
expect(
editor.documentSelection,
Selection.single(
path: [0],
startOffset: textNode.delta.nextRunePosition(i),
),
);
}
}
});
});
testWidgets(
'Presses arrow left/right key since selection is not collapsed and backward',
(tester) async {
await _testPressArrowKeyInNotCollapsedSelection(tester, true);
});
testWidgets(
'Presses arrow left/right key since selection is not collapsed and forward',
(tester) async {
await _testPressArrowKeyInNotCollapsedSelection(tester, false);
});
}
Future<void> _testPressArrowKeyInNotCollapsedSelection(
WidgetTester tester, bool isBackward) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor
..insertTextNode(text)
..insertTextNode(text);
await editor.startTesting();
final start = Position(path: [0], offset: 5);
final end = Position(path: [1], offset: 10);
final selection = Selection(
start: isBackward ? start : end,
end: isBackward ? end : start,
);
await editor.updateSelection(selection);
await editor.pressLogicKey(LogicalKeyboardKey.arrowLeft);
expect(editor.documentSelection?.start, start);
await editor.updateSelection(selection);
await editor.pressLogicKey(LogicalKeyboardKey.arrowRight);
expect(editor.documentSelection?.end, end);
}