|
|
|
|
|
|
using Unity.UIWidgets.async2; |
|
|
|
using Unity.UIWidgets.foundation; |
|
|
|
using Unity.UIWidgets.gestures; |
|
|
|
using Unity.UIWidgets.painting; |
|
|
|
using Unity.UIWidgets.widgets; |
|
|
|
using UnityEngine; |
|
|
|
using Object = System.Object; |
|
|
|
using Rect = Unity.UIWidgets.ui.Rect; |
|
|
|
using TextRange = Unity.UIWidgets.ui.TextRange; |
|
|
|
|
|
|
|
|
|
|
); |
|
|
|
public abstract Size getHandleSize(float textLineHeight); |
|
|
|
|
|
|
|
public abstract Size handleSize { get; } |
|
|
|
public abstract Size handleSize { get; }// [!!!]useless?
|
|
|
|
|
|
|
|
public virtual bool canCut(TextSelectionDelegate _delegate) { |
|
|
|
return _delegate.cutEnabled && !_delegate.textEditingValue.selection.isCollapsed; |
|
|
|
|
|
|
TextEditingValue value = null, |
|
|
|
BuildContext context = null, |
|
|
|
Widget debugRequiredFor = null, |
|
|
|
LayerLink layerLink = null, |
|
|
|
LayerLink toolbarLayerLink = null, |
|
|
|
LayerLink startHandleLayerLink = null, |
|
|
|
LayerLink endHandleLayerLink = null, |
|
|
|
bool handlesVisible = false, |
|
|
|
DragStartBehavior dragStartBehavior = DragStartBehavior.start) { |
|
|
|
DragStartBehavior dragStartBehavior = DragStartBehavior.start, |
|
|
|
VoidCallback onSelectionHandleTapped = null) { |
|
|
|
D.assert(handlesVisible != null); |
|
|
|
_handlesVisible = handlesVisible; |
|
|
|
this.layerLink = layerLink; |
|
|
|
this.toolbarLayerLink = toolbarLayerLink; |
|
|
|
this.startHandleLayerLink = startHandleLayerLink; |
|
|
|
this.endHandleLayerLink = endHandleLayerLink; |
|
|
|
this.onSelectionHandleTapped = onSelectionHandleTapped; |
|
|
|
OverlayState overlay = Overlay.of(context); |
|
|
|
OverlayState overlay = Overlay.of(context, rootOverlay: true); |
|
|
|
D.assert(overlay != null, () => $"No Overlay widget exists above {context}.\n" + |
|
|
|
"Usually the Navigator created by WidgetsApp provides the overlay. Perhaps your " + |
|
|
|
"app content was created above the Navigator with the WidgetsApp builder parameter."); |
|
|
|
|
|
|
|
|
|
|
public readonly BuildContext context; |
|
|
|
public readonly Widget debugRequiredFor; |
|
|
|
public readonly LayerLink layerLink; |
|
|
|
public readonly LayerLink toolbarLayerLink; |
|
|
|
public readonly LayerLink startHandleLayerLink; |
|
|
|
public readonly LayerLink endHandleLayerLink; |
|
|
|
public readonly VoidCallback onSelectionHandleTapped; |
|
|
|
|
|
|
|
public static readonly TimeSpan fadeDuration = TimeSpan.FromMilliseconds(150); |
|
|
|
AnimationController _toolbarController; |
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
TextEditingValue value { |
|
|
|
get { return _value; } |
|
|
|
} |
|
|
|
|
|
|
|
TextEditingValue _value; |
|
|
|
|
|
|
get { return _value.selection; } |
|
|
|
} |
|
|
|
|
|
|
|
bool _handlesVisible = false; |
|
|
|
public bool handlesVisible { |
|
|
|
get { |
|
|
|
return _handlesVisible; |
|
|
|
} |
|
|
|
set { |
|
|
|
D.assert(value != null); |
|
|
|
if (_handlesVisible == value) |
|
|
|
return; |
|
|
|
_handlesVisible = value; |
|
|
|
// If we are in build state, it will be too late to update visibility.
|
|
|
|
// We will need to schedule the build in next frame.
|
|
|
|
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) { |
|
|
|
SchedulerBinding.instance.addPostFrameCallback((TimeSpan timespan) => { |
|
|
|
_markNeedsBuild(); |
|
|
|
}); |
|
|
|
} else { |
|
|
|
_markNeedsBuild(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
public void showHandles() { |
|
|
|
D.assert(_handles == null); |
|
|
|
_handles = new List<OverlayEntry> { |
|
|
|
|
|
|
_buildHandle(context, _TextSelectionHandlePosition.end)), |
|
|
|
}; |
|
|
|
Overlay.of(this.context, debugRequiredFor: debugRequiredFor).insertAll(_handles); |
|
|
|
Overlay.of(this.context, rootOverlay: true, debugRequiredFor: debugRequiredFor).insertAll(_handles); |
|
|
|
public void hideHandles() { |
|
|
|
if (_handles != null) { |
|
|
|
_handles[0].remove(); |
|
|
|
_handles[1].remove(); |
|
|
|
_handles = null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
Overlay.of(context, debugRequiredFor: debugRequiredFor).insert(_toolbar); |
|
|
|
Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor).insert(_toolbar); |
|
|
|
_toolbarController.forward(from: 0.0f); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
public bool handlesAreVisible { |
|
|
|
get { return _handles != null; } |
|
|
|
get { return _handles != null && handlesVisible; } |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_handles[1].remove(); |
|
|
|
_handles = null; |
|
|
|
} |
|
|
|
if (_toolbar != null) { |
|
|
|
hideToolbar(); |
|
|
|
} |
|
|
|
} |
|
|
|
_toolbar?.remove(); |
|
|
|
void hideToolbar() { |
|
|
|
D.assert(_toolbar != null); |
|
|
|
_toolbarController.stop(); |
|
|
|
_toolbar.remove(); |
|
|
|
|
|
|
|
_toolbarController.stop(); |
|
|
|
} |
|
|
|
|
|
|
|
public void dispose() { |
|
|
|
|
|
|
return new Container(); // hide the second handle when collapsed
|
|
|
|
} |
|
|
|
|
|
|
|
return new _TextSelectionHandleOverlay( |
|
|
|
onSelectionHandleChanged: (TextSelection newSelection) => { |
|
|
|
_handleSelectionHandleChanged(newSelection, position); |
|
|
|
}, |
|
|
|
onSelectionHandleTapped: _handleSelectionHandleTapped, |
|
|
|
layerLink: layerLink, |
|
|
|
renderObject: renderObject, |
|
|
|
selection: _selection, |
|
|
|
selectionControls: selectionControls, |
|
|
|
position: position, |
|
|
|
dragStartBehavior: dragStartBehavior |
|
|
|
return new Visibility( |
|
|
|
visible: handlesVisible, |
|
|
|
child: new _TextSelectionHandleOverlay( |
|
|
|
onSelectionHandleChanged: (TextSelection newSelection) => { |
|
|
|
_handleSelectionHandleChanged(newSelection, position); |
|
|
|
}, |
|
|
|
onSelectionHandleTapped: onSelectionHandleTapped, |
|
|
|
startHandleLayerLink: startHandleLayerLink, |
|
|
|
endHandleLayerLink: endHandleLayerLink, |
|
|
|
renderObject: renderObject, |
|
|
|
selection: _selection, |
|
|
|
selectionControls: selectionControls, |
|
|
|
position: position, |
|
|
|
dragStartBehavior: dragStartBehavior |
|
|
|
) |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Find the horizontal midpoint, just above the selected text.
|
|
|
|
List<TextSelectionPoint> endpoints = renderObject.getEndpointsForSelection(_selection); |
|
|
|
Offset midpoint = new Offset( |
|
|
|
(endpoints.Count == 1) ? endpoints[0].point.dx : (endpoints[0].point.dx + endpoints[1].point.dx) / 2.0f, |
|
|
|
endpoints[0].point.dy - renderObject.preferredLineHeight |
|
|
|
); |
|
|
|
bool isMultiline = endpoints.last().point.dy - endpoints.first().point.dy > |
|
|
|
renderObject.preferredLineHeight / 2; |
|
|
|
|
|
|
|
float midX = isMultiline |
|
|
|
? editingRegion.width / 2 |
|
|
|
: (endpoints.first().point.dx + endpoints.last().point.dx) / 2; |
|
|
|
|
|
|
|
Offset midpoint = new Offset( |
|
|
|
midX, |
|
|
|
endpoints[0].point.dy - renderObject.preferredLineHeight |
|
|
|
); |
|
|
|
|
|
|
|
link: layerLink, |
|
|
|
link: toolbarLayerLink, |
|
|
|
child: selectionControls.buildToolbar(context, |
|
|
|
child: selectionControls.buildToolbar( |
|
|
|
context, |
|
|
|
editingRegion, |
|
|
|
renderObject.preferredLineHeight, |
|
|
|
midpoint, |
|
|
|
|
|
|
_value.copyWith(selection: newSelection, composing: TextRange.empty); |
|
|
|
selectionDelegate.bringIntoView(textPosition); |
|
|
|
} |
|
|
|
|
|
|
|
void _handleSelectionHandleTapped() { |
|
|
|
if (_value.selection.isCollapsed) { |
|
|
|
if (_toolbar != null) { |
|
|
|
_toolbar?.remove(); |
|
|
|
_toolbar = null; |
|
|
|
} |
|
|
|
else { |
|
|
|
showToolbar(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
class _TextSelectionHandleOverlay : StatefulWidget { |
|
|
|
|
|
|
_TextSelectionHandlePosition position = _TextSelectionHandlePosition.start, |
|
|
|
LayerLink layerLink = null, |
|
|
|
LayerLink startHandleLayerLink = null, |
|
|
|
LayerLink endHandleLayerLink = null, |
|
|
|
RenderEditable renderObject = null, |
|
|
|
ValueChanged<TextSelection> onSelectionHandleChanged = null, |
|
|
|
VoidCallback onSelectionHandleTapped = null, |
|
|
|
|
|
|
this.selection = selection; |
|
|
|
this.position = position; |
|
|
|
this.layerLink = layerLink; |
|
|
|
this.startHandleLayerLink = startHandleLayerLink; |
|
|
|
this.endHandleLayerLink = endHandleLayerLink; |
|
|
|
this.renderObject = renderObject; |
|
|
|
this.onSelectionHandleChanged = onSelectionHandleChanged; |
|
|
|
this.onSelectionHandleTapped = onSelectionHandleTapped; |
|
|
|
|
|
|
|
|
|
|
public readonly TextSelection selection; |
|
|
|
public readonly _TextSelectionHandlePosition position; |
|
|
|
public readonly LayerLink layerLink; |
|
|
|
public readonly LayerLink startHandleLayerLink; |
|
|
|
public readonly LayerLink endHandleLayerLink; |
|
|
|
public readonly RenderEditable renderObject; |
|
|
|
public readonly ValueChanged<TextSelection> onSelectionHandleChanged; |
|
|
|
public readonly VoidCallback onSelectionHandleTapped; |
|
|
|
|
|
|
|
|
|
|
AnimationController _controller; |
|
|
|
|
|
|
|
const float kMinInteractiveDimension = 48.0f; |
|
|
|
Animation<float> _opacity { |
|
|
|
get { return _controller.view; } |
|
|
|
} |
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
void _handleDragStart(DragStartDetails details) { |
|
|
|
_dragPosition = details.globalPosition + |
|
|
|
new Offset(0.0f, -widget.selectionControls.handleSize.height); |
|
|
|
Size handleSize = widget.selectionControls.getHandleSize( |
|
|
|
widget.renderObject.preferredLineHeight |
|
|
|
); |
|
|
|
_dragPosition = details.globalPosition + new Offset(0.0f, -handleSize.height); |
|
|
|
} |
|
|
|
|
|
|
|
void _handleDragUpdate(DragUpdateDetails details) { |
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
void _handleTap() { |
|
|
|
widget.onSelectionHandleTapped(); |
|
|
|
if (widget.onSelectionHandleTapped != null) |
|
|
|
widget.onSelectionHandleTapped(); |
|
|
|
} |
|
|
|
|
|
|
|
public override Widget build(BuildContext context) { |
|
|
|
|
|
|
|
|
|
|
LayerLink layerLink = null; |
|
|
|
point = endpoints[0].point; |
|
|
|
type = _chooseType(endpoints[0], TextSelectionHandleType.left, TextSelectionHandleType.right); |
|
|
|
layerLink = widget.startHandleLayerLink; |
|
|
|
type = _chooseType( |
|
|
|
widget.renderObject.textDirection, |
|
|
|
TextSelectionHandleType.left, |
|
|
|
TextSelectionHandleType.right |
|
|
|
); |
|
|
|
D.assert(endpoints.Count == 2); |
|
|
|
point = endpoints[1].point; |
|
|
|
type = _chooseType(endpoints[1], TextSelectionHandleType.right, TextSelectionHandleType.left); |
|
|
|
D.assert(!widget.selection.isCollapsed); |
|
|
|
layerLink = widget.endHandleLayerLink; |
|
|
|
type = _chooseType( |
|
|
|
widget.renderObject.textDirection, |
|
|
|
TextSelectionHandleType.right, |
|
|
|
TextSelectionHandleType.left |
|
|
|
); |
|
|
|
Size viewport = widget.renderObject.size; |
|
|
|
point = new Offset( |
|
|
|
point.dx.clamp(0.0f, viewport.width), |
|
|
|
point.dy.clamp(0.0f, viewport.height) |
|
|
|
Offset handleAnchor = widget.selectionControls.getHandleAnchor( |
|
|
|
type, |
|
|
|
widget.renderObject.preferredLineHeight |
|
|
|
); |
|
|
|
Size handleSize = widget.selectionControls.getHandleSize( |
|
|
|
widget.renderObject.preferredLineHeight |
|
|
|
); |
|
|
|
|
|
|
|
Rect handleRect = Rect.fromLTWH( |
|
|
|
-handleAnchor.dx, |
|
|
|
-handleAnchor.dy, |
|
|
|
handleSize.width, |
|
|
|
handleSize.height |
|
|
|
); |
|
|
|
|
|
|
|
Rect interactiveRect = handleRect.expandToInclude( |
|
|
|
Rect.fromCircle(center: handleRect.center, radius: kMinInteractiveDimension/ 2) |
|
|
|
); |
|
|
|
RelativeRect padding = RelativeRect.fromLTRB( |
|
|
|
Mathf.Max((interactiveRect.width - handleRect.width) / 2, 0), |
|
|
|
Mathf.Max((interactiveRect.height - handleRect.height) / 2, 0), |
|
|
|
Mathf.Max((interactiveRect.width - handleRect.width) / 2, 0), |
|
|
|
Mathf.Max((interactiveRect.height - handleRect.height) / 2, 0) |
|
|
|
link: widget.layerLink, |
|
|
|
link: layerLink, |
|
|
|
offset: interactiveRect.topLeft, |
|
|
|
behavior: HitTestBehavior.translucent, |
|
|
|
child: new Stack( |
|
|
|
overflow: Overflow.visible, |
|
|
|
children: new List<Widget>() { |
|
|
|
new Positioned( |
|
|
|
left: point.dx, |
|
|
|
top: point.dy, |
|
|
|
child: widget.selectionControls.buildHandle(context, type, |
|
|
|
widget.renderObject.preferredLineHeight) |
|
|
|
) |
|
|
|
} |
|
|
|
child: new Padding( |
|
|
|
padding: EdgeInsets.only( |
|
|
|
left: padding.left, |
|
|
|
top: padding.top, |
|
|
|
right: padding.right, |
|
|
|
bottom: padding.bottom |
|
|
|
), |
|
|
|
child: widget.selectionControls.buildHandle(context, type, |
|
|
|
widget.renderObject.preferredLineHeight) |
|
|
|
) |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
TextSelectionHandleType _chooseType( |
|
|
|
TextSelectionPoint endpoint, |
|
|
|
TextDirection? textDirection, |
|
|
|
TextSelectionHandleType ltrType, |
|
|
|
TextSelectionHandleType rtlType |
|
|
|
) { |
|
|
|
|
|
|
|
|
|
|
D.assert(endpoint.direction != null); |
|
|
|
switch (endpoint.direction) { |
|
|
|
D.assert(textDirection != null); |
|
|
|
switch (textDirection) { |
|
|
|
case TextDirection.ltr: |
|
|
|
return ltrType; |
|
|
|
case TextDirection.rtl: |
|
|
|
|
|
|
D.assert(() => throw new UIWidgetsError($"invalid endpoint.direction {endpoint.direction}")); |
|
|
|
D.assert(() => throw new UIWidgetsError($"invalid endpoint.direction {textDirection}")); |
|
|
|
return ltrType; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
protected void onSingleLongTapEnd(LongPressEndDetails details) { |
|
|
|
if (shouldShowSelectionToolbar) |
|
|
|
editableText.showToolbar(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
protected void onDoubleTapDown(TapDownDetails details) { |
|
|
|
if (_delegate.selectionEnabled) { |
|
|
|
|
|
|
public override Widget build(BuildContext context) { |
|
|
|
Dictionary<Type, GestureRecognizerFactory> gestures = new Dictionary<Type, GestureRecognizerFactory>(); |
|
|
|
|
|
|
|
gestures.Add(typeof(TapGestureRecognizer), new GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>( |
|
|
|
() => new TapGestureRecognizer(debugOwner: this), |
|
|
|
instance => { |
|
|
|
gestures.Add(typeof(_TransparentTapGestureRecognizer), new GestureRecognizerFactoryWithHandlers<_TransparentTapGestureRecognizer>( |
|
|
|
() => new _TransparentTapGestureRecognizer(debugOwner: this), |
|
|
|
(_TransparentTapGestureRecognizer instance) => { |
|
|
|
instance.onTapDown = _handleTapDown; |
|
|
|
instance.onTapUp = _handleTapUp; |
|
|
|
instance.onTapCancel = _handleTapCancel; |
|
|
|
|
|
|
behavior: widget.behavior, |
|
|
|
child: widget.child |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
public class _TransparentTapGestureRecognizer : TapGestureRecognizer { |
|
|
|
public _TransparentTapGestureRecognizer(Object debugOwner = default) : base(debugOwner: debugOwner) {} |
|
|
|
|
|
|
|
public override void rejectGesture(int pointer) { |
|
|
|
if (state == GestureRecognizerState.ready) { |
|
|
|
acceptGesture(pointer); |
|
|
|
} else { |
|
|
|
base.rejectGesture(pointer); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |