Skip to content

Commit bfd23f1

Browse files
mtallencaEchoEllet
andauthored
Fix selection in readOnly mode, Add magnifier via RawMagnifier widget (singerdmx#2529)
* enable selection handles in read only mode, added magnifier * hide the context menu when dragging selection handles * added entry to change log file * after a long press (from double tap or drag) make sure magnifier is removed * added dragOffsetNotifier explanation * created [QuillMagnifer] and added a [QuillEditorConfig] parameter quillMangifierBuilder, also added a defaultQuillMagnifierBuilder function, when not specified the magnifier will not be used. --------- Co-authored-by: Ellet <[email protected]>
1 parent 5d59de8 commit bfd23f1

File tree

10 files changed

+190
-3
lines changed

10 files changed

+190
-3
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
1111
## [Unreleased]
1212

13+
### Fixed
14+
15+
- Can't select text when `readOnly` is true [#2529](https://github.com/singerdmx/flutter-quill/pull/2529).
16+
17+
### Added
18+
19+
- Display magnifier using `RawMagnifier` widget when dragging on iOS/Android [#2529](https://github.com/singerdmx/flutter-quill/pull/2529).
20+
1321
## [11.2.0] - 2025-03-26
1422

1523
### Added

lib/flutter_quill.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export 'src/editor/style_widgets/style_widgets.dart';
2727
export 'src/editor/widgets/cursor.dart';
2828
export 'src/editor/widgets/default_styles.dart';
2929
export 'src/editor/widgets/link.dart';
30+
export 'src/editor/widgets/text/magnifier.dart';
3031
export 'src/editor/widgets/text/utils/text_block_utils.dart';
3132
export 'src/editor_toolbar_controller_shared/copy_cut_service/copy_cut_service.dart';
3233
export 'src/editor_toolbar_controller_shared/copy_cut_service/copy_cut_service_provider.dart';

lib/src/editor/config/editor_config.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import '../raw_editor/raw_editor.dart';
1414
import '../widgets/default_styles.dart';
1515
import '../widgets/delegate.dart';
1616
import '../widgets/link.dart';
17+
import '../widgets/text/magnifier.dart';
1718
import '../widgets/text/utils/text_block_utils.dart';
1819
import 'search_config.dart';
1920

@@ -56,6 +57,7 @@ class QuillEditorConfig {
5657
this.enableAlwaysIndentOnTab = false,
5758
this.embedBuilders,
5859
this.textSpanBuilder = defaultSpanBuilder,
60+
this.quillMagnifierBuilder,
5961
this.unknownEmbedBuilder,
6062
@experimental this.searchConfig = const QuillSearchConfig(),
6163
this.linkActionPickerDelegate = defaultLinkActionPickerDelegate,
@@ -367,6 +369,14 @@ class QuillEditorConfig {
367369

368370
final TextSpanBuilder textSpanBuilder;
369371

372+
/// To add a magnifier when selecting, specify a builder that returns the magnfier widget
373+
///
374+
/// The default is no magnifier
375+
///
376+
/// There is a provided magnifier [QuillMagnifier] that is available via the function
377+
/// defaultQuillMagnifierBuilder
378+
final QuillMagnifierBuilder? quillMagnifierBuilder;
379+
370380
/// See [search](https://github.com/singerdmx/flutter-quill/blob/master/doc/configurations/search.md)
371381
/// page for docs.
372382
@experimental

lib/src/editor/editor.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,9 @@ class QuillEditorState extends State<QuillEditor>
196196
QuillEditorConfig get configurations => widget.config;
197197
QuillEditorConfig get config => widget.config;
198198

199+
/// {@macro drag_offset_notifier}
200+
final dragOffsetNotifier = isMobileApp ? ValueNotifier<Offset?>(null) : null;
201+
199202
@override
200203
void initState() {
201204
super.initState();
@@ -260,6 +263,7 @@ class QuillEditorState extends State<QuillEditor>
260263
final child = QuillRawEditor(
261264
key: _editorKey,
262265
controller: controller,
266+
dragOffsetNotifier: dragOffsetNotifier,
263267
config: QuillRawEditorConfig(
264268
characterShortcutEvents: widget.config.characterShortcutEvents,
265269
spaceShortcutEvents: widget.config.spaceShortcutEvents,
@@ -305,6 +309,7 @@ class QuillEditorState extends State<QuillEditor>
305309
scrollPhysics: config.scrollPhysics,
306310
embedBuilder: _getEmbedBuilder,
307311
textSpanBuilder: config.textSpanBuilder,
312+
quillMagnifierBuilder: config.quillMagnifierBuilder,
308313
linkActionPickerDelegate: config.linkActionPickerDelegate,
309314
customStyleBuilder: config.customStyleBuilder,
310315
customRecognizerBuilder: config.customRecognizerBuilder,
@@ -330,6 +335,8 @@ class QuillEditorState extends State<QuillEditor>
330335
behavior: HitTestBehavior.translucent,
331336
detectWordBoundary: config.detectWordBoundary,
332337
child: child,
338+
dragOffsetNotifier: dragOffsetNotifier,
339+
quillMagnifierBuilder: config.quillMagnifierBuilder,
333340
)
334341
: child;
335342

lib/src/editor/raw_editor/config/raw_editor_config.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import '../../../editor/widgets/default_styles.dart';
1212
import '../../../editor/widgets/delegate.dart';
1313
import '../../../editor/widgets/link.dart';
1414
import '../../../toolbar/theme/quill_dialog_theme.dart';
15+
import '../../widgets/text/magnifier.dart';
1516
import '../../widgets/text/utils/text_block_utils.dart';
1617
import '../builders/leading_block_builder.dart';
1718
import 'events/events.dart';
@@ -70,6 +71,7 @@ class QuillRawEditorConfig {
7071
this.readOnlyMouseCursor = SystemMouseCursors.text,
7172
this.onPerformAction,
7273
@experimental this.customLeadingBuilder,
74+
this.quillMagnifierBuilder,
7375
});
7476

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

409411
/// Called when a text input action is performed.
410412
final void Function(TextInputAction action)? onPerformAction;
413+
414+
/// Used to build the [QuillMagnifier] when long-pressing/dragging selection
415+
final QuillMagnifierBuilder? quillMagnifierBuilder;
411416
}

lib/src/editor/raw_editor/raw_editor.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class QuillRawEditor extends StatefulWidget {
1111
QuillRawEditor({
1212
required this.config,
1313
required this.controller,
14+
this.dragOffsetNotifier,
1415
super.key,
1516
}) : assert(config.maxHeight == null || config.maxHeight! > 0,
1617
'maxHeight cannot be null'),
@@ -25,6 +26,28 @@ class QuillRawEditor extends StatefulWidget {
2526
final QuillController controller;
2627
final QuillRawEditorConfig config;
2728

29+
/// {@template drag_offset_notifier}
30+
/// dragOffsetNotifier - Only used on iOS and Android
31+
///
32+
/// [QuillRawEditor] contains a gesture detector [EditorTextSelectionGestureDetector]
33+
/// within it's widget tree that includes a [RawMagnifier]. The RawMagnifier needs
34+
/// the current position of selection drag events in order to display the magnifier
35+
/// in the correct location. Setting the position to null will hide the magnifier.
36+
///
37+
/// Initial selection events are posted by [EditorTextSelectionGestureDetector]. Once
38+
/// a selection has been created, dragging the selection handles happens in
39+
/// [EditorTextSelectionOverlay].
40+
///
41+
/// Both [EditorTextSelectionGestureDetector] and [EditorTextSelectionOverlay] will update
42+
/// the value of the dragOffsetNotifier.
43+
///
44+
/// The [EditorTextSelectionGestureDetector] will use the value to display the magnifier in
45+
/// the correct location (or hide the magnifier if null). [EditorTextSelectionOverlay] will
46+
/// use the value of the dragOffsetNotifier to hide the context menu when the magnifier is
47+
/// displayed and show the context menu when dragging is complete.
48+
/// {@endtemplate}
49+
final ValueNotifier<Offset?>? dragOffsetNotifier;
50+
2851
@override
2952
State<StatefulWidget> createState() => QuillRawEditorState();
3053
}

lib/src/editor/raw_editor/raw_editor_state.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -853,8 +853,9 @@ class QuillRawEditorState extends EditorState
853853
});
854854
}
855855

856+
controller.addListener(_didChangeTextEditingValueListener);
857+
856858
if (!widget.config.readOnly) {
857-
controller.addListener(_didChangeTextEditingValueListener);
858859
// listen to composing range changes
859860
composingRange.addListener(_onComposingRangeChanged);
860861
// Focus
@@ -965,8 +966,8 @@ class QuillRawEditorState extends EditorState
965966
assert(!hasConnection);
966967
_selectionOverlay?.dispose();
967968
_selectionOverlay = null;
969+
controller.removeListener(_didChangeTextEditingValueListener);
968970
if (!widget.config.readOnly) {
969-
controller.removeListener(_didChangeTextEditingValueListener);
970971
widget.config.focusNode.removeListener(_handleFocusChanged);
971972
composingRange.removeListener(_onComposingRangeChanged);
972973
}
@@ -1081,6 +1082,7 @@ class QuillRawEditorState extends EditorState
10811082
contextMenuBuilder: widget.config.contextMenuBuilder == null
10821083
? null
10831084
: (context) => widget.config.contextMenuBuilder!(context, this),
1085+
dragOffsetNotifier: widget.dragOffsetNotifier,
10841086
);
10851087
_selectionOverlay!.handlesVisible = _shouldShowSelectionHandles();
10861088
_selectionOverlay!.showHandles();

lib/src/editor/widgets/delegate.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import '../../document/attribute.dart';
88
import '../../document/nodes/leaf.dart';
99
import '../editor.dart';
1010
import '../raw_editor/raw_editor.dart';
11+
import 'text/magnifier.dart';
1112
import 'text/text_selection.dart';
1213

1314
typedef CustomStyleBuilder = TextStyle Function(Attribute attribute);
@@ -361,6 +362,8 @@ class EditorTextSelectionGestureDetectorBuilder {
361362
required Widget child,
362363
Key? key,
363364
bool detectWordBoundary = true,
365+
ValueNotifier<Offset?>? dragOffsetNotifier,
366+
QuillMagnifierBuilder? quillMagnifierBuilder,
364367
}) {
365368
return EditorTextSelectionGestureDetector(
366369
key: key,
@@ -379,6 +382,8 @@ class EditorTextSelectionGestureDetectorBuilder {
379382
onDragSelectionEnd: onDragSelectionEnd,
380383
behavior: behavior,
381384
detectWordBoundary: detectWordBoundary,
385+
dragOffsetNotifier: dragOffsetNotifier,
386+
quillMagnifierBuilder: quillMagnifierBuilder,
382387
child: child,
383388
);
384389
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import 'package:flutter/material.dart';
2+
3+
typedef QuillMagnifierBuilder = Widget Function(Offset dragPosition);
4+
5+
Widget defaultQuillMagnifierBuilder(Offset dragPosition) =>
6+
QuillMagnifier(dragPosition: dragPosition);
7+
8+
class QuillMagnifier extends StatelessWidget {
9+
const QuillMagnifier({required this.dragPosition, super.key});
10+
11+
final Offset dragPosition;
12+
13+
@override
14+
Widget build(BuildContext context) {
15+
final position = dragPosition.translate(-60, -80);
16+
return Positioned(
17+
top: position.dy,
18+
left: position.dx,
19+
child: Container(
20+
decoration: BoxDecoration(
21+
borderRadius: BorderRadius.circular(20),
22+
),
23+
child: RawMagnifier(
24+
clipBehavior: Clip.hardEdge,
25+
decoration: MagnifierDecoration(
26+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
27+
shadows: const [
28+
BoxShadow(
29+
color: Colors.black26,
30+
spreadRadius: 2,
31+
blurRadius: 5,
32+
offset: Offset(3, 3), // changes position of shadow
33+
),
34+
],
35+
),
36+
size: const Size(100, 45),
37+
focalPointOffset: const Offset(5, 55),
38+
magnificationScale: 1.3,
39+
),
40+
),
41+
);
42+
}
43+
}

0 commit comments

Comments
 (0)