Skip to content

Fix selection in readOnly mode, Add magnifier via RawMagnifier widget #2529

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Can't select text when `readOnly` is true [#2529](https://github.com/singerdmx/flutter-quill/pull/2529).

### Added

- Display magnifier using `RawMagnifier` widget when dragging on iOS/Android [#2529](https://github.com/singerdmx/flutter-quill/pull/2529).

## [11.2.0] - 2025-03-26

### Added
Expand Down
1 change: 1 addition & 0 deletions lib/flutter_quill.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export 'src/editor/style_widgets/style_widgets.dart';
export 'src/editor/widgets/cursor.dart';
export 'src/editor/widgets/default_styles.dart';
export 'src/editor/widgets/link.dart';
export 'src/editor/widgets/text/magnifier.dart';
export 'src/editor/widgets/text/utils/text_block_utils.dart';
export 'src/editor_toolbar_controller_shared/copy_cut_service/copy_cut_service.dart';
export 'src/editor_toolbar_controller_shared/copy_cut_service/copy_cut_service_provider.dart';
Expand Down
10 changes: 10 additions & 0 deletions lib/src/editor/config/editor_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import '../raw_editor/raw_editor.dart';
import '../widgets/default_styles.dart';
import '../widgets/delegate.dart';
import '../widgets/link.dart';
import '../widgets/text/magnifier.dart';
import '../widgets/text/utils/text_block_utils.dart';
import 'search_config.dart';

Expand Down Expand Up @@ -56,6 +57,7 @@ class QuillEditorConfig {
this.enableAlwaysIndentOnTab = false,
this.embedBuilders,
this.textSpanBuilder = defaultSpanBuilder,
this.quillMagnifierBuilder,
this.unknownEmbedBuilder,
@experimental this.searchConfig = const QuillSearchConfig(),
this.linkActionPickerDelegate = defaultLinkActionPickerDelegate,
Expand Down Expand Up @@ -365,6 +367,14 @@ class QuillEditorConfig {

final TextSpanBuilder textSpanBuilder;

/// To add a magnifier when selecting, specify a builder that returns the magnfier widget
///
/// The default is no magnifier
///
/// There is a provided magnifier [QuillMagnifier] that is available via the function
/// defaultQuillMagnifierBuilder
final QuillMagnifierBuilder? quillMagnifierBuilder;

/// See [search](https://github.com/singerdmx/flutter-quill/blob/master/doc/configurations/search.md)
/// page for docs.
@experimental
Expand Down
7 changes: 7 additions & 0 deletions lib/src/editor/editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ class QuillEditorState extends State<QuillEditor>
QuillEditorConfig get configurations => widget.config;
QuillEditorConfig get config => widget.config;

/// {@macro drag_offset_notifier}
final dragOffsetNotifier = isMobileApp ? ValueNotifier<Offset?>(null) : null;

@override
void initState() {
super.initState();
Expand Down Expand Up @@ -260,6 +263,7 @@ class QuillEditorState extends State<QuillEditor>
final child = QuillRawEditor(
key: _editorKey,
controller: controller,
dragOffsetNotifier: dragOffsetNotifier,
config: QuillRawEditorConfig(
characterShortcutEvents: widget.config.characterShortcutEvents,
spaceShortcutEvents: widget.config.spaceShortcutEvents,
Expand Down Expand Up @@ -305,6 +309,7 @@ class QuillEditorState extends State<QuillEditor>
scrollPhysics: config.scrollPhysics,
embedBuilder: _getEmbedBuilder,
textSpanBuilder: config.textSpanBuilder,
quillMagnifierBuilder: config.quillMagnifierBuilder,
linkActionPickerDelegate: config.linkActionPickerDelegate,
customStyleBuilder: config.customStyleBuilder,
customRecognizerBuilder: config.customRecognizerBuilder,
Expand All @@ -330,6 +335,8 @@ class QuillEditorState extends State<QuillEditor>
behavior: HitTestBehavior.translucent,
detectWordBoundary: config.detectWordBoundary,
child: child,
dragOffsetNotifier: dragOffsetNotifier,
quillMagnifierBuilder: config.quillMagnifierBuilder,
)
: child;

Expand Down
5 changes: 5 additions & 0 deletions lib/src/editor/raw_editor/config/raw_editor_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import '../../../editor/widgets/default_styles.dart';
import '../../../editor/widgets/delegate.dart';
import '../../../editor/widgets/link.dart';
import '../../../toolbar/theme/quill_dialog_theme.dart';
import '../../widgets/text/magnifier.dart';
import '../../widgets/text/utils/text_block_utils.dart';
import '../builders/leading_block_builder.dart';
import 'events/events.dart';
Expand Down Expand Up @@ -70,6 +71,7 @@ class QuillRawEditorConfig {
this.readOnlyMouseCursor = SystemMouseCursors.text,
this.onPerformAction,
@experimental this.customLeadingBuilder,
this.quillMagnifierBuilder,
});

/// Controls whether this editor has keyboard focus.
Expand Down Expand Up @@ -408,4 +410,7 @@ class QuillRawEditorConfig {

/// Called when a text input action is performed.
final void Function(TextInputAction action)? onPerformAction;

/// Used to build the [QuillMagnifier] when long-pressing/dragging selection
final QuillMagnifierBuilder? quillMagnifierBuilder;
}
23 changes: 23 additions & 0 deletions lib/src/editor/raw_editor/raw_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class QuillRawEditor extends StatefulWidget {
QuillRawEditor({
required this.config,
required this.controller,
this.dragOffsetNotifier,
super.key,
}) : assert(config.maxHeight == null || config.maxHeight! > 0,
'maxHeight cannot be null'),
Expand All @@ -25,6 +26,28 @@ class QuillRawEditor extends StatefulWidget {
final QuillController controller;
final QuillRawEditorConfig config;

/// {@template drag_offset_notifier}
/// dragOffsetNotifier - Only used on iOS and Android
///
/// [QuillRawEditor] contains a gesture detector [EditorTextSelectionGestureDetector]
/// within it's widget tree that includes a [RawMagnifier]. The RawMagnifier needs
/// the current position of selection drag events in order to display the magnifier
/// in the correct location. Setting the position to null will hide the magnifier.
///
/// Initial selection events are posted by [EditorTextSelectionGestureDetector]. Once
/// a selection has been created, dragging the selection handles happens in
/// [EditorTextSelectionOverlay].
///
/// Both [EditorTextSelectionGestureDetector] and [EditorTextSelectionOverlay] will update
/// the value of the dragOffsetNotifier.
///
/// The [EditorTextSelectionGestureDetector] will use the value to display the magnifier in
/// the correct location (or hide the magnifier if null). [EditorTextSelectionOverlay] will
/// use the value of the dragOffsetNotifier to hide the context menu when the magnifier is
/// displayed and show the context menu when dragging is complete.
/// {@endtemplate}
final ValueNotifier<Offset?>? dragOffsetNotifier;

@override
State<StatefulWidget> createState() => QuillRawEditorState();
}
Expand Down
6 changes: 4 additions & 2 deletions lib/src/editor/raw_editor/raw_editor_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -853,8 +853,9 @@ class QuillRawEditorState extends EditorState
});
}

controller.addListener(_didChangeTextEditingValueListener);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's looks like this PR removes the lines added by #2488 but keet some of the other changes, does this PR handle any related issues or conflicts with that PR? If no, then we need to revert that PR first, then merge this but revert the selection fix changes.


if (!widget.config.readOnly) {
controller.addListener(_didChangeTextEditingValueListener);
// listen to composing range changes
composingRange.addListener(_onComposingRangeChanged);
// Focus
Expand Down Expand Up @@ -965,8 +966,8 @@ class QuillRawEditorState extends EditorState
assert(!hasConnection);
_selectionOverlay?.dispose();
_selectionOverlay = null;
controller.removeListener(_didChangeTextEditingValueListener);
if (!widget.config.readOnly) {
controller.removeListener(_didChangeTextEditingValueListener);
widget.config.focusNode.removeListener(_handleFocusChanged);
composingRange.removeListener(_onComposingRangeChanged);
}
Expand Down Expand Up @@ -1081,6 +1082,7 @@ class QuillRawEditorState extends EditorState
contextMenuBuilder: widget.config.contextMenuBuilder == null
? null
: (context) => widget.config.contextMenuBuilder!(context, this),
dragOffsetNotifier: widget.dragOffsetNotifier,
);
_selectionOverlay!.handlesVisible = _shouldShowSelectionHandles();
_selectionOverlay!.showHandles();
Expand Down
5 changes: 5 additions & 0 deletions lib/src/editor/widgets/delegate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import '../../document/attribute.dart';
import '../../document/nodes/leaf.dart';
import '../editor.dart';
import '../raw_editor/raw_editor.dart';
import 'text/magnifier.dart';
import 'text/text_selection.dart';

typedef CustomStyleBuilder = TextStyle Function(Attribute attribute);
Expand Down Expand Up @@ -361,6 +362,8 @@ class EditorTextSelectionGestureDetectorBuilder {
required Widget child,
Key? key,
bool detectWordBoundary = true,
ValueNotifier<Offset?>? dragOffsetNotifier,
QuillMagnifierBuilder? quillMagnifierBuilder,
}) {
return EditorTextSelectionGestureDetector(
key: key,
Expand All @@ -379,6 +382,8 @@ class EditorTextSelectionGestureDetectorBuilder {
onDragSelectionEnd: onDragSelectionEnd,
behavior: behavior,
detectWordBoundary: detectWordBoundary,
dragOffsetNotifier: dragOffsetNotifier,
quillMagnifierBuilder: quillMagnifierBuilder,
child: child,
);
}
Expand Down
43 changes: 43 additions & 0 deletions lib/src/editor/widgets/text/magnifier.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';

typedef QuillMagnifierBuilder = Widget Function(Offset dragPosition);

Widget defaultQuillMagnifierBuilder(Offset dragPosition) =>
QuillMagnifier(dragPosition: dragPosition);

class QuillMagnifier extends StatelessWidget {
const QuillMagnifier({required this.dragPosition, super.key});

final Offset dragPosition;

@override
Widget build(BuildContext context) {
final position = dragPosition.translate(-60, -80);
return Positioned(
top: position.dy,
left: position.dx,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
),
child: RawMagnifier(
clipBehavior: Clip.hardEdge,
decoration: MagnifierDecoration(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
shadows: const [
BoxShadow(
color: Colors.black26,
spreadRadius: 2,
blurRadius: 5,
offset: Offset(3, 3), // changes position of shadow
),
],
),
size: const Size(100, 45),
focalPointOffset: const Offset(5, 55),
magnificationScale: 1.3,
),
),
);
}
}
Loading