Line data Source code
1 : // Copyright 2014 The Flutter Authors.
2 : // Copyright 2021 Suragch.
3 : // All rights reserved.
4 : // Use of this source code is governed by a BSD-style license that can be
5 : // found in the LICENSE file.
6 :
7 : import 'dart:math' as math;
8 :
9 : import 'package:flutter/material.dart' show Theme, TextSelectionTheme, Icons;
10 : import 'package:flutter/rendering.dart';
11 : import 'package:flutter/scheduler.dart';
12 : import 'package:flutter/services.dart' show TextSelectionDelegate;
13 : import 'package:flutter/widgets.dart';
14 :
15 : import 'mongol_text_selection_toolbar.dart';
16 : import 'mongol_text_selection_toolbar_button.dart';
17 :
18 : // https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/material/text_selection.dart
19 : // This file builds the Copy/Paste toolbar that pops up when you long click, etc.
20 : // If you want a different style you can replace this class with another one.
21 : // That's what Flutter does to give a different style for Material, Cupertino
22 : // and others.
23 :
24 : const double _kHandleSize = 22.0;
25 :
26 : // Padding between the toolbar and the anchor.
27 : const double _kToolbarContentDistanceRight = _kHandleSize - 2.0;
28 : const double _kToolbarContentDistance = 8.0;
29 :
30 : /// Mongol styled text selection controls. (Adapted from Android Material version)
31 : ///
32 : /// In order to avoid Mongolian Unicode and font issues, the text editing
33 : /// controls use icons rather than text for the copy/cut/past/select buttons.
34 : class MongolTextSelectionControls extends TextSelectionControls {
35 : /// Returns the size of the handle.
36 1 : @override
37 : Size getHandleSize(double textLineWidth) =>
38 : const Size(_kHandleSize, _kHandleSize);
39 :
40 : /// Builder for Mongol copy/paste text selection toolbar.
41 1 : @override
42 : Widget buildToolbar(
43 : BuildContext context,
44 : Rect globalEditableRegion,
45 : double textLineWidth,
46 : Offset selectionMidpoint,
47 : List<TextSelectionPoint> endpoints,
48 : TextSelectionDelegate delegate,
49 : ClipboardStatusNotifier clipboardStatus,
50 : Offset? lastSecondaryTapDownPosition,
51 : ) {
52 1 : return _TextSelectionControlsToolbar(
53 : globalEditableRegion: globalEditableRegion,
54 : textLineWidth: textLineWidth,
55 : selectionMidpoint: selectionMidpoint,
56 : endpoints: endpoints,
57 : delegate: delegate,
58 : clipboardStatus: clipboardStatus,
59 1 : handleCut: canCut(delegate) ? () => handleCut(delegate) : null,
60 1 : handleCopy: canCopy(delegate)
61 0 : ? () => handleCopy(delegate, clipboardStatus)
62 : : null,
63 1 : handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null,
64 : handleSelectAll:
65 1 : canSelectAll(delegate) ? () => handleSelectAll(delegate) : null,
66 : );
67 : }
68 :
69 : /// Builder for material-style text selection handles.
70 1 : @override
71 : Widget buildHandle(
72 : BuildContext context, TextSelectionHandleType type, double textHeight) {
73 1 : final theme = Theme.of(context);
74 2 : final handleColor = TextSelectionTheme.of(context).selectionHandleColor ??
75 2 : theme.colorScheme.primary;
76 1 : final Widget handle = SizedBox(
77 : width: _kHandleSize,
78 : height: _kHandleSize,
79 1 : child: CustomPaint(
80 1 : painter: _TextSelectionHandlePainter(
81 : color: handleColor,
82 : ),
83 : ),
84 : );
85 :
86 : // [handle] is a circle, with a rectangle in the top left quadrant of that
87 : // circle (an onion pointing to 10:30). We rotate [handle] to point
88 : // down-right, up-left, or left depending on the handle type.
89 : switch (type) {
90 1 : case TextSelectionHandleType.left: // points down-right
91 1 : return Transform.rotate(
92 : angle: math.pi,
93 : child: handle,
94 : );
95 1 : case TextSelectionHandleType.right: // points up-left
96 : return handle;
97 1 : case TextSelectionHandleType.collapsed: // points left
98 1 : return Transform.rotate(
99 2 : angle: -math.pi / 4.0,
100 : child: handle,
101 : );
102 : }
103 : }
104 :
105 : /// Gets anchor for material-style text selection handles.
106 : ///
107 : /// See [TextSelectionControls.getHandleAnchor].
108 1 : @override
109 : Offset getHandleAnchor(TextSelectionHandleType type, double textLineWidth) {
110 : switch (type) {
111 1 : case TextSelectionHandleType.left:
112 : //return const Offset(0, 0);
113 : return const Offset(_kHandleSize, _kHandleSize);
114 1 : case TextSelectionHandleType.right:
115 : return Offset.zero;
116 : default:
117 : return const Offset(-4, _kHandleSize / 2);
118 : }
119 : }
120 :
121 1 : @override
122 : bool canSelectAll(TextSelectionDelegate delegate) {
123 : // Android allows SelectAll when selection is not collapsed, unless
124 : // everything has already been selected.
125 1 : final value = delegate.textEditingValue;
126 1 : return delegate.selectAllEnabled &&
127 2 : value.text.isNotEmpty &&
128 3 : !(value.selection.start == 0 &&
129 5 : value.selection.end == value.text.length);
130 : }
131 : }
132 :
133 : // The label and callback for the available default text selection menu buttons.
134 : class _TextSelectionToolbarItemData {
135 1 : const _TextSelectionToolbarItemData({
136 : required this.icon,
137 : required this.onPressed,
138 : });
139 :
140 : final IconData icon;
141 : final VoidCallback onPressed;
142 : }
143 :
144 : // The highest level toolbar widget, built directly by buildToolbar.
145 : class _TextSelectionControlsToolbar extends StatefulWidget {
146 1 : const _TextSelectionControlsToolbar({
147 : Key? key,
148 : required this.clipboardStatus,
149 : required this.delegate,
150 : required this.endpoints,
151 : required this.globalEditableRegion,
152 : required this.handleCut,
153 : required this.handleCopy,
154 : required this.handlePaste,
155 : required this.handleSelectAll,
156 : required this.selectionMidpoint,
157 : required this.textLineWidth,
158 1 : }) : super(key: key);
159 :
160 : final ClipboardStatusNotifier clipboardStatus;
161 : final TextSelectionDelegate delegate;
162 : final List<TextSelectionPoint> endpoints;
163 : final Rect globalEditableRegion;
164 : final VoidCallback? handleCut;
165 : final VoidCallback? handleCopy;
166 : final VoidCallback? handlePaste;
167 : final VoidCallback? handleSelectAll;
168 : final Offset selectionMidpoint;
169 : final double textLineWidth;
170 :
171 1 : @override
172 : _TextSelectionControlsToolbarState createState() =>
173 1 : _TextSelectionControlsToolbarState();
174 : }
175 :
176 : class _TextSelectionControlsToolbarState
177 : extends State<_TextSelectionControlsToolbar> with TickerProviderStateMixin {
178 0 : void _onChangedClipboardStatus() {
179 0 : setState(() {
180 : // Inform the widget that the value of clipboardStatus has changed.
181 : });
182 : }
183 :
184 1 : @override
185 : void initState() {
186 1 : super.initState();
187 4 : widget.clipboardStatus.addListener(_onChangedClipboardStatus);
188 3 : widget.clipboardStatus.update();
189 : }
190 :
191 0 : @override
192 : void didUpdateWidget(_TextSelectionControlsToolbar oldWidget) {
193 0 : super.didUpdateWidget(oldWidget);
194 0 : if (widget.clipboardStatus != oldWidget.clipboardStatus) {
195 0 : widget.clipboardStatus.addListener(_onChangedClipboardStatus);
196 0 : oldWidget.clipboardStatus.removeListener(_onChangedClipboardStatus);
197 : }
198 0 : widget.clipboardStatus.update();
199 : }
200 :
201 1 : @override
202 : void dispose() {
203 1 : super.dispose();
204 : // When used in an Overlay, it can happen that this is disposed after its
205 : // creator has already disposed _clipboardStatus.
206 3 : if (!widget.clipboardStatus.disposed) {
207 4 : widget.clipboardStatus.removeListener(_onChangedClipboardStatus);
208 : }
209 : }
210 :
211 1 : @override
212 : Widget build(BuildContext context) {
213 : // If there are no buttons to be shown, don't render anything.
214 2 : if (widget.handleCut == null &&
215 2 : widget.handleCopy == null &&
216 2 : widget.handlePaste == null &&
217 2 : widget.handleSelectAll == null) {
218 : return const SizedBox.shrink();
219 : }
220 : // If the paste button is desired, don't render anything until the state of
221 : // the clipboard is known, since it's used to determine if paste is shown.
222 2 : if (widget.handlePaste != null &&
223 4 : widget.clipboardStatus.value == ClipboardStatus.unknown) {
224 : return const SizedBox.shrink();
225 : }
226 :
227 : // Calculate the positioning of the menu. It is placed to the left of the
228 : // selection if there is enough room, or otherwise to the right.
229 3 : final startTextSelectionPoint = widget.endpoints[0];
230 : final endTextSelectionPoint =
231 10 : widget.endpoints.length > 1 ? widget.endpoints[1] : widget.endpoints[0];
232 1 : final anchorLeft = Offset(
233 4 : widget.globalEditableRegion.left +
234 3 : startTextSelectionPoint.point.dx -
235 3 : widget.textLineWidth -
236 : _kToolbarContentDistance,
237 7 : widget.globalEditableRegion.top + widget.selectionMidpoint.dy);
238 1 : final anchorRight = Offset(
239 4 : widget.globalEditableRegion.left +
240 3 : endTextSelectionPoint.point.dx +
241 : _kToolbarContentDistanceRight,
242 7 : widget.globalEditableRegion.top + widget.selectionMidpoint.dy);
243 :
244 : // Determine which buttons will appear so that the order and total number is
245 : // known.
246 : final itemDatas =
247 1 : <_TextSelectionToolbarItemData>[
248 2 : if (widget.handleCut != null)
249 1 : _TextSelectionToolbarItemData(
250 : icon: Icons.cut,
251 2 : onPressed: widget.handleCut!,
252 : ),
253 2 : if (widget.handleCopy != null)
254 1 : _TextSelectionToolbarItemData(
255 : icon: Icons.copy,
256 2 : onPressed: widget.handleCopy!,
257 : ),
258 2 : if (widget.handlePaste != null &&
259 4 : widget.clipboardStatus.value == ClipboardStatus.pasteable)
260 1 : _TextSelectionToolbarItemData(
261 : icon: Icons.paste,
262 2 : onPressed: widget.handlePaste!,
263 : ),
264 2 : if (widget.handleSelectAll != null)
265 1 : _TextSelectionToolbarItemData(
266 : icon: Icons.select_all,
267 2 : onPressed: widget.handleSelectAll!,
268 : ),
269 : ];
270 :
271 : // If there is no option available, build an empty widget.
272 1 : if (itemDatas.isEmpty) {
273 : return const SizedBox(width: 0.0, height: 0.0);
274 : }
275 :
276 1 : return MongolTextSelectionToolbar(
277 : anchorLeft: anchorLeft,
278 : anchorRight: anchorRight,
279 : children: itemDatas
280 1 : .asMap()
281 1 : .entries
282 2 : .map((MapEntry<int, _TextSelectionToolbarItemData> entry) {
283 1 : return MongolTextSelectionToolbarButton(
284 1 : padding: MongolTextSelectionToolbarButton.getPadding(
285 2 : entry.key, itemDatas.length),
286 2 : onPressed: entry.value.onPressed,
287 3 : child: Icon(entry.value.icon),
288 : );
289 1 : }).toList(),
290 : );
291 : }
292 : }
293 :
294 : /// Draws a single text selection handle which points up and to the left.
295 : class _TextSelectionHandlePainter extends CustomPainter {
296 1 : _TextSelectionHandlePainter({required this.color});
297 :
298 : final Color color;
299 :
300 1 : @override
301 : void paint(Canvas canvas, Size size) {
302 3 : final paint = Paint()..color = color;
303 2 : final radius = size.width / 2.0;
304 : final circle =
305 2 : Rect.fromCircle(center: Offset(radius, radius), radius: radius);
306 1 : final point = Rect.fromLTWH(0.0, 0.0, radius, radius);
307 1 : final path = Path()
308 1 : ..addOval(circle)
309 1 : ..addRect(point);
310 1 : canvas.drawPath(path, paint);
311 : }
312 :
313 1 : @override
314 : bool shouldRepaint(_TextSelectionHandlePainter oldPainter) {
315 3 : return color != oldPainter.color;
316 : }
317 : }
318 :
319 : /// Text selection controls that follow the Material Design specification.
320 2 : final TextSelectionControls mongolTextSelectionControls =
321 1 : MongolTextSelectionControls();
|