parent
37fc173dde
commit
f5a1a61938
8 changed files with 664 additions and 0 deletions
@ -0,0 +1,124 @@ |
||||
import 'dart:math' as math; |
||||
|
||||
import 'package:flutter/cupertino.dart'; |
||||
import 'package:flutter_quill/models/documents/attribute.dart'; |
||||
import 'package:flutter_quill/models/documents/document.dart'; |
||||
import 'package:flutter_quill/models/documents/nodes/embed.dart'; |
||||
import 'package:flutter_quill/models/documents/style.dart'; |
||||
import 'package:flutter_quill/utils/diff_delta.dart'; |
||||
import 'package:quill_delta/quill_delta.dart'; |
||||
|
||||
class QuillController extends ChangeNotifier { |
||||
final Document document; |
||||
TextSelection selection; |
||||
Style toggledStyle = Style(); |
||||
|
||||
QuillController(this.document, this.selection, this.toggledStyle) |
||||
: assert(document != null), |
||||
assert(selection != null); |
||||
|
||||
Style getSelectionStyle() { |
||||
return document |
||||
.collectStyle(selection.start, selection.end - selection.start) |
||||
.mergeAll(toggledStyle); |
||||
} |
||||
|
||||
replaceText(int index, int len, Object data, TextSelection textSelection) { |
||||
assert(data is String || data is Embeddable); |
||||
|
||||
Delta delta; |
||||
if (len > 0 || data is! String || (data as String).isNotEmpty) { |
||||
delta = document.replace(index, len, data); |
||||
if (delta != null && |
||||
toggledStyle.isNotEmpty && |
||||
delta.isNotEmpty && |
||||
delta.length <= 2 && |
||||
delta.last.isInsert) { |
||||
Delta retainDelta = Delta() |
||||
..retain(index) |
||||
..retain(data is String ? data.length : 1, toggledStyle.toJson()); |
||||
document.compose(retainDelta, ChangeSource.LOCAL); |
||||
} |
||||
} |
||||
|
||||
toggledStyle = Style(); |
||||
if (textSelection != null) { |
||||
if (delta == null) { |
||||
_updateSelection(textSelection, ChangeSource.LOCAL); |
||||
} else { |
||||
Delta user = Delta() |
||||
..retain(index) |
||||
..insert(data) |
||||
..delete(len); |
||||
int positionDelta = getPositionDelta(user, delta); |
||||
_updateSelection( |
||||
textSelection.copyWith( |
||||
baseOffset: textSelection.baseOffset + positionDelta, |
||||
extentOffset: textSelection.extentOffset + positionDelta, |
||||
), |
||||
ChangeSource.LOCAL, |
||||
); |
||||
} |
||||
} |
||||
notifyListeners(); |
||||
} |
||||
|
||||
formatText(int index, int len, Attribute attribute) { |
||||
if (len == 0 && attribute.isInline && attribute.key != Attribute.link.key) { |
||||
toggledStyle = toggledStyle.put(attribute); |
||||
} |
||||
|
||||
Delta change = document.format(index, len, attribute); |
||||
TextSelection adjustedSelection = selection.copyWith( |
||||
baseOffset: change.transformPosition(selection.baseOffset), |
||||
extentOffset: change.transformPosition(selection.extentOffset)); |
||||
if (selection != adjustedSelection) { |
||||
_updateSelection(adjustedSelection, ChangeSource.LOCAL); |
||||
} |
||||
notifyListeners(); |
||||
} |
||||
|
||||
formatSelection(Attribute attribute) { |
||||
formatText(selection.start, selection.end - selection.start, attribute); |
||||
} |
||||
|
||||
updateSelection(TextSelection textSelection, ChangeSource source) { |
||||
_updateSelection(textSelection, source); |
||||
notifyListeners(); |
||||
} |
||||
|
||||
compose(Delta delta, TextSelection textSelection, ChangeSource source) { |
||||
if (delta.isNotEmpty) { |
||||
document.compose(delta, source); |
||||
} |
||||
if (textSelection != null) { |
||||
_updateSelection(textSelection, source); |
||||
} else { |
||||
textSelection = selection.copyWith( |
||||
baseOffset: |
||||
delta.transformPosition(selection.baseOffset, force: false), |
||||
extentOffset: |
||||
delta.transformPosition(selection.extentOffset, force: false)); |
||||
if (selection != textSelection) { |
||||
_updateSelection(textSelection, source); |
||||
} |
||||
} |
||||
notifyListeners(); |
||||
} |
||||
|
||||
@override |
||||
void dispose() { |
||||
document.close(); |
||||
super.dispose(); |
||||
} |
||||
|
||||
_updateSelection(TextSelection textSelection, ChangeSource source) { |
||||
assert(textSelection != null); |
||||
assert(source != null); |
||||
selection = textSelection; |
||||
int end = document.length - 1; |
||||
selection = selection.copyWith( |
||||
baseOffset: math.min(selection.baseOffset, end), |
||||
extentOffset: math.min(selection.extentOffset, end)); |
||||
} |
||||
} |
@ -0,0 +1,31 @@ |
||||
import 'package:flutter/cupertino.dart'; |
||||
|
||||
import 'editor.dart'; |
||||
|
||||
abstract class EditorTextSelectionGestureDetectorBuilderDelegate { |
||||
GlobalKey<EditorState> getEditableTextKey(); |
||||
|
||||
bool getForcePressEnabled(); |
||||
|
||||
bool getSelectionEnabled(); |
||||
} |
||||
|
||||
class EditorTextSelectionGestureDetectorBuilder { |
||||
final EditorTextSelectionGestureDetectorBuilderDelegate delegate; |
||||
bool shouldShowSelectionToolbar = true; |
||||
|
||||
EditorTextSelectionGestureDetectorBuilder(this.delegate) |
||||
: assert(delegate != null); |
||||
|
||||
EditorState getEditor() { |
||||
return delegate.getEditableTextKey().currentState; |
||||
} |
||||
|
||||
RenderEditor getRenderEditor() { |
||||
return this.getEditor().getRenderEditor(); |
||||
} |
||||
|
||||
onTapDown(TapDownDetails details) { |
||||
// getRenderEditor().handleTapDown(details); |
||||
} |
||||
} |
@ -0,0 +1,247 @@ |
||||
import 'package:flutter/cupertino.dart'; |
||||
import 'package:flutter/rendering.dart'; |
||||
import 'package:flutter_quill/models/documents/document.dart'; |
||||
import 'package:flutter_quill/widgets/text_selection.dart'; |
||||
|
||||
import 'controller.dart'; |
||||
import 'delegate.dart'; |
||||
|
||||
abstract class RenderAbstractEditor { |
||||
TextSelection selectWordAtPosition(TextPosition position); |
||||
|
||||
TextSelection selectLineAtPosition(TextPosition position); |
||||
|
||||
double preferredLineHeight(TextPosition position); |
||||
|
||||
TextPosition getPositionForOffset(Offset offset); |
||||
|
||||
List<TextSelectionPoint> getEndpointsForSelection( |
||||
TextSelection textSelection); |
||||
|
||||
void handleTapDown(TapDownDetails details); |
||||
|
||||
void selectWordsInRange( |
||||
Offset from, |
||||
Offset to, |
||||
SelectionChangedCause cause, |
||||
); |
||||
|
||||
void selectWordEdge(SelectionChangedCause cause); |
||||
|
||||
void selectPositionAt(Offset from, Offset to, SelectionChangedCause cause); |
||||
|
||||
void selectWord(SelectionChangedCause cause); |
||||
|
||||
void selectPosition(SelectionChangedCause cause); |
||||
} |
||||
|
||||
class QuillEditor extends StatefulWidget { |
||||
final QuillController controller; |
||||
final FocusNode focusNode; |
||||
final ScrollController scrollController; |
||||
final bool scrollable; |
||||
final EdgeInsetsGeometry padding; |
||||
final bool autoFocus; |
||||
final bool showCursor; |
||||
final bool readOnly; |
||||
final bool enableInteractiveSelection; |
||||
final double minHeight; |
||||
final double maxHeight; |
||||
final bool expands; |
||||
final TextCapitalization textCapitalization; |
||||
final Brightness keyboardAppearance; |
||||
final ScrollPhysics scrollPhysics; |
||||
final ValueChanged<String> onLaunchUrl; |
||||
|
||||
QuillEditor( |
||||
this.controller, |
||||
this.focusNode, |
||||
this.scrollController, |
||||
this.scrollable, |
||||
this.padding, |
||||
this.autoFocus, |
||||
this.showCursor, |
||||
this.readOnly, |
||||
this.enableInteractiveSelection, |
||||
this.minHeight, |
||||
this.maxHeight, |
||||
this.expands, |
||||
this.textCapitalization, |
||||
this.keyboardAppearance, |
||||
this.scrollPhysics, |
||||
this.onLaunchUrl) |
||||
: assert(controller != null), |
||||
assert(scrollController != null), |
||||
assert(scrollable != null), |
||||
assert(autoFocus != null), |
||||
assert(readOnly != null); |
||||
|
||||
@override |
||||
_QuillEditorState createState() => _QuillEditorState(); |
||||
} |
||||
|
||||
class _QuillEditorState extends State<QuillEditor> |
||||
implements EditorTextSelectionGestureDetectorBuilderDelegate { |
||||
final GlobalKey<EditorState> _editorKey = GlobalKey<EditorState>(); |
||||
EditorTextSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder; |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
// TODO: implement build |
||||
throw UnimplementedError(); |
||||
} |
||||
|
||||
@override |
||||
GlobalKey<EditorState> getEditableTextKey() { |
||||
return _editorKey; |
||||
} |
||||
|
||||
@override |
||||
bool getForcePressEnabled() { |
||||
return false; |
||||
} |
||||
|
||||
@override |
||||
bool getSelectionEnabled() { |
||||
return widget.enableInteractiveSelection; |
||||
} |
||||
} |
||||
|
||||
class RawEditor extends StatefulWidget { |
||||
@override |
||||
State<StatefulWidget> createState() { |
||||
// TODO: implement createState |
||||
throw UnimplementedError(); |
||||
} |
||||
} |
||||
|
||||
class RenderEditor extends RenderEditableContainerBox |
||||
implements RenderAbstractEditor { |
||||
Document document; |
||||
TextSelection selection; |
||||
bool _hasFocus = false; |
||||
|
||||
setDocument(Document doc) { |
||||
assert(doc != null); |
||||
if (document == doc) { |
||||
return; |
||||
} |
||||
document = doc; |
||||
markNeedsLayout(); |
||||
} |
||||
|
||||
setHasFocus(bool h) { |
||||
assert(h != null); |
||||
if (_hasFocus == h) { |
||||
return; |
||||
} |
||||
_hasFocus = h; |
||||
markNeedsSemanticsUpdate(); |
||||
} |
||||
|
||||
setSelection(TextSelection t) { |
||||
if (selection == t) { |
||||
return; |
||||
} |
||||
selection = t; |
||||
markNeedsPaint(); |
||||
} |
||||
|
||||
@override |
||||
List<TextSelectionPoint> getEndpointsForSelection( |
||||
TextSelection textSelection) { |
||||
// TODO: implement getEndpointsForSelection |
||||
throw UnimplementedError(); |
||||
} |
||||
|
||||
@override |
||||
TextPosition getPositionForOffset(Offset offset) { |
||||
// TODO: implement getPositionForOffset |
||||
throw UnimplementedError(); |
||||
} |
||||
|
||||
@override |
||||
void handleTapDown(TapDownDetails details) { |
||||
// TODO: implement handleTapDown |
||||
} |
||||
|
||||
@override |
||||
double preferredLineHeight(TextPosition position) { |
||||
// TODO: implement preferredLineHeight |
||||
throw UnimplementedError(); |
||||
} |
||||
|
||||
@override |
||||
TextSelection selectLineAtPosition(TextPosition position) { |
||||
// TODO: implement selectLineAtPosition |
||||
throw UnimplementedError(); |
||||
} |
||||
|
||||
@override |
||||
void selectPosition(SelectionChangedCause cause) { |
||||
// TODO: implement selectPosition |
||||
} |
||||
|
||||
@override |
||||
void selectPositionAt(Offset from, Offset to, SelectionChangedCause cause) { |
||||
// TODO: implement selectPositionAt |
||||
} |
||||
|
||||
@override |
||||
void selectWord(SelectionChangedCause cause) { |
||||
// TODO: implement selectWord |
||||
} |
||||
|
||||
@override |
||||
TextSelection selectWordAtPosition(TextPosition position) { |
||||
// TODO: implement selectWordAtPosition |
||||
throw UnimplementedError(); |
||||
} |
||||
|
||||
@override |
||||
void selectWordEdge(SelectionChangedCause cause) { |
||||
// TODO: implement selectWordEdge |
||||
} |
||||
|
||||
@override |
||||
void selectWordsInRange(Offset from, Offset to, SelectionChangedCause cause) { |
||||
// TODO: implement selectWordsInRange |
||||
} |
||||
} |
||||
|
||||
class RenderEditableContainerBox extends RenderBox { |
||||
Container _container; |
||||
TextDirection _textDirection; |
||||
|
||||
setContainer(Container c) { |
||||
assert(c != null); |
||||
if (_container == c) { |
||||
return; |
||||
} |
||||
_container = c; |
||||
markNeedsLayout(); |
||||
} |
||||
|
||||
setTextDirection(TextDirection t) { |
||||
if (_textDirection == t) { |
||||
return; |
||||
} |
||||
_textDirection = t; |
||||
} |
||||
} |
||||
|
||||
abstract class EditorState extends State<RawEditor> { |
||||
TextEditingValue getTextEditingValue(); |
||||
|
||||
void setTextEditingValue(TextEditingValue value); |
||||
|
||||
RenderEditor getRenderEditor(); |
||||
|
||||
EditorTextSelectionOverlay getSelectionOverlay(); |
||||
|
||||
bool showToolbar(); |
||||
|
||||
void hideToolbar(); |
||||
|
||||
void requestKeyboard(); |
||||
} |
@ -0,0 +1,100 @@ |
||||
import 'package:flutter/services.dart'; |
||||
|
||||
enum InputShortcut { CUT, COPY, PAST, SELECT_ALL } |
||||
|
||||
typedef CursorMoveCallback = void Function(LogicalKeyboardKey key, |
||||
{bool wordModifier, bool lineModifier, bool shift}); |
||||
typedef InputShortcutCallback = void Function(InputShortcut shortcut); |
||||
typedef OnDeleteCallback = void Function(bool forward); |
||||
|
||||
class KeyboardListener { |
||||
final CursorMoveCallback onCursorMove; |
||||
final InputShortcutCallback onShortcut; |
||||
final OnDeleteCallback onDelete; |
||||
static final Set<LogicalKeyboardKey> _moveKeys = <LogicalKeyboardKey>{ |
||||
LogicalKeyboardKey.arrowRight, |
||||
LogicalKeyboardKey.arrowLeft, |
||||
LogicalKeyboardKey.arrowUp, |
||||
LogicalKeyboardKey.arrowDown, |
||||
}; |
||||
|
||||
static final Set<LogicalKeyboardKey> _shortcutKeys = <LogicalKeyboardKey>{ |
||||
LogicalKeyboardKey.keyA, |
||||
LogicalKeyboardKey.keyC, |
||||
LogicalKeyboardKey.keyV, |
||||
LogicalKeyboardKey.keyX, |
||||
LogicalKeyboardKey.delete, |
||||
LogicalKeyboardKey.backspace, |
||||
}; |
||||
|
||||
static final Set<LogicalKeyboardKey> _nonModifierKeys = <LogicalKeyboardKey>{ |
||||
..._shortcutKeys, |
||||
..._moveKeys, |
||||
}; |
||||
|
||||
static final Set<LogicalKeyboardKey> _modifierKeys = <LogicalKeyboardKey>{ |
||||
LogicalKeyboardKey.shift, |
||||
LogicalKeyboardKey.control, |
||||
LogicalKeyboardKey.alt, |
||||
}; |
||||
|
||||
static final Set<LogicalKeyboardKey> _macOsModifierKeys = |
||||
<LogicalKeyboardKey>{ |
||||
LogicalKeyboardKey.shift, |
||||
LogicalKeyboardKey.meta, |
||||
LogicalKeyboardKey.alt, |
||||
}; |
||||
|
||||
static final Set<LogicalKeyboardKey> _interestingKeys = <LogicalKeyboardKey>{ |
||||
..._modifierKeys, |
||||
..._macOsModifierKeys, |
||||
..._nonModifierKeys, |
||||
}; |
||||
|
||||
static final Map<LogicalKeyboardKey, InputShortcut> _keyToShortcut = { |
||||
LogicalKeyboardKey.keyX: InputShortcut.CUT, |
||||
LogicalKeyboardKey.keyC: InputShortcut.COPY, |
||||
LogicalKeyboardKey.keyV: InputShortcut.PAST, |
||||
LogicalKeyboardKey.keyA: InputShortcut.SELECT_ALL, |
||||
}; |
||||
|
||||
KeyboardListener(this.onCursorMove, this.onShortcut, this.onDelete) |
||||
: assert(onCursorMove != null), |
||||
assert(onShortcut != null), |
||||
assert(onDelete != null); |
||||
|
||||
bool handleRawKeyEvent(RawKeyEvent event) { |
||||
if (event is! RawKeyDownEvent) { |
||||
return false; |
||||
} |
||||
|
||||
Set<LogicalKeyboardKey> keysPressed = |
||||
LogicalKeyboardKey.collapseSynonyms(RawKeyboard.instance.keysPressed); |
||||
LogicalKeyboardKey key = event.logicalKey; |
||||
bool isMacOS = event.data is RawKeyEventDataMacOs; |
||||
if (!_nonModifierKeys.contains(key) || |
||||
keysPressed |
||||
.difference(isMacOS ? _macOsModifierKeys : _modifierKeys) |
||||
.length > |
||||
1 || |
||||
keysPressed.difference(_interestingKeys).isNotEmpty) { |
||||
return false; |
||||
} |
||||
|
||||
if (_moveKeys.contains(key)) { |
||||
onCursorMove(key, |
||||
wordModifier: isMacOS ? event.isAltPressed : event.isControlPressed, |
||||
lineModifier: isMacOS ? event.isMetaPressed : event.isAltPressed, |
||||
shift: event.isShiftPressed); |
||||
} else if (isMacOS |
||||
? event.isMetaPressed |
||||
: event.isControlPressed && _shortcutKeys.contains(key)) { |
||||
onShortcut(_keyToShortcut[key]); |
||||
} else if (key == LogicalKeyboardKey.delete) { |
||||
onDelete(true); |
||||
} else if (key == LogicalKeyboardKey.backspace) { |
||||
onDelete(false); |
||||
} |
||||
return false; |
||||
} |
||||
} |
@ -0,0 +1,83 @@ |
||||
import 'package:flutter/cupertino.dart'; |
||||
import 'package:flutter/gestures.dart'; |
||||
import 'package:flutter/rendering.dart'; |
||||
|
||||
import 'editor.dart'; |
||||
|
||||
class EditorTextSelectionOverlay { |
||||
TextEditingValue value; |
||||
bool handlesVisible = false; |
||||
final BuildContext context; |
||||
final Widget debugRequiredFor; |
||||
final LayerLink toolbarLayerLink; |
||||
final LayerLink startHandleLayerLink; |
||||
final LayerLink endHandleLayerLink; |
||||
final RenderEditor renderObject; |
||||
final TextSelectionControls selectionCtrls; |
||||
final TextSelectionDelegate selectionDelegate; |
||||
final DragStartBehavior dragStartBehavior; |
||||
final VoidCallback onSelectionHandleTapped; |
||||
final ClipboardStatusNotifier clipboardStatus; |
||||
AnimationController _toolbarController; |
||||
List<OverlayEntry> _handles; |
||||
OverlayEntry _toolbar; |
||||
|
||||
EditorTextSelectionOverlay( |
||||
this.value, |
||||
this.handlesVisible, |
||||
this.context, |
||||
this.debugRequiredFor, |
||||
this.toolbarLayerLink, |
||||
this.startHandleLayerLink, |
||||
this.endHandleLayerLink, |
||||
this.renderObject, |
||||
this.selectionCtrls, |
||||
this.selectionDelegate, |
||||
this.dragStartBehavior, |
||||
this.onSelectionHandleTapped, |
||||
this.clipboardStatus) |
||||
: assert(value != null), |
||||
assert(context != null), |
||||
assert(handlesVisible != null) { |
||||
OverlayState overlay = Overlay.of(context, rootOverlay: true); |
||||
assert( |
||||
overlay != null, |
||||
); |
||||
_toolbarController = AnimationController( |
||||
duration: Duration(milliseconds: 150), vsync: overlay); |
||||
} |
||||
|
||||
setHandlesVisible(bool visible) {} |
||||
|
||||
hideHandles() { |
||||
if (_handles == null) { |
||||
return; |
||||
} |
||||
_handles[0].remove(); |
||||
_handles[1].remove(); |
||||
_handles = null; |
||||
} |
||||
|
||||
/// Shows the toolbar by inserting it into the [context]'s overlay. |
||||
showToolbar() { |
||||
assert(_toolbar == null); |
||||
_toolbar = OverlayEntry(builder: _buildToolbar); |
||||
Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor) |
||||
.insert(_toolbar); |
||||
_toolbarController.forward(from: 0.0); |
||||
} |
||||
|
||||
Widget _buildToolbar(BuildContext context) { |
||||
if (selectionCtrls == null) { |
||||
return Container(); |
||||
} |
||||
} |
||||
|
||||
_markNeedsBuild() { |
||||
if (_handles != null) { |
||||
_handles[0].markNeedsBuild(); |
||||
_handles[1].markNeedsBuild(); |
||||
} |
||||
_toolbar?.markNeedsBuild(); |
||||
} |
||||
} |
Loading…
Reference in new issue