Commit aa096b50 authored by xster's avatar xster Committed by GitHub

iOS text selection (#11224)

Extract common text selection overlay logic from Material to Widget and create a Cupertino version of the overlays
parent 3b35c0c9
...@@ -17,5 +17,6 @@ export 'src/cupertino/page.dart'; ...@@ -17,5 +17,6 @@ export 'src/cupertino/page.dart';
export 'src/cupertino/scaffold.dart'; export 'src/cupertino/scaffold.dart';
export 'src/cupertino/slider.dart'; export 'src/cupertino/slider.dart';
export 'src/cupertino/switch.dart'; export 'src/cupertino/switch.dart';
export 'src/cupertino/text_selection.dart';
export 'src/cupertino/thumb_painter.dart'; export 'src/cupertino/thumb_painter.dart';
export 'widgets.dart'; export 'widgets.dart';
...@@ -47,8 +47,9 @@ class CupertinoButton extends StatefulWidget { ...@@ -47,8 +47,9 @@ class CupertinoButton extends StatefulWidget {
this.color, this.color,
this.minSize: 44.0, this.minSize: 44.0,
this.pressedOpacity: 0.1, this.pressedOpacity: 0.1,
this.borderRadius: const BorderRadius.all(const Radius.circular(8.0)),
@required this.onPressed, @required this.onPressed,
}) : assert(pressedOpacity >= 0.0 && pressedOpacity <= 1.0); }) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0));
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
/// ///
...@@ -83,9 +84,15 @@ class CupertinoButton extends StatefulWidget { ...@@ -83,9 +84,15 @@ class CupertinoButton extends StatefulWidget {
/// The opacity that the button will fade to when it is pressed. /// The opacity that the button will fade to when it is pressed.
/// The button will have an opacity of 1.0 when it is not pressed. /// The button will have an opacity of 1.0 when it is not pressed.
/// ///
/// This defaults to 0.1. /// This defaults to 0.1. If null, opacity will not change on pressed if using
/// your own custom effects is desired.
final double pressedOpacity; final double pressedOpacity;
/// The radius of the button's corners when it has a background color.
///
/// Defaults to round corners of 8 logical pixels.
final BorderRadius borderRadius;
/// Whether the button is enabled or disabled. Buttons are disabled by default. To /// Whether the button is enabled or disabled. Buttons are disabled by default. To
/// enable a button, set its [onPressed] property to a non-null value. /// enable a button, set its [onPressed] property to a non-null value.
bool get enabled => onPressed != null; bool get enabled => onPressed != null;
...@@ -112,7 +119,7 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv ...@@ -112,7 +119,7 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
void _setTween() { void _setTween() {
_opacityTween = new Tween<double>( _opacityTween = new Tween<double>(
begin: 1.0, begin: 1.0,
end: widget.pressedOpacity, end: widget.pressedOpacity ?? 1.0,
); );
} }
...@@ -164,10 +171,12 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv ...@@ -164,10 +171,12 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
child: new GestureDetector( child: new GestureDetector(
onTap: widget.onPressed, onTap: widget.onPressed,
child: new ConstrainedBox( child: new ConstrainedBox(
constraints: new BoxConstraints( constraints: widget.minSize == null
minWidth: widget.minSize, ? const BoxConstraints()
minHeight: widget.minSize, : new BoxConstraints(
), minWidth: widget.minSize,
minHeight: widget.minSize,
),
child: new FadeTransition( child: new FadeTransition(
opacity: _opacityTween.animate(new CurvedAnimation( opacity: _opacityTween.animate(new CurvedAnimation(
parent: _animationController, parent: _animationController,
...@@ -175,17 +184,15 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv ...@@ -175,17 +184,15 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
)), )),
child: new DecoratedBox( child: new DecoratedBox(
decoration: new BoxDecoration( decoration: new BoxDecoration(
borderRadius: const BorderRadius.all(const Radius.circular(8.0)), borderRadius: widget.borderRadius,
color: backgroundColor != null && !enabled color: backgroundColor != null && !enabled
? _kDisabledBackground ? _kDisabledBackground
: backgroundColor, : backgroundColor,
), ),
child: new Padding( child: new Padding(
padding: widget.padding != null padding: widget.padding ?? (backgroundColor != null
? widget.padding ? _kBackgroundButtonPadding
: backgroundColor != null : _kButtonPadding),
? _kBackgroundButtonPadding
: _kButtonPadding,
child: new Center( child: new Center(
widthFactor: 1.0, widthFactor: 1.0,
heightFactor: 1.0, heightFactor: 1.0,
......
This diff is collapsed.
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -166,8 +167,9 @@ class TextField extends StatefulWidget { ...@@ -166,8 +167,9 @@ class TextField extends StatefulWidget {
/// field. /// field.
final ValueChanged<String> onSubmitted; final ValueChanged<String> onSubmitted;
/// Optional input validation and formatting overrides. Formatters are run /// Optional input validation and formatting overrides.
/// in the provided order when the text input changes. ///
/// Formatters are run in the provided order when the text input changes.
final List<TextInputFormatter> inputFormatters; final List<TextInputFormatter> inputFormatters;
@override @override
...@@ -257,7 +259,9 @@ class _TextFieldState extends State<TextField> { ...@@ -257,7 +259,9 @@ class _TextFieldState extends State<TextField> {
maxLines: widget.maxLines, maxLines: widget.maxLines,
cursorColor: themeData.textSelectionColor, cursorColor: themeData.textSelectionColor,
selectionColor: themeData.textSelectionColor, selectionColor: themeData.textSelectionColor,
selectionControls: materialTextSelectionControls, selectionControls: themeData.platform == TargetPlatform.iOS
? cupertinoTextSelectionControls
: materialTextSelectionControls,
onChanged: widget.onChanged, onChanged: widget.onChanged,
onSubmitted: widget.onSubmitted, onSubmitted: widget.onSubmitted,
onSelectionChanged: (TextSelection _, bool longPress) => _onSelectionChanged(context, longPress), onSelectionChanged: (TextSelection _, bool longPress) => _onSelectionChanged(context, longPress),
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -13,32 +12,47 @@ import 'flat_button.dart'; ...@@ -13,32 +12,47 @@ import 'flat_button.dart';
import 'material.dart'; import 'material.dart';
import 'theme.dart'; import 'theme.dart';
const double _kHandleSize = 22.0; // pixels const double _kHandleSize = 22.0;
const double _kToolbarScreenPadding = 8.0; // pixels // Minimal padding from all edges of the selection toolbar to all edges of the
// viewport.
const double _kToolbarScreenPadding = 8.0;
/// Manages a copy/paste text selection toolbar. /// Manages a copy/paste text selection toolbar.
class _TextSelectionToolbar extends StatelessWidget { class _TextSelectionToolbar extends StatelessWidget {
const _TextSelectionToolbar(this.delegate, {Key key}) : super(key: key); const _TextSelectionToolbar({
Key key,
this.delegate,
this.handleCut,
this.handleCopy,
this.handlePaste,
this.handleSelectAll,
}) : super(key: key);
final TextSelectionDelegate delegate; final TextSelectionDelegate delegate;
TextEditingValue get value => delegate.textEditingValue; TextEditingValue get value => delegate.textEditingValue;
final VoidCallback handleCut;
final VoidCallback handleCopy;
final VoidCallback handlePaste;
final VoidCallback handleSelectAll;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<Widget> items = <Widget>[]; final List<Widget> items = <Widget>[];
if (!value.selection.isCollapsed) { if (!value.selection.isCollapsed) {
items.add(new FlatButton(child: const Text('CUT'), onPressed: _handleCut)); items.add(new FlatButton(child: const Text('CUT'), onPressed: handleCut));
items.add(new FlatButton(child: const Text('COPY'), onPressed: _handleCopy)); items.add(new FlatButton(child: const Text('COPY'), onPressed: handleCopy));
} }
items.add(new FlatButton( items.add(new FlatButton(
child: const Text('PASTE'), child: const Text('PASTE'),
// TODO(mpcomplete): This should probably be grayed-out if there is nothing to paste. // TODO(https://github.com/flutter/flutter/issues/11254):
onPressed: _handlePaste // This should probably be grayed-out if there is nothing to paste.
onPressed: handlePaste,
)); ));
if (value.text.isNotEmpty) { if (value.text.isNotEmpty) {
if (value.selection.isCollapsed) if (value.selection.isCollapsed)
items.add(new FlatButton(child: const Text('SELECT ALL'), onPressed: _handleSelectAll)); items.add(new FlatButton(child: const Text('SELECT ALL'), onPressed: handleSelectAll));
} }
return new Material( return new Material(
...@@ -49,43 +63,6 @@ class _TextSelectionToolbar extends StatelessWidget { ...@@ -49,43 +63,6 @@ class _TextSelectionToolbar extends StatelessWidget {
) )
); );
} }
void _handleCut() {
Clipboard.setData(new ClipboardData(text: value.selection.textInside(value.text)));
delegate.textEditingValue = new TextEditingValue(
text: value.selection.textBefore(value.text) + value.selection.textAfter(value.text),
selection: new TextSelection.collapsed(offset: value.selection.start)
);
delegate.hideToolbar();
}
void _handleCopy() {
Clipboard.setData(new ClipboardData(text: value.selection.textInside(value.text)));
delegate.textEditingValue = new TextEditingValue(
text: value.text,
selection: new TextSelection.collapsed(offset: value.selection.end)
);
delegate.hideToolbar();
}
Future<Null> _handlePaste() async {
final TextEditingValue value = this.value; // Snapshot the input before using `await`.
final ClipboardData data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null) {
delegate.textEditingValue = new TextEditingValue(
text: value.selection.textBefore(value.text) + data.text + value.selection.textAfter(value.text),
selection: new TextSelection.collapsed(offset: value.selection.start + data.text.length)
);
}
delegate.hideToolbar();
}
void _handleSelectAll() {
delegate.textEditingValue = new TextEditingValue(
text: value.text,
selection: new TextSelection(baseOffset: 0, extentOffset: value.text.length)
);
}
} }
/// Centers the toolbar around the given position, ensuring that it remains on /// Centers the toolbar around the given position, ensuring that it remains on
...@@ -172,14 +149,20 @@ class _MaterialTextSelectionControls extends TextSelectionControls { ...@@ -172,14 +149,20 @@ class _MaterialTextSelectionControls extends TextSelectionControls {
globalEditableRegion, globalEditableRegion,
position, position,
), ),
child: new _TextSelectionToolbar(delegate), child: new _TextSelectionToolbar(
delegate: delegate,
handleCut: () => handleCut(delegate),
handleCopy: () => handleCopy(delegate),
handlePaste: () => handlePaste(delegate),
handleSelectAll: () => handleSelectAll(delegate),
),
) )
); );
} }
/// Builder for material-style text selection handles. /// Builder for material-style text selection handles.
@override @override
Widget buildHandle(BuildContext context, TextSelectionHandleType type) { Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textHeight) {
final Widget handle = new SizedBox( final Widget handle = new SizedBox(
width: _kHandleSize, width: _kHandleSize,
height: _kHandleSize, height: _kHandleSize,
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
...@@ -66,9 +68,14 @@ abstract class TextSelectionDelegate { ...@@ -66,9 +68,14 @@ abstract class TextSelectionDelegate {
/// An interface for building the selection UI, to be provided by the /// An interface for building the selection UI, to be provided by the
/// implementor of the toolbar widget. /// implementor of the toolbar widget.
///
/// Override text operations such as [handleCut] if needed.
abstract class TextSelectionControls { abstract class TextSelectionControls {
/// Builds a selection handle of the given type. /// Builds a selection handle of the given type.
Widget buildHandle(BuildContext context, TextSelectionHandleType type); ///
/// The top left corner of this widget is positioned at the bottom of the
/// selection position.
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight);
/// Builds a toolbar near a text selection. /// Builds a toolbar near a text selection.
/// ///
...@@ -77,6 +84,59 @@ abstract class TextSelectionControls { ...@@ -77,6 +84,59 @@ abstract class TextSelectionControls {
/// Returns the size of the selection handle. /// Returns the size of the selection handle.
Size get handleSize; Size get handleSize;
void handleCut(TextSelectionDelegate delegate) {
final TextEditingValue value = delegate.textEditingValue;
Clipboard.setData(new ClipboardData(
text: value.selection.textInside(value.text),
));
delegate.textEditingValue = new TextEditingValue(
text: value.selection.textBefore(value.text)
+ value.selection.textAfter(value.text),
selection: new TextSelection.collapsed(
offset: value.selection.start
),
);
delegate.hideToolbar();
}
void handleCopy(TextSelectionDelegate delegate) {
final TextEditingValue value = delegate.textEditingValue;
Clipboard.setData(new ClipboardData(
text: value.selection.textInside(value.text),
));
delegate.textEditingValue = new TextEditingValue(
text: value.text,
selection: new TextSelection.collapsed(offset: value.selection.end),
);
delegate.hideToolbar();
}
Future<Null> handlePaste(TextSelectionDelegate delegate) async {
final TextEditingValue value = delegate.textEditingValue; // Snapshot the input before using `await`.
final ClipboardData data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null) {
delegate.textEditingValue = new TextEditingValue(
text: value.selection.textBefore(value.text)
+ data.text
+ value.selection.textAfter(value.text),
selection: new TextSelection.collapsed(
offset: value.selection.start + data.text.length
),
);
}
delegate.hideToolbar();
}
void handleSelectAll(TextSelectionDelegate delegate) {
delegate.textEditingValue = new TextEditingValue(
text: delegate.textEditingValue.text,
selection: new TextSelection(
baseOffset: 0,
extentOffset: delegate.textEditingValue.text.length
),
);
}
} }
/// An object that manages a pair of text selection handles. /// An object that manages a pair of text selection handles.
...@@ -416,7 +476,11 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay ...@@ -416,7 +476,11 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay
new Positioned( new Positioned(
left: point.dx, left: point.dx,
top: point.dy, top: point.dy,
child: widget.selectionControls.buildHandle(context, type), child: widget.selectionControls.buildHandle(
context,
type,
widget.renderObject.size.height / widget.renderObject.maxLines,
),
), ),
], ],
), ),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment