|
|
|
|
|
|
RenderEditable renderObject = null, |
|
|
|
TextSelectionControls selectionControls = null, |
|
|
|
TextSelectionDelegate selectionDelegate = null, |
|
|
|
DragStartBehavior? dragStartBehavior = null) { |
|
|
|
DragStartBehavior dragStartBehavior = DragStartBehavior.start) { |
|
|
|
D.assert(value != null); |
|
|
|
D.assert(context != null); |
|
|
|
this.context = context; |
|
|
|
|
|
|
this.selectionDelegate = selectionDelegate; |
|
|
|
this._value = value; |
|
|
|
OverlayState overlay = Overlay.of(context); |
|
|
|
D.assert(overlay != null); |
|
|
|
this._handleController = new AnimationController(duration: _fadeDuration, vsync: overlay); |
|
|
|
this._toolbarController = new AnimationController(duration: _fadeDuration, vsync: overlay); |
|
|
|
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."); |
|
|
|
this._toolbarController = new AnimationController(duration: fadeDuration, vsync: overlay); |
|
|
|
this.dragStartBehavior = dragStartBehavior; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
public readonly RenderEditable renderObject; |
|
|
|
public readonly TextSelectionControls selectionControls; |
|
|
|
public readonly TextSelectionDelegate selectionDelegate; |
|
|
|
public readonly DragStartBehavior? dragStartBehavior; |
|
|
|
public readonly DragStartBehavior dragStartBehavior; |
|
|
|
public static TimeSpan _fadeDuration = TimeSpan.FromMilliseconds(150); |
|
|
|
AnimationController _handleController; |
|
|
|
public static readonly TimeSpan fadeDuration = TimeSpan.FromMilliseconds(150); |
|
|
|
|
|
|
|
Animation<float> _handleOpacity { |
|
|
|
get { return this._handleController.view; } |
|
|
|
} |
|
|
|
|
|
|
|
Animation<float> _toolbarOpacity { |
|
|
|
get { return this._toolbarController.view; } |
|
|
|
|
|
|
this._buildHandle(context, _TextSelectionHandlePosition.end)), |
|
|
|
}; |
|
|
|
Overlay.of(this.context, debugRequiredFor: this.debugRequiredFor).insertAll(this._handles); |
|
|
|
this._handleController.forward(from: 0.0f); |
|
|
|
} |
|
|
|
|
|
|
|
public void showToolbar() { |
|
|
|
|
|
|
this._toolbar?.remove(); |
|
|
|
this._toolbar = null; |
|
|
|
|
|
|
|
this._handleController.stop(); |
|
|
|
this._handleController.dispose(); |
|
|
|
this._toolbarController.dispose(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
return new Container(); // hide the second handle when collapsed
|
|
|
|
} |
|
|
|
|
|
|
|
return new FadeTransition( |
|
|
|
opacity: this._handleOpacity, |
|
|
|
child: new _TextSelectionHandleOverlay( |
|
|
|
onSelectionHandleChanged: (TextSelection newSelection) => { |
|
|
|
this._handleSelectionHandleChanged(newSelection, position); |
|
|
|
}, |
|
|
|
onSelectionHandleTapped: this._handleSelectionHandleTapped, |
|
|
|
layerLink: this.layerLink, |
|
|
|
renderObject: this.renderObject, |
|
|
|
selection: this._selection, |
|
|
|
selectionControls: this.selectionControls, |
|
|
|
position: position, |
|
|
|
dragStartBehavior: this.dragStartBehavior ?? DragStartBehavior.down |
|
|
|
) |
|
|
|
return new _TextSelectionHandleOverlay( |
|
|
|
onSelectionHandleChanged: (TextSelection newSelection) => { |
|
|
|
this._handleSelectionHandleChanged(newSelection, position); |
|
|
|
}, |
|
|
|
onSelectionHandleTapped: this._handleSelectionHandleTapped, |
|
|
|
layerLink: this.layerLink, |
|
|
|
renderObject: this.renderObject, |
|
|
|
selection: this._selection, |
|
|
|
selectionControls: this.selectionControls, |
|
|
|
position: position, |
|
|
|
dragStartBehavior: this.dragStartBehavior |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
ValueChanged<TextSelection> onSelectionHandleChanged = null, |
|
|
|
VoidCallback onSelectionHandleTapped = null, |
|
|
|
TextSelectionControls selectionControls = null, |
|
|
|
DragStartBehavior dragStartBehavior = DragStartBehavior.down |
|
|
|
DragStartBehavior dragStartBehavior = DragStartBehavior.start |
|
|
|
) : base(key: key) { |
|
|
|
this.selection = selection; |
|
|
|
this.position = position; |
|
|
|
|
|
|
public override State createState() { |
|
|
|
return new _TextSelectionHandleOverlayState(); |
|
|
|
} |
|
|
|
|
|
|
|
internal ValueListenable<bool> _visibility { |
|
|
|
get { |
|
|
|
switch (this.position) { |
|
|
|
case _TextSelectionHandlePosition.start: |
|
|
|
return this.renderObject.selectionStartInViewport; |
|
|
|
case _TextSelectionHandlePosition.end: |
|
|
|
return this.renderObject.selectionEndInViewport; |
|
|
|
} |
|
|
|
|
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
class _TextSelectionHandleOverlayState : State<_TextSelectionHandleOverlay> { |
|
|
|
class _TextSelectionHandleOverlayState : SingleTickerProviderStateMixin<_TextSelectionHandleOverlay> { |
|
|
|
AnimationController _controller; |
|
|
|
|
|
|
|
Animation<float> _opacity { |
|
|
|
get { return this._controller.view; } |
|
|
|
} |
|
|
|
|
|
|
|
public override void initState() { |
|
|
|
base.initState(); |
|
|
|
this._controller = new AnimationController(duration: TextSelectionOverlay.fadeDuration, vsync: this); |
|
|
|
this._handleVisibilityChanged(); |
|
|
|
this.widget._visibility.addListener(this._handleVisibilityChanged); |
|
|
|
} |
|
|
|
|
|
|
|
void _handleVisibilityChanged() { |
|
|
|
if (this.widget._visibility.value) { |
|
|
|
this._controller.forward(); |
|
|
|
} |
|
|
|
else { |
|
|
|
this._controller.reverse(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public override void didUpdateWidget(StatefulWidget oldWidget) { |
|
|
|
base.didUpdateWidget(oldWidget); |
|
|
|
(oldWidget as _TextSelectionHandleOverlay)._visibility.removeListener(this._handleVisibilityChanged); |
|
|
|
this._handleVisibilityChanged(); |
|
|
|
this.widget._visibility.addListener(this._handleVisibilityChanged); |
|
|
|
} |
|
|
|
|
|
|
|
public override void dispose() { |
|
|
|
this.widget._visibility.removeListener(this._handleVisibilityChanged); |
|
|
|
this._controller.dispose(); |
|
|
|
base.dispose(); |
|
|
|
} |
|
|
|
|
|
|
|
void _handleDragStart(DragStartDetails details) { |
|
|
|
this._dragPosition = details.globalPosition + |
|
|
|
new Offset(0.0f, -this.widget.selectionControls.handleSize.height); |
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
Size viewport = this.widget.renderObject.size; |
|
|
|
point = new Offset( |
|
|
|
point.dx.clamp(0.0f, viewport.width), |
|
|
|
point.dy.clamp(0.0f, viewport.height) |
|
|
|
); |
|
|
|
|
|
|
|
child: new GestureDetector( |
|
|
|
dragStartBehavior: this.widget.dragStartBehavior, |
|
|
|
onPanStart: this._handleDragStart, |
|
|
|
onPanUpdate: this._handleDragUpdate, |
|
|
|
onTap: this._handleTap, |
|
|
|
child: new Stack( |
|
|
|
overflow: Overflow.visible, |
|
|
|
children: new List<Widget>() { |
|
|
|
new Positioned( |
|
|
|
left: point.dx, |
|
|
|
top: point.dy, |
|
|
|
child: this.widget.selectionControls.buildHandle(context, type, |
|
|
|
this.widget.renderObject.preferredLineHeight) |
|
|
|
) |
|
|
|
} |
|
|
|
child: new FadeTransition( |
|
|
|
opacity: this._opacity, |
|
|
|
child: new GestureDetector( |
|
|
|
dragStartBehavior: this.widget.dragStartBehavior, |
|
|
|
onPanStart: this._handleDragStart, |
|
|
|
onPanUpdate: this._handleDragUpdate, |
|
|
|
onTap: this._handleTap, |
|
|
|
child: new Stack( |
|
|
|
overflow: Overflow.visible, |
|
|
|
children: new List<Widget>() { |
|
|
|
new Positioned( |
|
|
|
left: point.dx, |
|
|
|
top: point.dy, |
|
|
|
child: this.widget.selectionControls.buildHandle(context, type, |
|
|
|
this.widget.renderObject.preferredLineHeight) |
|
|
|
) |
|
|
|
} |
|
|
|
) |
|
|
|
) |
|
|
|
) |
|
|
|
); |
|
|
|
|
|
|
GestureTapUpCallback onSingleTapUp = null, |
|
|
|
GestureTapCancelCallback onSingleTapCancel = null, |
|
|
|
GestureLongPressCallback onSingleLongTapStart = null, |
|
|
|
// TODO: GestureLongPressMoveUpdateCallback onSingleLongTapMoveUpdate = null,
|
|
|
|
// TODO: GestureLongPressEndCallback onSingleLongTapEnd = null,
|
|
|
|
GestureTapDownCallback onDoubleTapDown = null, |
|
|
|
GestureDragStartCallback onDragSelectionStart = null, |
|
|
|
DragSelectionUpdateCallback onDragSelectionUpdate = null, |
|
|
|
|
|
|
public readonly GestureTapCancelCallback onSingleTapCancel; |
|
|
|
|
|
|
|
public readonly GestureLongPressCallback onSingleLongTapStart; |
|
|
|
|
|
|
|
// TODO: public readonly GestureLongPressMoveUpdateCallback onSingleLongTapMoveUpdate;
|
|
|
|
|
|
|
|
// TODO: public readonly GestureLongPressEndCallback onSingleLongTapEnd;
|
|
|
|
|
|
|
|
public readonly GestureTapDownCallback onDoubleTapDown; |
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// TODO: void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
|
|
|
|
// }
|
|
|
|
|
|
|
|
// TODO: void _handleLongPressEnd(LongPressEndDetails details) {
|
|
|
|
// }
|
|
|
|
|
|
|
|
void _doubleTapTimeout() { |
|
|
|
this._doubleTapTimer = null; |
|
|
|
this._lastTapOffset = null; |
|
|
|
|
|
|
) |
|
|
|
); |
|
|
|
|
|
|
|
if (this.widget.onSingleLongTapStart != null) { |
|
|
|
if (this.widget.onSingleLongTapStart != null // ||
|
|
|
|
// TODO: this.widget.onSingleLongTapMoveUpdatee != null ||
|
|
|
|
// TODO: this.widget.onSingleLongTapEnd != null
|
|
|
|
) { |
|
|
|
instance => { instance.onLongPress = this._handleLongPressStart; }); |
|
|
|
instance => { |
|
|
|
instance.onLongPress = this._handleLongPressStart; |
|
|
|
// TODO: instance.onLongPressMoveUpdate = _handleLongPressMoveUpdate
|
|
|
|
// TODO: instance.onLongPressEnd = _handleLongPressEnd
|
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
if (this.widget.onDragSelectionStart != null || |
|
|
|
|
|
|
) |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// TODO: if (this.widget.onForcePressStart != null || this.widget.onForcePressEnd != null) {
|
|
|
|
// }
|
|
|
|
|
|
|
|
return new RawGestureDetector( |
|
|
|
gestures: gestures, |
|
|
|