Add delegate and controller

pull/13/head
singerdmx 4 years ago
parent 37fc173dde
commit f5a1a61938
  1. 21
      lib/models/documents/document.dart
  2. 46
      lib/models/documents/nodes/line.dart
  3. 12
      lib/models/documents/style.dart
  4. 124
      lib/widgets/controller.dart
  5. 31
      lib/widgets/delegate.dart
  6. 247
      lib/widgets/editor.dart
  7. 100
      lib/widgets/keyborad_listener.dart
  8. 83
      lib/widgets/text_selection.dart

@ -1,5 +1,8 @@
import 'dart:async';
import 'package:flutter_quill/models/documents/nodes/block.dart';
import 'package:flutter_quill/models/documents/nodes/container.dart';
import 'package:flutter_quill/models/documents/nodes/line.dart';
import 'package:flutter_quill/models/documents/style.dart';
import 'package:quill_delta/quill_delta.dart';
import 'package:tuple/tuple.dart';
@ -87,6 +90,20 @@ class Document {
return delta;
}
Style collectStyle(int index, int len) {
ChildQuery res = queryChild(index);
return (res.node as Line).collectStyle(res.offset, len);
}
ChildQuery queryChild(int offset) {
ChildQuery res = _root.queryChild(offset, true);
if (res.node is Line) {
return res;
}
Block block = res.node;
return block.queryChild(res.offset, true);
}
compose(Delta delta, ChangeSource changeSource) {
assert(!_observer.isClosed);
delta.trim();
@ -135,6 +152,10 @@ class Document {
? data
: Embeddable.fromJson(data);
}
close() {
_observer.close();
}
}
enum ChangeSource {

@ -261,4 +261,50 @@ class Line extends Container<Leaf> {
Node newInstance() {
return Line();
}
Style collectStyle(int offset, int len) {
int local = math.min(this.length - offset, len);
Style res = Style();
var excluded = <Attribute>{};
void _handle(Style style) {
if (res.isEmpty) {
excluded.addAll(style.values);
} else {
for (Attribute attr in res.values) {
if (!style.containsKey(attr.key)) {
excluded.add(attr);
}
}
}
Style remain = style.removeAll(excluded);
res = res.removeAll(excluded);
res = res.mergeAll(remain);
}
ChildQuery data = queryChild(offset, true);
Leaf node = data.node;
if (node != null) {
res = res.mergeAll(node.style);
int pos = node.length - data.offset;
while (!node.isLast && pos < local) {
node = node.next as Leaf;
_handle(node.style);
pos += node.length;
}
}
res = res.mergeAll(style);
if (parent is Block) {
Block block = parent;
res = res.mergeAll(block.style);
}
int remain = len - local;
if (remain > 0) {
_handle(nextLine.collectStyle(0, remain));
}
return res;
}
}

@ -67,4 +67,16 @@ class Style {
}
return result;
}
Style removeAll(Set<Attribute> attributes) {
Map<String, Attribute> merged = Map<String, Attribute>.from(_attributes);
attributes.map((item) => item.key).forEach(merged.remove);
return Style.attr(merged);
}
Style put(Attribute attribute) {
Map<String, Attribute> m = Map<String, Attribute>.from(attributes);
m[attribute.key] = attribute;
return Style.attr(m);
}
}

@ -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…
Cancel
Save