Merge branch 'singerdmx:master' into master

pull/1110/head
Cierra_Runis 2 years ago committed by GitHub
commit 6ea9cd0526
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 30
      CHANGELOG.md
  2. 5
      README.md
  3. 1
      flutter_quill_extensions/lib/embeds/toolbar/image_video_utils.dart
  4. 2
      lib/src/models/rules/insert.dart
  5. 36
      lib/src/translations/toolbar.i18n.dart
  6. 4
      lib/src/utils/platform.dart
  7. 45
      lib/src/widgets/delegate.dart
  8. 56
      lib/src/widgets/editor.dart
  9. 137
      lib/src/widgets/raw_editor.dart
  10. 9
      lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart
  11. 93
      lib/src/widgets/text_selection.dart
  12. 85
      lib/src/widgets/toolbar.dart
  13. 80
      lib/src/widgets/toolbar/arrow_indicated_button_list.dart
  14. 97
      lib/src/widgets/toolbar/select_header_style_button.dart
  15. 18
      pubspec.yaml

@ -1,3 +1,33 @@
# [6.4.4]
* Increased compatibility with Flutter widget tests.
# [6.4.3]
* Update dependencies (collection: 1.17.0, flutter_keyboard_visibility: 5.4.0, quiver: 3.2.1, tuple: 2.0.1, url_launcher: 6.1.9, characters: 1.2.1, i18n_extension: 7.0.0, device_info_plus: 8.1.0)
# [6.4.2]
* Replace `buildToolbar` with `contextMenuBuilder`.
# [6.4.1]
* Control the detect word boundary behaviour.
# [6.4.0]
* Use `axis` to make the toolbar vertical.
* Use `toolbarIconCrossAlignment` to align the toolbar icons on the cross axis.
* Breaking change: `QuillToolbar`'s parameter `toolbarHeight` was renamed to `toolbarSize`.
# [6.3.5]
* Ability to add custom shortcuts.
# [6.3.4]
* Update clipboard status prior to showing selected text overlay.
# [6.3.3]
* Fixed handling of mac intents.
# [6.3.2]
* Added `unknownEmbedBuilder` to QuillEditor.
* Fix error style when input chinese japanese or korean.
# [6.3.1]
* Add color property to the basic factory function.

@ -90,7 +90,7 @@ You can then write this to storage.
To open a FlutterQuill editor with an existing JSON representation that you've previously stored, you can do something like this:
```dart
var myJSON = jsonDecode(incomingJSONText);
var myJSON = jsonDecode(r'{"insert":"hello\n"}');
_controller = QuillController(
document: Document.fromJson(myJSON),
selection: TextSelection.collapsed(offset: 0),
@ -346,7 +346,7 @@ QuillToolbar(locale: Locale('fr'), ...)
QuillEditor(locale: Locale('fr'), ...)
```
Currently, translations are available for these 25 locales:
Currently, translations are available for these 26 locales:
* `Locale('en')`
* `Locale('ar')`
@ -367,6 +367,7 @@ Currently, translations are available for these 25 locales:
* `Locale('pl')`
* `Locale('vi')`
* `Locale('id')`
* `Locale('ms')`
* `Locale('nl')`
* `Locale('no')`
* `Locale('fa')`

@ -80,7 +80,6 @@ class ImageVideoUtils {
context: context,
builder: (ctx) => AlertDialog(
contentPadding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
content: Column(
mainAxisSize: MainAxisSize.min,
children: [

@ -477,7 +477,7 @@ class PreserveInlineStylesRule extends InsertRule {
}
final itr = DeltaIterator(document);
final prev = itr.skip(index);
final prev = itr.skip(len == 0 ? index : index + 1);
if (prev == null ||
prev.data is! String ||
(prev.data as String).contains('\n')) {

@ -898,7 +898,41 @@ extension Localization on String {
'Next': 'הבא',
'Camera': 'מצלמה',
'Video': 'וידאו',
}
},
'ms': {
'Paste a link': 'Tampal Pautan',
'Ok': 'Ok',
'Select Color': 'Pilih Warna',
'Gallery': 'Galeri',
'Link': 'Pautan',
'Please first select some text to transform into a link.':
'Sila pilih beberapa patah perkataan'
' untuk diubah menjadi pautan.',
'Open': 'Buka',
'Copy': 'Salin',
'Remove': 'Buang',
'Save': 'Simpan',
'Zoom': 'Zum',
'Saved': 'Telah Disimpan',
'Text': 'Perkataan',
'What is entered is not a link': 'Apa yang diisi bukan pautan',
'Resize': 'Ubah saiz',
'Width': 'Lebar',
'Height': 'Tinggi',
'Size': 'Saiz',
'Small': 'Kecil',
'Large': 'Besar',
'Huge': 'Amat Besar',
'Clear': 'Padam',
'Font': 'Fon',
'Search': 'Carian',
'matches': 'padanan',
'showing match': 'menunjukkan padanan',
'Prev': 'Sebelum',
'Next': 'Seterusnya',
'Camera': 'Kamera',
'Video': 'Video',
},
};
String get i18n => localize(this, _t);

@ -26,6 +26,10 @@ bool isAppleOS([TargetPlatform? targetPlatform]) {
}
Future<bool> isIOSSimulator() async {
if (!isAppleOS()) {
return false;
}
final deviceInfo = DeviceInfoPlugin();
final osInfo = await deviceInfo.deviceInfo;

@ -66,7 +66,8 @@ class EditorTextSelectionGestureDetectorBuilder {
/// Creates a [EditorTextSelectionGestureDetectorBuilder].
///
/// The [delegate] must not be null.
EditorTextSelectionGestureDetectorBuilder({required this.delegate});
EditorTextSelectionGestureDetectorBuilder(
{required this.delegate, this.detectWordBoundary = true});
/// The delegate for this [EditorTextSelectionGestureDetectorBuilder].
///
@ -83,6 +84,8 @@ class EditorTextSelectionGestureDetectorBuilder {
/// a stylus.
bool shouldShowSelectionToolbar = true;
bool detectWordBoundary = true;
/// The [State] of the [EditableText] for which the builder will provide a
/// [EditorTextSelectionGestureDetector].
@protected
@ -337,24 +340,28 @@ class EditorTextSelectionGestureDetectorBuilder {
///
/// The [child] or its subtree should contain [EditableText].
Widget build(
{required HitTestBehavior behavior, required Widget child, Key? key}) {
{required HitTestBehavior behavior,
required Widget child,
Key? key,
bool detectWordBoundary = true}) {
return EditorTextSelectionGestureDetector(
key: key,
onTapDown: onTapDown,
onForcePressStart: delegate.forcePressEnabled ? onForcePressStart : null,
onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null,
onSingleTapUp: onSingleTapUp,
onSingleTapCancel: onSingleTapCancel,
onSingleLongTapStart: onSingleLongTapStart,
onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
onSingleLongTapEnd: onSingleLongTapEnd,
onDoubleTapDown: onDoubleTapDown,
onSecondarySingleTapUp: onSecondarySingleTapUp,
onDragSelectionStart: onDragSelectionStart,
onDragSelectionUpdate: onDragSelectionUpdate,
onDragSelectionEnd: onDragSelectionEnd,
behavior: behavior,
child: child,
);
key: key,
onTapDown: onTapDown,
onForcePressStart:
delegate.forcePressEnabled ? onForcePressStart : null,
onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null,
onSingleTapUp: onSingleTapUp,
onSingleTapCancel: onSingleTapCancel,
onSingleLongTapStart: onSingleLongTapStart,
onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
onSingleLongTapEnd: onSingleLongTapEnd,
onDoubleTapDown: onDoubleTapDown,
onSecondarySingleTapUp: onSecondarySingleTapUp,
onDragSelectionStart: onDragSelectionStart,
onDragSelectionUpdate: onDragSelectionUpdate,
onDragSelectionEnd: onDragSelectionEnd,
behavior: behavior,
detectWordBoundary: detectWordBoundary,
child: child);
}
}

@ -180,6 +180,9 @@ class QuillEditor extends StatefulWidget {
this.floatingCursorDisabled = false,
this.textSelectionControls,
this.onImagePaste,
this.customShortcuts,
this.customActions,
this.detectWordBoundary = true,
Key? key})
: super(key: key);
@ -394,6 +397,11 @@ class QuillEditor extends StatefulWidget {
/// Returns the url of the image if the image should be inserted.
final Future<String?> Function(Uint8List imageBytes)? onImagePaste;
final Map<LogicalKeySet, Intent>? customShortcuts;
final Map<Type, Action<Intent>>? customActions;
final bool detectWordBoundary;
@override
QuillEditorState createState() => QuillEditorState();
}
@ -408,7 +416,8 @@ class QuillEditorState extends State<QuillEditor>
void initState() {
super.initState();
_selectionGestureDetectorBuilder =
_QuillEditorSelectionGestureDetectorBuilder(this);
_QuillEditorSelectionGestureDetectorBuilder(
this, widget.detectWordBoundary);
}
@override
@ -458,12 +467,8 @@ class QuillEditorState extends State<QuillEditor>
readOnly: widget.readOnly,
placeholder: widget.placeholder,
onLaunchUrl: widget.onLaunchUrl,
toolbarOptions: ToolbarOptions(
copy: showSelectionToolbar,
cut: showSelectionToolbar,
paste: showSelectionToolbar,
selectAll: showSelectionToolbar,
),
contextMenuBuilder:
showSelectionToolbar ? RawEditor.defaultContextMenuBuilder : null,
showSelectionHandles: isMobile(theme.platform),
showCursor: widget.showCursor,
cursorStyle: CursorStyle(
@ -494,16 +499,18 @@ class QuillEditorState extends State<QuillEditor>
readOnly,
) =>
_buildCustomBlockEmbed(
node,
context,
controller,
readOnly,
widget.unknownEmbedBuilder,
),
node,
context,
controller,
readOnly,
widget.unknownEmbedBuilder,
),
linkActionPickerDelegate: widget.linkActionPickerDelegate,
customStyleBuilder: widget.customStyleBuilder,
floatingCursorDisabled: widget.floatingCursorDisabled,
onImagePaste: widget.onImagePaste,
customShortcuts: widget.customShortcuts,
customActions: widget.customActions,
);
final editor = I18n(
@ -511,6 +518,7 @@ class QuillEditorState extends State<QuillEditor>
child: selectionEnabled
? _selectionGestureDetectorBuilder.build(
behavior: HitTestBehavior.translucent,
detectWordBoundary: widget.detectWordBoundary,
child: child,
)
: child,
@ -541,7 +549,7 @@ class QuillEditorState extends State<QuillEditor>
EmbedsBuilder? unknownEmbedBuilder,
) {
final builders = widget.embedBuilders;
var _node = node;
// Creates correct node for custom embed
if (node.value.type == BlockEmbed.customType) {
@ -555,7 +563,7 @@ class QuillEditorState extends State<QuillEditor>
}
}
}
if (unknownEmbedBuilder != null) {
return unknownEmbedBuilder(context, controller, _node, readOnly);
}
@ -584,10 +592,12 @@ class QuillEditorState extends State<QuillEditor>
class _QuillEditorSelectionGestureDetectorBuilder
extends EditorTextSelectionGestureDetectorBuilder {
_QuillEditorSelectionGestureDetectorBuilder(this._state)
: super(delegate: _state);
_QuillEditorSelectionGestureDetectorBuilder(
this._state, this._detectWordBoundary)
: super(delegate: _state, detectWordBoundary: _detectWordBoundary);
final QuillEditorState _state;
final bool _detectWordBoundary;
@override
void onForcePressStart(ForcePressDetails details) {
@ -705,9 +715,15 @@ class _QuillEditorSelectionGestureDetectorBuilder
case PointerDeviceKind.unknown:
// On macOS/iOS/iPadOS a touch tap places the cursor at the edge
// of the word.
renderEditor!
..selectWordEdge(SelectionChangedCause.tap)
..onSelectionCompleted();
if (_detectWordBoundary) {
renderEditor!
..selectWordEdge(SelectionChangedCause.tap)
..onSelectionCompleted();
} else {
renderEditor!
..selectPosition(cause: SelectionChangedCause.tap)
..onSelectionCompleted();
}
break;
case PointerDeviceKind.trackpad:
// TODO: Handle this case.

@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;
// ignore: unnecessary_import
import 'dart:typed_data';
@ -57,12 +58,7 @@ class RawEditor extends StatefulWidget {
this.readOnly = false,
this.placeholder,
this.onLaunchUrl,
this.toolbarOptions = const ToolbarOptions(
copy: true,
cut: true,
paste: true,
selectAll: true,
),
this.contextMenuBuilder = defaultContextMenuBuilder,
this.showSelectionHandles = false,
bool? showCursor,
this.textCapitalization = TextCapitalization.none,
@ -70,6 +66,8 @@ class RawEditor extends StatefulWidget {
this.minHeight,
this.maxContentWidth,
this.customStyles,
this.customShortcuts,
this.customActions,
this.expands = false,
this.autoFocus = false,
this.keyboardAppearance = Brightness.light,
@ -112,11 +110,24 @@ class RawEditor extends StatefulWidget {
/// a link in the document.
final ValueChanged<String>? onLaunchUrl;
/// Configuration of toolbar options.
/// Builds the text selection toolbar when requested by the user.
///
/// See also:
/// * [EditableText.contextMenuBuilder], which builds the default
/// text selection toolbar for [EditableText].
///
/// By default, all options are enabled. If [readOnly] is true,
/// paste and cut will be disabled regardless.
final ToolbarOptions toolbarOptions;
/// If not provided, no context menu will be shown.
final QuillEditorContextMenuBuilder? contextMenuBuilder;
static Widget defaultContextMenuBuilder(
BuildContext context,
RawEditorState state,
) {
return AdaptiveTextSelectionToolbar.buttonItems(
buttonItems: state.contextMenuButtonItems,
anchors: state.contextMenuAnchors,
);
}
/// Whether to show selection handles.
///
@ -227,6 +238,9 @@ class RawEditor extends StatefulWidget {
final Future<String?> Function(Uint8List imageBytes)? onImagePaste;
final Map<LogicalKeySet, Intent>? customShortcuts;
final Map<Type, Action<Intent>>? customActions;
/// Builder function for embeddable objects.
final EmbedsBuilder embedBuilder;
final LinkActionPickerDelegate linkActionPickerDelegate;
@ -288,6 +302,74 @@ class RawEditorState extends EditorState
TextDirection get _textDirection => Directionality.of(context);
/// Returns the [ContextMenuButtonItem]s representing the buttons in this
/// platform's default selection menu for [RawEditor].
///
/// Copied from [EditableTextState].
List<ContextMenuButtonItem> get contextMenuButtonItems {
return EditableText.getEditableButtonItems(
clipboardStatus: _clipboardStatus.value,
onCopy: copyEnabled
? () => copySelection(SelectionChangedCause.toolbar)
: null,
onCut:
cutEnabled ? () => cutSelection(SelectionChangedCause.toolbar) : null,
onPaste:
pasteEnabled ? () => pasteText(SelectionChangedCause.toolbar) : null,
onSelectAll: selectAllEnabled
? () => selectAll(SelectionChangedCause.toolbar)
: 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.item1,
endGlyphHeight: glyphHeights.item2,
selectionEndpoints: points,
);
}
/// Gets the line heights at the start and end of the selection for the given
/// [RawEditorState].
///
/// Copied from [EditableTextState].
Tuple2<double, double> _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 Tuple2(
renderEditor.preferredLineHeight(selection.base),
renderEditor.preferredLineHeight(selection.base),
);
}
final startCharacterRect =
renderEditor.getLocalRectForCaret(selection.base);
final endCharacterRect =
renderEditor.getLocalRectForCaret(selection.extent);
return Tuple2(
startCharacterRect.height,
endCharacterRect.height,
);
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
@ -428,9 +510,14 @@ class RawEditorState extends EditorState
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift,
LogicalKeyboardKey.keyL): const ApplyCheckListIntent(),
if (widget.customShortcuts != null) ...widget.customShortcuts!,
},
child: Actions(
actions: _actions,
actions: {
..._actions,
if (widget.customActions != null) ...widget.customActions!,
},
child: Focus(
focusNode: widget.focusNode,
onKey: _onKey,
@ -758,6 +845,9 @@ class RawEditorState extends EditorState
if (isKeyboardOS()) {
_keyboardVisible = true;
} else if (!kIsWeb && Platform.environment.containsKey('FLUTTER_TEST')) {
// treat tests like a keyboard OS
_keyboardVisible = true;
} else {
// treat iOS Simulator like a keyboard OS
isIOSSimulator().then((isIosSimulator) {
@ -964,13 +1054,15 @@ class RawEditorState extends EditorState
value: textEditingValue,
context: context,
debugRequiredFor: widget,
toolbarLayerLink: _toolbarLayerLink,
startHandleLayerLink: _startHandleLayerLink,
endHandleLayerLink: _endHandleLayerLink,
renderObject: renderEditor,
selectionCtrls: widget.selectionCtrls,
selectionDelegate: this,
clipboardStatus: _clipboardStatus,
contextMenuBuilder: widget.contextMenuBuilder == null
? null
: (context) => widget.contextMenuBuilder!(context, this),
);
_selectionOverlay!.handlesVisible = _shouldShowSelectionHandles();
_selectionOverlay!.showHandles();
@ -1431,7 +1523,14 @@ class RawEditorState extends EditorState
@override
void performSelector(String selectorName) {
// TODO: implement performSelector
final intent = intentForMacOSSelector(selectorName);
if (intent != null) {
final primaryContext = primaryFocus?.context;
if (primaryContext != null) {
Actions.invoke(primaryContext, intent);
}
}
}
}
@ -2316,3 +2415,15 @@ class _ApplyCheckListAction extends Action<ApplyCheckListIntent> {
@override
bool get isActionEnabled => true;
}
/// Signature for a widget builder that builds a context menu for the given
/// [RawEditorState].
///
/// See also:
///
/// * [EditableTextContextMenuBuilder], which performs the same role for
/// [EditableText]
typedef QuillEditorContextMenuBuilder = Widget Function(
BuildContext context,
RawEditorState rawEditorState,
);

@ -150,14 +150,15 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState
}
@override
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly;
bool get cutEnabled => widget.contextMenuBuilder != null && !widget.readOnly;
@override
bool get copyEnabled => widget.toolbarOptions.copy;
bool get copyEnabled => widget.contextMenuBuilder != null;
@override
bool get pasteEnabled => widget.toolbarOptions.paste && !widget.readOnly;
bool get pasteEnabled =>
widget.contextMenuBuilder != null && !widget.readOnly;
@override
bool get selectAllEnabled => widget.toolbarOptions.selectAll;
bool get selectAllEnabled => widget.contextMenuBuilder != null;
}

@ -69,7 +69,6 @@ class EditorTextSelectionOverlay {
EditorTextSelectionOverlay({
required this.value,
required this.context,
required this.toolbarLayerLink,
required this.startHandleLayerLink,
required this.endHandleLayerLink,
required this.renderObject,
@ -77,14 +76,18 @@ class EditorTextSelectionOverlay {
required this.selectionCtrls,
required this.selectionDelegate,
required this.clipboardStatus,
required this.contextMenuBuilder,
this.onSelectionHandleTapped,
this.dragStartBehavior = DragStartBehavior.start,
this.handlesVisible = false,
}) {
final overlay = Overlay.of(context, rootOverlay: true);
_toolbarController = AnimationController(
duration: const Duration(milliseconds: 150), vsync: overlay);
// Clipboard status is only checked on first instance of
// ClipboardStatusNotifier
// if state has changed after creation, but prior to
// our listener being created
// we won't know the status unless there is forced update
// i.e. occasionally no paste
clipboardStatus.update();
}
TextEditingValue value;
@ -114,10 +117,6 @@ class EditorTextSelectionOverlay {
/// Debugging information for explaining why the [Overlay] is required.
final Widget debugRequiredFor;
/// The object supplied to the [CompositedTransformTarget] that wraps the text
/// field.
final LayerLink toolbarLayerLink;
/// The objects supplied to the [CompositedTransformTarget] that wraps the
/// location of start selection handle.
final LayerLink startHandleLayerLink;
@ -136,6 +135,11 @@ class EditorTextSelectionOverlay {
/// text field.
final TextSelectionDelegate selectionDelegate;
/// {@macro flutter.widgets.EditableText.contextMenuBuilder}
///
/// If not provided, no context menu will be built.
final WidgetBuilder? contextMenuBuilder;
/// Determines the way that drag start behavior is handled.
///
/// If set to [DragStartBehavior.start], handle drag behavior will
@ -169,7 +173,6 @@ class EditorTextSelectionOverlay {
/// Useful because the actual value of the clipboard can only be checked
/// asynchronously (see [Clipboard.getData]).
final ClipboardStatusNotifier clipboardStatus;
late AnimationController _toolbarController;
/// A pair of handles. If this is non-null, there are always 2, though the
/// second is hidden when the selection is collapsed.
@ -180,8 +183,6 @@ class EditorTextSelectionOverlay {
TextSelection get _selection => value.selection;
Animation<double> get _toolbarOpacity => _toolbarController.view;
void setHandlesVisible(bool visible) {
if (handlesVisible == visible) {
return;
@ -212,7 +213,6 @@ class EditorTextSelectionOverlay {
/// To hide the whole overlay, see [hide].
void hideToolbar() {
assert(toolbar != null);
_toolbarController.stop();
toolbar!.remove();
toolbar = null;
}
@ -220,10 +220,12 @@ class EditorTextSelectionOverlay {
/// Shows the toolbar by inserting it into the [context]'s overlay.
void showToolbar() {
assert(toolbar == null);
toolbar = OverlayEntry(builder: _buildToolbar);
if (contextMenuBuilder == null) return;
toolbar = OverlayEntry(builder: (context) {
return contextMenuBuilder!(context);
});
Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)
.insert(toolbar!);
_toolbarController.forward(from: 0);
// make sure handles are visible as well
if (_handles == null) {
@ -311,63 +313,6 @@ class EditorTextSelectionOverlay {
..bringIntoView(textPosition);
}
Widget _buildToolbar(BuildContext context) {
// Find the horizontal midpoint, just above the selected text.
List<TextSelectionPoint> endpoints;
try {
// building with an invalid selection with throw an exception
// This happens where the selection has changed, but the toolbar
// hasn't been dismissed yet.
endpoints = renderObject.getEndpointsForSelection(_selection);
} catch (_) {
return Container();
}
final editingRegion = Rect.fromPoints(
renderObject.localToGlobal(Offset.zero),
renderObject.localToGlobal(renderObject.size.bottomRight(Offset.zero)),
);
final baseLineHeight = renderObject.preferredLineHeight(_selection.base);
final extentLineHeight =
renderObject.preferredLineHeight(_selection.extent);
final smallestLineHeight = math.min(baseLineHeight, extentLineHeight);
final isMultiline = endpoints.last.point.dy - endpoints.first.point.dy >
smallestLineHeight / 2;
// If the selected text spans more than 1 line,
// horizontally center the toolbar.
// Derived from both iOS and Android.
final midX = isMultiline
? editingRegion.width / 2
: (endpoints.first.point.dx + endpoints.last.point.dx) / 2;
final midpoint = Offset(
midX,
// The y-coordinate won't be made use of most likely.
endpoints[0].point.dy - baseLineHeight,
);
return FadeTransition(
opacity: _toolbarOpacity,
child: CompositedTransformFollower(
link: toolbarLayerLink,
showWhenUnlinked: false,
offset: -editingRegion.topLeft,
child: selectionCtrls.buildToolbar(
context,
editingRegion,
baseLineHeight,
midpoint,
endpoints,
selectionDelegate,
clipboardStatus,
null),
),
);
}
void markNeedsBuild([Duration? duration]) {
if (_handles != null) {
_handles![0].markNeedsBuild();
@ -391,7 +336,6 @@ class EditorTextSelectionOverlay {
/// Final cleanup.
void dispose() {
hide();
_toolbarController.dispose();
}
/// Builds the handles by inserting them into the [context]'s overlay.
@ -706,6 +650,7 @@ class EditorTextSelectionGestureDetector extends StatefulWidget {
this.onDragSelectionUpdate,
this.onDragSelectionEnd,
this.behavior,
this.detectWordBoundary = true,
Key? key,
}) : super(key: key);
@ -781,6 +726,8 @@ class EditorTextSelectionGestureDetector extends StatefulWidget {
/// Child below this widget.
final Widget child;
final bool detectWordBoundary;
@override
State<StatefulWidget> createState() =>
_EditorTextSelectionGestureDetectorState();

@ -45,8 +45,10 @@ const double kIconButtonFactor = 1.77;
class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
const QuillToolbar({
required this.children,
this.toolbarHeight = 36,
this.axis = Axis.horizontal,
this.toolbarSize = 36,
this.toolbarIconAlignment = WrapAlignment.center,
this.toolbarIconCrossAlignment = WrapCrossAlignment.center,
this.toolbarSectionSpacing = 4,
this.multiRowsDisplay = true,
this.color,
@ -58,9 +60,11 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
factory QuillToolbar.basic({
required QuillController controller,
Axis axis = Axis.horizontal,
double toolbarIconSize = kDefaultIconSize,
double toolbarSectionSpacing = 4,
WrapAlignment toolbarIconAlignment = WrapAlignment.center,
WrapCrossAlignment toolbarIconCrossAlignment = WrapCrossAlignment.center,
bool showDividers = true,
bool showFontFamily = true,
bool showFontSize = true,
@ -170,10 +174,12 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
return QuillToolbar(
key: key,
axis: axis,
color: color,
toolbarHeight: toolbarIconSize * 2,
toolbarSize: toolbarIconSize * 2,
toolbarSectionSpacing: toolbarSectionSpacing,
toolbarIconAlignment: toolbarIconAlignment,
toolbarIconCrossAlignment: toolbarIconCrossAlignment,
multiRowsDisplay: multiRowsDisplay,
customButtons: customButtons,
locale: locale,
@ -334,11 +340,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
isButtonGroupShown[3] ||
isButtonGroupShown[4] ||
isButtonGroupShown[5]))
VerticalDivider(
indent: 12,
endIndent: 12,
color: Colors.grey.shade400,
),
_dividerOnAxis(axis),
if (showAlignmentButtons)
SelectAlignmentButton(
controller: controller,
@ -365,14 +367,11 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
isButtonGroupShown[3] ||
isButtonGroupShown[4] ||
isButtonGroupShown[5]))
VerticalDivider(
indent: 12,
endIndent: 12,
color: Colors.grey.shade400,
),
_dividerOnAxis(axis),
if (showHeaderStyle)
SelectHeaderStyleButton(
controller: controller,
axis: axis,
iconSize: toolbarIconSize,
iconTheme: iconTheme,
afterButtonPressed: afterButtonPressed,
@ -383,11 +382,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
(isButtonGroupShown[3] ||
isButtonGroupShown[4] ||
isButtonGroupShown[5]))
VerticalDivider(
indent: 12,
endIndent: 12,
color: Colors.grey.shade400,
),
_dividerOnAxis(axis),
if (showListNumbers)
ToggleStyleButton(
attribute: Attribute.ol,
@ -427,11 +422,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
if (showDividers &&
isButtonGroupShown[3] &&
(isButtonGroupShown[4] || isButtonGroupShown[5]))
VerticalDivider(
indent: 12,
endIndent: 12,
color: Colors.grey.shade400,
),
_dividerOnAxis(axis),
if (showQuote)
ToggleStyleButton(
attribute: Attribute.blockQuote,
@ -460,11 +451,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
afterButtonPressed: afterButtonPressed,
),
if (showDividers && isButtonGroupShown[4] && isButtonGroupShown[5])
VerticalDivider(
indent: 12,
endIndent: 12,
color: Colors.grey.shade400,
),
_dividerOnAxis(axis),
if (showLink)
LinkStyleButton(
controller: controller,
@ -483,12 +470,7 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
afterButtonPressed: afterButtonPressed,
),
if (customButtons.isNotEmpty)
if (showDividers)
VerticalDivider(
indent: 12,
endIndent: 12,
color: Colors.grey.shade400,
),
if (showDividers) _dividerOnAxis(axis),
for (var customButton in customButtons)
QuillIconButton(
highlightElevation: 0,
@ -503,10 +485,28 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
);
}
static Widget _dividerOnAxis(Axis axis) {
if (axis == Axis.horizontal) {
return const VerticalDivider(
indent: 12,
endIndent: 12,
color: Colors.grey,
);
} else {
return const Divider(
indent: 12,
endIndent: 12,
color: Colors.grey,
);
}
}
final List<Widget> children;
final double toolbarHeight;
final Axis axis;
final double toolbarSize;
final double toolbarSectionSpacing;
final WrapAlignment toolbarIconAlignment;
final WrapCrossAlignment toolbarIconCrossAlignment;
final bool multiRowsDisplay;
/// The color of the toolbar.
@ -523,7 +523,9 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
final List<QuillCustomButton> customButtons;
@override
Size get preferredSize => Size.fromHeight(toolbarHeight);
Size get preferredSize => axis == Axis.horizontal
? Size.fromHeight(toolbarSize)
: Size.fromWidth(toolbarSize);
@override
Widget build(BuildContext context) {
@ -531,16 +533,23 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget {
initialLocale: locale,
child: multiRowsDisplay
? Wrap(
direction: axis,
alignment: toolbarIconAlignment,
crossAxisAlignment: toolbarIconCrossAlignment,
runSpacing: 4,
spacing: toolbarSectionSpacing,
children: children,
)
: Container(
constraints:
BoxConstraints.tightFor(height: preferredSize.height),
constraints: BoxConstraints.tightFor(
height: axis == Axis.horizontal ? toolbarSize : null,
width: axis == Axis.vertical ? toolbarSize : null,
),
color: color ?? Theme.of(context).canvasColor,
child: ArrowIndicatedButtonList(buttons: children),
child: ArrowIndicatedButtonList(
axis: axis,
buttons: children,
),
),
);
}

@ -7,9 +7,13 @@ import 'package:flutter/material.dart';
/// The arrow indicators are automatically hidden if the list is not
/// scrollable in the direction of the respective arrow.
class ArrowIndicatedButtonList extends StatefulWidget {
const ArrowIndicatedButtonList({required this.buttons, Key? key})
: super(key: key);
const ArrowIndicatedButtonList({
required this.axis,
required this.buttons,
Key? key,
}) : super(key: key);
final Axis axis;
final List<Widget> buttons;
@override
@ -20,8 +24,8 @@ class ArrowIndicatedButtonList extends StatefulWidget {
class _ArrowIndicatedButtonListState extends State<ArrowIndicatedButtonList>
with WidgetsBindingObserver {
final ScrollController _controller = ScrollController();
bool _showLeftArrow = false;
bool _showRightArrow = false;
bool _showBackwardArrow = false;
bool _showForwardArrow = false;
@override
void initState() {
@ -40,13 +44,19 @@ class _ArrowIndicatedButtonListState extends State<ArrowIndicatedButtonList>
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
_buildLeftArrow(),
_buildScrollableList(),
_buildRightColor(),
],
);
final children = <Widget>[
_buildBackwardArrow(),
_buildScrollableList(),
_buildForwardArrow(),
];
return widget.axis == Axis.horizontal
? Row(
children: children,
)
: Column(
children: children,
);
}
@override
@ -63,20 +73,29 @@ class _ArrowIndicatedButtonListState extends State<ArrowIndicatedButtonList>
if (!mounted) return;
setState(() {
_showLeftArrow =
_showBackwardArrow =
_controller.position.minScrollExtent != _controller.position.pixels;
_showRightArrow =
_showForwardArrow =
_controller.position.maxScrollExtent != _controller.position.pixels;
});
}
Widget _buildLeftArrow() {
Widget _buildBackwardArrow() {
IconData? icon;
if (_showBackwardArrow) {
if (widget.axis == Axis.horizontal) {
icon = Icons.arrow_left;
} else {
icon = Icons.arrow_drop_up;
}
}
return SizedBox(
width: 8,
child: Transform.translate(
// Move the icon a few pixels to center it
offset: const Offset(-5, 0),
child: _showLeftArrow ? const Icon(Icons.arrow_left, size: 18) : null,
child: icon != null ? Icon(icon, size: 18) : null,
),
);
}
@ -87,18 +106,24 @@ class _ArrowIndicatedButtonListState extends State<ArrowIndicatedButtonList>
// Remove the glowing effect, as we already have the arrow indicators
behavior: _NoGlowBehavior(),
// The CustomScrollView is necessary so that the children are not
// stretched to the height of the toolbar, https://bit.ly/3uC3bjI
// stretched to the height of the toolbar:
// https://stackoverflow.com/a/65998731/7091839
child: CustomScrollView(
scrollDirection: Axis.horizontal,
scrollDirection: widget.axis,
controller: _controller,
physics: const ClampingScrollPhysics(),
slivers: [
SliverFillRemaining(
hasScrollBody: false,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: widget.buttons,
),
child: widget.axis == Axis.horizontal
? Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: widget.buttons,
)
: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: widget.buttons,
),
)
],
),
@ -106,13 +131,22 @@ class _ArrowIndicatedButtonListState extends State<ArrowIndicatedButtonList>
);
}
Widget _buildRightColor() {
Widget _buildForwardArrow() {
IconData? icon;
if (_showForwardArrow) {
if (widget.axis == Axis.horizontal) {
icon = Icons.arrow_right;
} else {
icon = Icons.arrow_drop_down;
}
}
return SizedBox(
width: 8,
child: Transform.translate(
// Move the icon a few pixels to center it
offset: const Offset(-5, 0),
child: _showRightArrow ? const Icon(Icons.arrow_right, size: 18) : null,
child: icon != null ? Icon(icon, size: 18) : null,
),
);
}

@ -10,6 +10,7 @@ import '../toolbar.dart';
class SelectHeaderStyleButton extends StatefulWidget {
const SelectHeaderStyleButton({
required this.controller,
this.axis = Axis.horizontal,
this.iconSize = kDefaultIconSize,
this.iconTheme,
this.attributes = const [
@ -23,6 +24,7 @@ class SelectHeaderStyleButton extends StatefulWidget {
}) : super(key: key);
final QuillController controller;
final Axis axis;
final double iconSize;
final QuillIconTheme? iconTheme;
final List<Attribute> attributes;
@ -67,53 +69,60 @@ class _SelectHeaderStyleButtonState extends State<SelectHeaderStyleButton> {
fontSize: widget.iconSize * 0.7,
);
return Row(
mainAxisSize: MainAxisSize.min,
children: widget.attributes.map((attribute) {
final isSelected = _selectedAttribute == attribute;
return Padding(
// ignore: prefer_const_constructors
padding: EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0),
child: ConstrainedBox(
constraints: BoxConstraints.tightFor(
width: widget.iconSize * kIconButtonFactor,
height: widget.iconSize * kIconButtonFactor,
),
child: RawMaterialButton(
hoverElevation: 0,
highlightElevation: 0,
elevation: 0,
visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
widget.iconTheme?.borderRadius ?? 2)),
fillColor: isSelected
? (widget.iconTheme?.iconSelectedFillColor ??
Theme.of(context).primaryColor)
: (widget.iconTheme?.iconUnselectedFillColor ??
theme.canvasColor),
onPressed: () {
final _attribute = _selectedAttribute == attribute
? Attribute.header
: attribute;
widget.controller.formatSelection(_attribute);
widget.afterButtonPressed?.call();
},
child: Text(
_valueToText[attribute] ?? '',
style: style.copyWith(
color: isSelected
? (widget.iconTheme?.iconSelectedColor ??
theme.primaryIconTheme.color)
: (widget.iconTheme?.iconUnselectedColor ??
theme.iconTheme.color),
),
final children = widget.attributes.map((attribute) {
final isSelected = _selectedAttribute == attribute;
return Padding(
// ignore: prefer_const_constructors
padding: EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0),
child: ConstrainedBox(
constraints: BoxConstraints.tightFor(
width: widget.iconSize * kIconButtonFactor,
height: widget.iconSize * kIconButtonFactor,
),
child: RawMaterialButton(
hoverElevation: 0,
highlightElevation: 0,
elevation: 0,
visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(widget.iconTheme?.borderRadius ?? 2)),
fillColor: isSelected
? (widget.iconTheme?.iconSelectedFillColor ??
Theme.of(context).primaryColor)
: (widget.iconTheme?.iconUnselectedFillColor ??
theme.canvasColor),
onPressed: () {
final _attribute = _selectedAttribute == attribute
? Attribute.header
: attribute;
widget.controller.formatSelection(_attribute);
widget.afterButtonPressed?.call();
},
child: Text(
_valueToText[attribute] ?? '',
style: style.copyWith(
color: isSelected
? (widget.iconTheme?.iconSelectedColor ??
theme.primaryIconTheme.color)
: (widget.iconTheme?.iconUnselectedColor ??
theme.iconTheme.color),
),
),
),
);
}).toList(),
);
),
);
}).toList();
return widget.axis == Axis.horizontal
? Row(
mainAxisSize: MainAxisSize.min,
children: children,
)
: Column(
mainAxisSize: MainAxisSize.min,
children: children,
);
}
void _didChangeEditingValue() {

@ -1,6 +1,6 @@
name: flutter_quill
description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us)
version: 6.3.1
version: 6.4.4
#author: bulletjournal
homepage: https://bulletjournal.us/home/index.html
repository: https://github.com/singerdmx/flutter-quill
@ -12,17 +12,17 @@ environment:
dependencies:
flutter:
sdk: flutter
collection: ^1.16.0
collection: ^1.17.0
flutter_colorpicker: ^1.0.3
flutter_keyboard_visibility: ^5.2.0
quiver: ^3.1.0
tuple: ^2.0.0
url_launcher: ^6.1.2
flutter_keyboard_visibility: ^5.4.0
quiver: ^3.2.1
tuple: ^2.0.1
url_launcher: ^6.1.9
pedantic: ^1.11.1
characters: ^1.2.0
characters: ^1.2.1
diff_match_patch: ^0.4.1
i18n_extension: ^6.0.0
device_info_plus: ^8.0.0
i18n_extension: ^7.0.0
device_info_plus: ^8.1.0
platform: ^3.1.0
pasteboard: ^0.2.0

Loading…
Cancel
Save