mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-04-20 03:06:59 +08:00
opt: exclude analysis flutter widget (#1745)
This commit is contained in:
committed by
GitHub
parent
27ae296b28
commit
5ee83d902d
462
lib/common/widgets/flutter/custom_layout.dart
Normal file
462
lib/common/widgets/flutter/custom_layout.dart
Normal file
@@ -0,0 +1,462 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// ignore_for_file: uri_does_not_exist_in_doc_import
|
||||
|
||||
/// @docImport 'package:flutter/widgets.dart';
|
||||
///
|
||||
/// @docImport 'stack.dart';
|
||||
library;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class CustomMultiChildLayout extends MultiChildRenderObjectWidget {
|
||||
/// Creates a custom multi-child layout.
|
||||
const CustomMultiChildLayout({
|
||||
super.key,
|
||||
required this.delegate,
|
||||
super.children,
|
||||
});
|
||||
|
||||
/// The delegate that controls the layout of the children.
|
||||
final MultiChildLayoutDelegate delegate;
|
||||
|
||||
@override
|
||||
RenderCustomMultiChildLayoutBox createRenderObject(BuildContext context) {
|
||||
return RenderCustomMultiChildLayoutBox(delegate: delegate);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
RenderCustomMultiChildLayoutBox renderObject,
|
||||
) {
|
||||
renderObject.delegate = delegate;
|
||||
}
|
||||
}
|
||||
|
||||
/// A delegate that controls the layout of multiple children.
|
||||
///
|
||||
/// Used with [CustomMultiChildLayout] (in the widgets library) and
|
||||
/// [RenderCustomMultiChildLayoutBox] (in the rendering library).
|
||||
///
|
||||
/// Delegates must be idempotent. Specifically, if two delegates are equal, then
|
||||
/// they must produce the same layout. To change the layout, replace the
|
||||
/// delegate with a different instance whose [shouldRelayout] returns true when
|
||||
/// given the previous instance.
|
||||
///
|
||||
/// Override [getSize] to control the overall size of the layout. The size of
|
||||
/// the layout cannot depend on layout properties of the children. This was
|
||||
/// a design decision to simplify the delegate implementations: This way,
|
||||
/// the delegate implementations do not have to also handle various intrinsic
|
||||
/// sizing functions if the parent's size depended on the children.
|
||||
/// If you want to build a custom layout where you define the size of that widget
|
||||
/// based on its children, then you will have to create a custom render object.
|
||||
/// See [MultiChildRenderObjectWidget] with [ContainerRenderObjectMixin] and
|
||||
/// [RenderBoxContainerDefaultsMixin] to get started or [RenderStack] for an
|
||||
/// example implementation.
|
||||
///
|
||||
/// Override [performLayout] to size and position the children. An
|
||||
/// implementation of [performLayout] must call [layoutChild] exactly once for
|
||||
/// each child, but it may call [layoutChild] on children in an arbitrary order.
|
||||
/// Typically a delegate will use the size returned from [layoutChild] on one
|
||||
/// child to determine the constraints for [performLayout] on another child or
|
||||
/// to determine the offset for [positionChild] for that child or another child.
|
||||
///
|
||||
/// Override [shouldRelayout] to determine when the layout of the children needs
|
||||
/// to be recomputed when the delegate changes.
|
||||
///
|
||||
/// The most efficient way to trigger a relayout is to supply a `relayout`
|
||||
/// argument to the constructor of the [MultiChildLayoutDelegate]. The custom
|
||||
/// layout will listen to this value and relayout whenever the Listenable
|
||||
/// notifies its listeners, such as when an [Animation] ticks. This allows
|
||||
/// the custom layout to avoid the build phase of the pipeline.
|
||||
///
|
||||
/// Each child must be wrapped in a [LayoutId] widget to assign the id that
|
||||
/// identifies it to the delegate. The [LayoutId.id] needs to be unique among
|
||||
/// the children that the [CustomMultiChildLayout] manages.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
///
|
||||
/// Below is an example implementation of [performLayout] that causes one widget
|
||||
/// (the follower) to be the same size as another (the leader):
|
||||
///
|
||||
/// ```dart
|
||||
/// // Define your own slot numbers, depending upon the id assigned by LayoutId.
|
||||
/// // Typical usage is to define an enum like the one below, and use those
|
||||
/// // values as the ids.
|
||||
/// enum _Slot {
|
||||
/// leader,
|
||||
/// follower,
|
||||
/// }
|
||||
///
|
||||
/// class FollowTheLeader extends MultiChildLayoutDelegate {
|
||||
/// @override
|
||||
/// void performLayout(Size size) {
|
||||
/// Size leaderSize = Size.zero;
|
||||
///
|
||||
/// if (hasChild(_Slot.leader)) {
|
||||
/// leaderSize = layoutChild(_Slot.leader, BoxConstraints.loose(size));
|
||||
/// positionChild(_Slot.leader, Offset.zero);
|
||||
/// }
|
||||
///
|
||||
/// if (hasChild(_Slot.follower)) {
|
||||
/// layoutChild(_Slot.follower, BoxConstraints.tight(leaderSize));
|
||||
/// positionChild(_Slot.follower, Offset(size.width - leaderSize.width,
|
||||
/// size.height - leaderSize.height));
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// @override
|
||||
/// bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
|
||||
/// }
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// The delegate gives the leader widget loose constraints, which means the
|
||||
/// child determines what size to be (subject to fitting within the given size).
|
||||
/// The delegate then remembers the size of that child and places it in the
|
||||
/// upper left corner.
|
||||
///
|
||||
/// The delegate then gives the follower widget tight constraints, forcing it to
|
||||
/// match the size of the leader widget. The delegate then places the follower
|
||||
/// widget in the bottom right corner.
|
||||
///
|
||||
/// The leader and follower widget will paint in the order they appear in the
|
||||
/// child list, regardless of the order in which [layoutChild] is called on
|
||||
/// them.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CustomMultiChildLayout], the widget that uses this delegate.
|
||||
/// * [RenderCustomMultiChildLayoutBox], render object that uses this
|
||||
/// delegate.
|
||||
abstract class MultiChildLayoutDelegate {
|
||||
/// Creates a layout delegate.
|
||||
///
|
||||
/// The layout will update whenever [relayout] notifies its listeners.
|
||||
MultiChildLayoutDelegate({Listenable? relayout}) : _relayout = relayout;
|
||||
|
||||
final Listenable? _relayout;
|
||||
|
||||
Map<Object, RenderBox>? _idToChild;
|
||||
Set<RenderBox>? _debugChildrenNeedingLayout;
|
||||
|
||||
/// True if a non-null LayoutChild was provided for the specified id.
|
||||
///
|
||||
/// Call this from the [performLayout] method to determine which children
|
||||
/// are available, if the child list might vary.
|
||||
///
|
||||
/// This method cannot be called from [getSize] as the size is not allowed
|
||||
/// to depend on the children.
|
||||
bool hasChild(Object childId) => _idToChild![childId] != null;
|
||||
|
||||
/// Ask the child to update its layout within the limits specified by
|
||||
/// the constraints parameter. The child's size is returned.
|
||||
///
|
||||
/// Call this from your [performLayout] function to lay out each
|
||||
/// child. Every child must be laid out using this function exactly
|
||||
/// once each time the [performLayout] function is called.
|
||||
Size layoutChild(Object childId, BoxConstraints constraints) {
|
||||
final RenderBox? child = _idToChild![childId];
|
||||
assert(() {
|
||||
if (child == null) {
|
||||
throw FlutterError(
|
||||
'The $this custom multichild layout delegate tried to lay out a non-existent child.\n'
|
||||
'There is no child with the id "$childId".',
|
||||
);
|
||||
}
|
||||
if (!_debugChildrenNeedingLayout!.remove(child)) {
|
||||
throw FlutterError(
|
||||
'The $this custom multichild layout delegate tried to lay out the child with id "$childId" more than once.\n'
|
||||
'Each child must be laid out exactly once.',
|
||||
);
|
||||
}
|
||||
try {
|
||||
assert(constraints.debugAssertIsValid(isAppliedConstraint: true));
|
||||
} on AssertionError catch (exception) {
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary(
|
||||
'The $this custom multichild layout delegate provided invalid box constraints for the child with id "$childId".',
|
||||
),
|
||||
DiagnosticsProperty<AssertionError>(
|
||||
'Exception',
|
||||
exception,
|
||||
showName: false,
|
||||
),
|
||||
ErrorDescription(
|
||||
'The minimum width and height must be greater than or equal to zero.\n'
|
||||
'The maximum width must be greater than or equal to the minimum width.\n'
|
||||
'The maximum height must be greater than or equal to the minimum height.',
|
||||
),
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
child!.layout(constraints, parentUsesSize: true);
|
||||
return child.size;
|
||||
}
|
||||
|
||||
/// Specify the child's origin relative to this origin.
|
||||
///
|
||||
/// Call this from your [performLayout] function to position each
|
||||
/// child. If you do not call this for a child, its position will
|
||||
/// remain unchanged. Children initially have their position set to
|
||||
/// (0,0), i.e. the top left of the [RenderCustomMultiChildLayoutBox].
|
||||
void positionChild(Object childId, Offset offset) {
|
||||
final RenderBox? child = _idToChild![childId];
|
||||
assert(() {
|
||||
if (child == null) {
|
||||
throw FlutterError(
|
||||
'The $this custom multichild layout delegate tried to position out a non-existent child:\n'
|
||||
'There is no child with the id "$childId".',
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
final MultiChildLayoutParentData childParentData =
|
||||
child!.parentData! as MultiChildLayoutParentData;
|
||||
childParentData.offset = offset;
|
||||
}
|
||||
|
||||
DiagnosticsNode _debugDescribeChild(RenderBox child) {
|
||||
final MultiChildLayoutParentData childParentData =
|
||||
child.parentData! as MultiChildLayoutParentData;
|
||||
return DiagnosticsProperty<RenderBox>('${childParentData.id}', child);
|
||||
}
|
||||
|
||||
void _callPerformLayout(Size size, RenderBox? firstChild) {
|
||||
// A particular layout delegate could be called reentrantly, e.g. if it used
|
||||
// by both a parent and a child. So, we must restore the _idToChild map when
|
||||
// we return.
|
||||
final Map<Object, RenderBox>? previousIdToChild = _idToChild;
|
||||
|
||||
Set<RenderBox>? debugPreviousChildrenNeedingLayout;
|
||||
assert(() {
|
||||
debugPreviousChildrenNeedingLayout = _debugChildrenNeedingLayout;
|
||||
_debugChildrenNeedingLayout = <RenderBox>{};
|
||||
return true;
|
||||
}());
|
||||
|
||||
try {
|
||||
_idToChild = <Object, RenderBox>{};
|
||||
RenderBox? child = firstChild;
|
||||
while (child != null) {
|
||||
final MultiChildLayoutParentData childParentData =
|
||||
child.parentData! as MultiChildLayoutParentData;
|
||||
assert(() {
|
||||
if (childParentData.id == null) {
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary(
|
||||
'Every child of a RenderCustomMultiChildLayoutBox must have an ID in its parent data.',
|
||||
),
|
||||
child!.describeForError('The following child has no ID'),
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
_idToChild![childParentData.id!] = child;
|
||||
assert(() {
|
||||
_debugChildrenNeedingLayout!.add(child!);
|
||||
return true;
|
||||
}());
|
||||
child = childParentData.nextSibling;
|
||||
}
|
||||
performLayout(size);
|
||||
assert(() {
|
||||
if (_debugChildrenNeedingLayout!.isNotEmpty) {
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary('Each child must be laid out exactly once.'),
|
||||
DiagnosticsBlock(
|
||||
name:
|
||||
'The $this custom multichild layout delegate forgot '
|
||||
'to lay out the following '
|
||||
'${_debugChildrenNeedingLayout!.length > 1 ? 'children' : 'child'}',
|
||||
properties: _debugChildrenNeedingLayout!
|
||||
.map<DiagnosticsNode>(_debugDescribeChild)
|
||||
.toList(),
|
||||
),
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
} finally {
|
||||
_idToChild = previousIdToChild;
|
||||
assert(() {
|
||||
_debugChildrenNeedingLayout = debugPreviousChildrenNeedingLayout;
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
}
|
||||
|
||||
/// Override this method to return the size of this object given the
|
||||
/// incoming constraints.
|
||||
///
|
||||
/// The size cannot reflect the sizes of the children. If this layout has a
|
||||
/// fixed width or height the returned size can reflect that; the size will be
|
||||
/// constrained to the given constraints.
|
||||
///
|
||||
/// By default, attempts to size the box to the biggest size
|
||||
/// possible given the constraints.
|
||||
Size getSize(BoxConstraints constraints) => constraints.biggest;
|
||||
|
||||
/// Override this method to lay out and position all children given this
|
||||
/// widget's size.
|
||||
///
|
||||
/// This method must call [layoutChild] for each child. It should also specify
|
||||
/// the final position of each child with [positionChild].
|
||||
void performLayout(Size size);
|
||||
|
||||
/// Override this method to return true when the children need to be
|
||||
/// laid out.
|
||||
///
|
||||
/// This should compare the fields of the current delegate and the given
|
||||
/// `oldDelegate` and return true if the fields are such that the layout would
|
||||
/// be different.
|
||||
bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate);
|
||||
|
||||
/// Override this method to include additional information in the
|
||||
/// debugging data printed by [debugDumpRenderTree] and friends.
|
||||
///
|
||||
/// By default, returns the [runtimeType] of the class.
|
||||
@override
|
||||
String toString() => objectRuntimeType(this, 'MultiChildLayoutDelegate');
|
||||
}
|
||||
|
||||
/// Defers the layout of multiple children to a delegate.
|
||||
///
|
||||
/// The delegate can determine the layout constraints for each child and can
|
||||
/// decide where to position each child. The delegate can also determine the
|
||||
/// size of the parent, but the size of the parent cannot depend on the sizes of
|
||||
/// the children.
|
||||
class RenderCustomMultiChildLayoutBox extends RenderBox
|
||||
with
|
||||
ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
|
||||
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
|
||||
/// Creates a render object that customizes the layout of multiple children.
|
||||
RenderCustomMultiChildLayoutBox({
|
||||
List<RenderBox>? children,
|
||||
required MultiChildLayoutDelegate delegate,
|
||||
}) : _delegate = delegate {
|
||||
addAll(children);
|
||||
}
|
||||
|
||||
@override
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! MultiChildLayoutParentData) {
|
||||
child.parentData = MultiChildLayoutParentData();
|
||||
}
|
||||
}
|
||||
|
||||
/// The delegate that controls the layout of the children.
|
||||
MultiChildLayoutDelegate get delegate => _delegate;
|
||||
MultiChildLayoutDelegate _delegate;
|
||||
set delegate(MultiChildLayoutDelegate newDelegate) {
|
||||
if (_delegate == newDelegate) {
|
||||
return;
|
||||
}
|
||||
final MultiChildLayoutDelegate oldDelegate = _delegate;
|
||||
if (newDelegate.runtimeType != oldDelegate.runtimeType ||
|
||||
newDelegate.shouldRelayout(oldDelegate)) {
|
||||
markNeedsLayout();
|
||||
}
|
||||
_delegate = newDelegate;
|
||||
if (attached) {
|
||||
oldDelegate._relayout?.removeListener(markNeedsLayout);
|
||||
newDelegate._relayout?.addListener(markNeedsLayout);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void attach(PipelineOwner owner) {
|
||||
super.attach(owner);
|
||||
_delegate._relayout?.addListener(markNeedsLayout);
|
||||
}
|
||||
|
||||
@override
|
||||
void detach() {
|
||||
_delegate._relayout?.removeListener(markNeedsLayout);
|
||||
super.detach();
|
||||
}
|
||||
|
||||
Size _getSize(BoxConstraints constraints) {
|
||||
assert(constraints.debugAssertIsValid());
|
||||
return constraints.constrain(_delegate.getSize(constraints));
|
||||
}
|
||||
|
||||
// TODO(ianh): It's a bit dubious to be using the getSize function from the delegate to
|
||||
// figure out the intrinsic dimensions. We really should either not support intrinsics,
|
||||
// or we should expose intrinsic delegate callbacks and throw if they're not implemented.
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
final double width = _getSize(
|
||||
BoxConstraints.tightForFinite(height: height),
|
||||
).width;
|
||||
if (width.isFinite) {
|
||||
return width;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicWidth(double height) {
|
||||
final double width = _getSize(
|
||||
BoxConstraints.tightForFinite(height: height),
|
||||
).width;
|
||||
if (width.isFinite) {
|
||||
return width;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicHeight(double width) {
|
||||
final double height = _getSize(
|
||||
BoxConstraints.tightForFinite(width: width),
|
||||
).height;
|
||||
if (height.isFinite) {
|
||||
return height;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicHeight(double width) {
|
||||
final double height = _getSize(
|
||||
BoxConstraints.tightForFinite(width: width),
|
||||
).height;
|
||||
if (height.isFinite) {
|
||||
return height;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
@protected
|
||||
Size computeDryLayout(covariant BoxConstraints constraints) {
|
||||
return _getSize(constraints);
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
size = _getSize(constraints);
|
||||
delegate._callPerformLayout(size, firstChild);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
defaultPaint(context, offset);
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
|
||||
return defaultHitTestChildren(result, position: position);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isRepaintBoundary => true;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
789
lib/common/widgets/flutter/dyn/button.dart
Normal file
789
lib/common/widgets/flutter/dyn/button.dart
Normal file
@@ -0,0 +1,789 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// ignore_for_file: uri_does_not_exist_in_doc_import
|
||||
|
||||
/// @docImport 'elevated_button_theme.dart';
|
||||
/// @docImport 'menu_anchor.dart';
|
||||
/// @docImport 'text_button_theme.dart';
|
||||
/// @docImport 'text_theme.dart';
|
||||
/// @docImport 'theme.dart';
|
||||
library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/dyn/ink_well.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide InkWell;
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
/// The base [StatefulWidget] class for buttons whose style is defined by a [ButtonStyle] object.
|
||||
///
|
||||
/// Concrete subclasses must override [defaultStyleOf] and [themeStyleOf].
|
||||
///
|
||||
/// See also:
|
||||
/// * [ElevatedButton], a filled button whose material elevates when pressed.
|
||||
/// * [FilledButton], a filled button that doesn't elevate when pressed.
|
||||
/// * [FilledButton.tonal], a filled button variant that uses a secondary fill color.
|
||||
/// * [OutlinedButton], a button with an outlined border and no fill color.
|
||||
/// * [TextButton], a button with no outline or fill color.
|
||||
/// * <https://m3.material.io/components/buttons/overview>, an overview of each of
|
||||
/// the Material Design button types and how they should be used in designs.
|
||||
abstract class ButtonStyleButton extends StatefulWidget {
|
||||
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||
/// const constructors so that they can be used in const expressions.
|
||||
const ButtonStyleButton({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
required this.onLongPress,
|
||||
required this.onHover,
|
||||
required this.onFocusChange,
|
||||
required this.style,
|
||||
required this.focusNode,
|
||||
required this.autofocus,
|
||||
required this.clipBehavior,
|
||||
this.statesController,
|
||||
this.isSemanticButton = true,
|
||||
@Deprecated(
|
||||
'Remove this parameter as it is now ignored. '
|
||||
'Use ButtonStyle.iconAlignment instead. '
|
||||
'This feature was deprecated after v3.28.0-1.0.pre.',
|
||||
)
|
||||
this.iconAlignment,
|
||||
this.tooltip,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
/// Called when the button is tapped or otherwise activated.
|
||||
///
|
||||
/// If this callback and [onLongPress] are null, then the button will be disabled.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [enabled], which is true if the button is enabled.
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
/// Called when the button is long-pressed.
|
||||
///
|
||||
/// If this callback and [onPressed] are null, then the button will be disabled.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [enabled], which is true if the button is enabled.
|
||||
final VoidCallback? onLongPress;
|
||||
|
||||
/// Called when a pointer enters or exits the button response area.
|
||||
///
|
||||
/// The value passed to the callback is true if a pointer has entered this
|
||||
/// part of the material and false if a pointer has exited this part of the
|
||||
/// material.
|
||||
final ValueChanged<bool>? onHover;
|
||||
|
||||
/// Handler called when the focus changes.
|
||||
///
|
||||
/// Called with true if this widget's node gains focus, and false if it loses
|
||||
/// focus.
|
||||
final ValueChanged<bool>? onFocusChange;
|
||||
|
||||
/// Customizes this button's appearance.
|
||||
///
|
||||
/// Non-null properties of this style override the corresponding
|
||||
/// properties in [themeStyleOf] and [defaultStyleOf]. [WidgetStateProperty]s
|
||||
/// that resolve to non-null values will similarly override the corresponding
|
||||
/// [WidgetStateProperty]s in [themeStyleOf] and [defaultStyleOf].
|
||||
///
|
||||
/// Null by default.
|
||||
final ButtonStyle? style;
|
||||
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// Defaults to [Clip.none] unless [ButtonStyle.backgroundBuilder] or
|
||||
/// [ButtonStyle.foregroundBuilder] is specified. In those
|
||||
/// cases the default is [Clip.antiAlias].
|
||||
final Clip? clipBehavior;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
/// {@macro flutter.material.inkwell.statesController}
|
||||
final WidgetStatesController? statesController;
|
||||
|
||||
/// Determine whether this subtree represents a button.
|
||||
///
|
||||
/// If this is null, the screen reader will not announce "button" when this
|
||||
/// is focused. This is useful for [MenuItemButton] and [SubmenuButton] when we
|
||||
/// traverse the menu system.
|
||||
///
|
||||
/// Defaults to true.
|
||||
final bool? isSemanticButton;
|
||||
|
||||
/// {@macro flutter.material.ButtonStyleButton.iconAlignment}
|
||||
@Deprecated(
|
||||
'Remove this parameter as it is now ignored. '
|
||||
'Use ButtonStyle.iconAlignment instead. '
|
||||
'This feature was deprecated after v3.28.0-1.0.pre.',
|
||||
)
|
||||
final IconAlignment? iconAlignment;
|
||||
|
||||
/// Text that describes the action that will occur when the button is pressed or
|
||||
/// hovered over.
|
||||
///
|
||||
/// This text is displayed when the user long-presses or hovers over the button
|
||||
/// in a tooltip. This string is also used for accessibility.
|
||||
///
|
||||
/// If null, the button will not display a tooltip.
|
||||
final String? tooltip;
|
||||
|
||||
/// Typically the button's label.
|
||||
///
|
||||
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||
final Widget? child;
|
||||
|
||||
/// Returns a [ButtonStyle] that's based primarily on the [Theme]'s
|
||||
/// [ThemeData.textTheme] and [ThemeData.colorScheme], but has most values
|
||||
/// filled out (non-null).
|
||||
///
|
||||
/// The returned style can be overridden by the [style] parameter and by the
|
||||
/// style returned by [themeStyleOf] that some button-specific themes like
|
||||
/// [TextButtonTheme] or [ElevatedButtonTheme] override. For example the
|
||||
/// default style of the [TextButton] subclass can be overridden with its
|
||||
/// [TextButton.style] constructor parameter, or with a [TextButtonTheme].
|
||||
///
|
||||
/// Concrete button subclasses should return a [ButtonStyle] with as many
|
||||
/// non-null properties as possible, where all of the non-null
|
||||
/// [WidgetStateProperty] properties resolve to non-null values.
|
||||
///
|
||||
/// ## Properties that can be null
|
||||
///
|
||||
/// Some properties, like [ButtonStyle.fixedSize] would override other values
|
||||
/// in the same [ButtonStyle] if set, so they are allowed to be null. Here is
|
||||
/// a summary of properties that are allowed to be null when returned in the
|
||||
/// [ButtonStyle] returned by this function, an why:
|
||||
///
|
||||
/// - [ButtonStyle.fixedSize] because it would override other values in the
|
||||
/// same [ButtonStyle], like [ButtonStyle.maximumSize].
|
||||
/// - [ButtonStyle.side] because null is a valid value for a button that has
|
||||
/// no side. [OutlinedButton] returns a non-null default for this, however.
|
||||
/// - [ButtonStyle.backgroundBuilder] and [ButtonStyle.foregroundBuilder]
|
||||
/// because they would override the [ButtonStyle.foregroundColor] and
|
||||
/// [ButtonStyle.backgroundColor] of the same [ButtonStyle].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [themeStyleOf], returns the ButtonStyle of this button's component
|
||||
/// theme.
|
||||
@protected
|
||||
ButtonStyle defaultStyleOf(BuildContext context);
|
||||
|
||||
/// Returns the ButtonStyle that belongs to the button's component theme.
|
||||
///
|
||||
/// The returned style can be overridden by the [style] parameter.
|
||||
///
|
||||
/// Concrete button subclasses should return the ButtonStyle for the
|
||||
/// nearest subclass-specific inherited theme, and if no such theme
|
||||
/// exists, then the same value from the overall [Theme].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [defaultStyleOf], Returns the default [ButtonStyle] for this button.
|
||||
@protected
|
||||
ButtonStyle? themeStyleOf(BuildContext context);
|
||||
|
||||
/// Whether the button is enabled or disabled.
|
||||
///
|
||||
/// Buttons are disabled by default. To enable a button, set its [onPressed]
|
||||
/// or [onLongPress] properties to a non-null value.
|
||||
bool get enabled => onPressed != null || onLongPress != null;
|
||||
|
||||
@override
|
||||
State<ButtonStyleButton> createState() => _ButtonStyleState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(
|
||||
FlagProperty('enabled', value: enabled, ifFalse: 'disabled'),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<ButtonStyle>('style', style, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<FocusNode>(
|
||||
'focusNode',
|
||||
focusNode,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns null if [value] is null, otherwise `WidgetStatePropertyAll<T>(value)`.
|
||||
///
|
||||
/// A convenience method for subclasses.
|
||||
static WidgetStateProperty<T>? allOrNull<T>(T? value) =>
|
||||
value == null ? null : WidgetStatePropertyAll<T>(value);
|
||||
|
||||
/// Returns null if [enabled] and [disabled] are null.
|
||||
/// Otherwise, returns a [WidgetStateProperty] that resolves to [disabled]
|
||||
/// when [WidgetState.disabled] is active, and [enabled] otherwise.
|
||||
///
|
||||
/// A convenience method for subclasses.
|
||||
static WidgetStateProperty<Color?>? defaultColor(
|
||||
Color? enabled,
|
||||
Color? disabled,
|
||||
) {
|
||||
if ((enabled ?? disabled) == null) {
|
||||
return null;
|
||||
}
|
||||
return WidgetStateProperty<Color?>.fromMap(<WidgetStatesConstraint, Color?>{
|
||||
WidgetState.disabled: disabled,
|
||||
WidgetState.any: enabled,
|
||||
});
|
||||
}
|
||||
|
||||
/// A convenience method used by subclasses in the framework, that returns an
|
||||
/// interpolated value based on the [fontSizeMultiplier] parameter:
|
||||
///
|
||||
/// * 0 - 1 [geometry1x]
|
||||
/// * 1 - 2 lerp([geometry1x], [geometry2x], [fontSizeMultiplier] - 1)
|
||||
/// * 2 - 3 lerp([geometry2x], [geometry3x], [fontSizeMultiplier] - 2)
|
||||
/// * otherwise [geometry3x]
|
||||
///
|
||||
/// This method is used by the framework for estimating the default paddings to
|
||||
/// use on a button with a text label, when the system text scaling setting
|
||||
/// changes. It's usually supplied with empirical [geometry1x], [geometry2x],
|
||||
/// [geometry3x] values adjusted for different system text scaling values, when
|
||||
/// the unscaled font size is set to 14.0 (the default [TextTheme.labelLarge]
|
||||
/// value).
|
||||
///
|
||||
/// The `fontSizeMultiplier` argument, for historical reasons, is the default
|
||||
/// font size specified in the [ButtonStyle], scaled by the ambient font
|
||||
/// scaler, then divided by 14.0 (the default font size used in buttons).
|
||||
static EdgeInsetsGeometry scaledPadding(
|
||||
EdgeInsetsGeometry geometry1x,
|
||||
EdgeInsetsGeometry geometry2x,
|
||||
EdgeInsetsGeometry geometry3x,
|
||||
double fontSizeMultiplier,
|
||||
) {
|
||||
return switch (fontSizeMultiplier) {
|
||||
<= 1 => geometry1x,
|
||||
< 2 => EdgeInsetsGeometry.lerp(
|
||||
geometry1x,
|
||||
geometry2x,
|
||||
fontSizeMultiplier - 1,
|
||||
)!,
|
||||
< 3 => EdgeInsetsGeometry.lerp(
|
||||
geometry2x,
|
||||
geometry3x,
|
||||
fontSizeMultiplier - 2,
|
||||
)!,
|
||||
_ => geometry3x,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// The base [State] class for buttons whose style is defined by a [ButtonStyle] object.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ButtonStyleButton], the [StatefulWidget] subclass for which this class is the [State].
|
||||
/// * [ElevatedButton], a filled button whose material elevates when pressed.
|
||||
/// * [FilledButton], a filled ButtonStyleButton that doesn't elevate when pressed.
|
||||
/// * [OutlinedButton], similar to [TextButton], but with an outline.
|
||||
/// * [TextButton], a simple button without a shadow.
|
||||
class _ButtonStyleState extends State<ButtonStyleButton>
|
||||
with TickerProviderStateMixin {
|
||||
AnimationController? controller;
|
||||
double? elevation;
|
||||
Color? backgroundColor;
|
||||
WidgetStatesController? internalStatesController;
|
||||
|
||||
void handleStatesControllerChange() {
|
||||
// Force a rebuild to resolve WidgetStateProperty properties
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
WidgetStatesController get statesController =>
|
||||
widget.statesController ?? internalStatesController!;
|
||||
|
||||
void initStatesController() {
|
||||
if (widget.statesController == null) {
|
||||
internalStatesController = WidgetStatesController();
|
||||
}
|
||||
statesController
|
||||
..update(WidgetState.disabled, !widget.enabled)
|
||||
..addListener(handleStatesControllerChange);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initStatesController();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ButtonStyleButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.statesController != oldWidget.statesController) {
|
||||
oldWidget.statesController?.removeListener(handleStatesControllerChange);
|
||||
if (widget.statesController != null) {
|
||||
internalStatesController?.dispose();
|
||||
internalStatesController = null;
|
||||
}
|
||||
initStatesController();
|
||||
}
|
||||
if (widget.enabled != oldWidget.enabled) {
|
||||
statesController.update(WidgetState.disabled, !widget.enabled);
|
||||
if (!widget.enabled) {
|
||||
// The button may have been disabled while a press gesture is currently underway.
|
||||
statesController.update(WidgetState.pressed, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
statesController.removeListener(handleStatesControllerChange);
|
||||
internalStatesController?.dispose();
|
||||
controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final IconThemeData iconTheme = IconTheme.of(context);
|
||||
final ButtonStyle? widgetStyle = widget.style;
|
||||
final ButtonStyle? themeStyle = widget.themeStyleOf(context);
|
||||
final ButtonStyle defaultStyle = widget.defaultStyleOf(context);
|
||||
|
||||
T? effectiveValue<T>(T? Function(ButtonStyle? style) getProperty) {
|
||||
final T? widgetValue = getProperty(widgetStyle);
|
||||
final T? themeValue = getProperty(themeStyle);
|
||||
final T? defaultValue = getProperty(defaultStyle);
|
||||
return widgetValue ?? themeValue ?? defaultValue;
|
||||
}
|
||||
|
||||
T? resolve<T>(
|
||||
WidgetStateProperty<T>? Function(ButtonStyle? style) getProperty,
|
||||
) {
|
||||
return effectiveValue((ButtonStyle? style) {
|
||||
return getProperty(style)?.resolve(statesController.value);
|
||||
});
|
||||
}
|
||||
|
||||
Color? effectiveIconColor() {
|
||||
return widgetStyle?.iconColor?.resolve(statesController.value) ??
|
||||
themeStyle?.iconColor?.resolve(statesController.value) ??
|
||||
widgetStyle?.foregroundColor?.resolve(statesController.value) ??
|
||||
themeStyle?.foregroundColor?.resolve(statesController.value) ??
|
||||
defaultStyle.iconColor?.resolve(statesController.value) ??
|
||||
// Fallback to foregroundColor if iconColor is null.
|
||||
defaultStyle.foregroundColor?.resolve(statesController.value);
|
||||
}
|
||||
|
||||
final double? resolvedElevation = resolve<double?>(
|
||||
(ButtonStyle? style) => style?.elevation,
|
||||
);
|
||||
final TextStyle? resolvedTextStyle = resolve<TextStyle?>(
|
||||
(ButtonStyle? style) => style?.textStyle,
|
||||
);
|
||||
Color? resolvedBackgroundColor = resolve<Color?>(
|
||||
(ButtonStyle? style) => style?.backgroundColor,
|
||||
);
|
||||
final Color? resolvedForegroundColor = resolve<Color?>(
|
||||
(ButtonStyle? style) => style?.foregroundColor,
|
||||
);
|
||||
final Color? resolvedShadowColor = resolve<Color?>(
|
||||
(ButtonStyle? style) => style?.shadowColor,
|
||||
);
|
||||
final Color? resolvedSurfaceTintColor = resolve<Color?>(
|
||||
(ButtonStyle? style) => style?.surfaceTintColor,
|
||||
);
|
||||
final EdgeInsetsGeometry? resolvedPadding = resolve<EdgeInsetsGeometry?>(
|
||||
(ButtonStyle? style) => style?.padding,
|
||||
);
|
||||
final Size? resolvedMinimumSize = resolve<Size?>(
|
||||
(ButtonStyle? style) => style?.minimumSize,
|
||||
);
|
||||
final Size? resolvedFixedSize = resolve<Size?>(
|
||||
(ButtonStyle? style) => style?.fixedSize,
|
||||
);
|
||||
final Size? resolvedMaximumSize = resolve<Size?>(
|
||||
(ButtonStyle? style) => style?.maximumSize,
|
||||
);
|
||||
final Color? resolvedIconColor = effectiveIconColor();
|
||||
final double? resolvedIconSize = resolve<double?>(
|
||||
(ButtonStyle? style) => style?.iconSize,
|
||||
);
|
||||
final BorderSide? resolvedSide = resolve<BorderSide?>(
|
||||
(ButtonStyle? style) => style?.side,
|
||||
);
|
||||
final OutlinedBorder? resolvedShape = resolve<OutlinedBorder?>(
|
||||
(ButtonStyle? style) => style?.shape,
|
||||
);
|
||||
|
||||
final WidgetStateMouseCursor mouseCursor = _MouseCursor(
|
||||
(Set<WidgetState> states) => effectiveValue(
|
||||
(ButtonStyle? style) => style?.mouseCursor?.resolve(states),
|
||||
),
|
||||
);
|
||||
|
||||
final WidgetStateProperty<Color?> overlayColor =
|
||||
WidgetStateProperty.resolveWith<Color?>(
|
||||
(Set<WidgetState> states) => effectiveValue(
|
||||
(ButtonStyle? style) => style?.overlayColor?.resolve(states),
|
||||
),
|
||||
);
|
||||
|
||||
final VisualDensity? resolvedVisualDensity = effectiveValue(
|
||||
(ButtonStyle? style) => style?.visualDensity,
|
||||
);
|
||||
final MaterialTapTargetSize? resolvedTapTargetSize = effectiveValue(
|
||||
(ButtonStyle? style) => style?.tapTargetSize,
|
||||
);
|
||||
final Duration? resolvedAnimationDuration = effectiveValue(
|
||||
(ButtonStyle? style) => style?.animationDuration,
|
||||
);
|
||||
final bool resolvedEnableFeedback =
|
||||
effectiveValue((ButtonStyle? style) => style?.enableFeedback) ?? true;
|
||||
final AlignmentGeometry? resolvedAlignment = effectiveValue(
|
||||
(ButtonStyle? style) => style?.alignment,
|
||||
);
|
||||
final Offset densityAdjustment = resolvedVisualDensity!.baseSizeAdjustment;
|
||||
final InteractiveInkFeatureFactory? resolvedSplashFactory = effectiveValue(
|
||||
(ButtonStyle? style) => style?.splashFactory,
|
||||
);
|
||||
final ButtonLayerBuilder? resolvedBackgroundBuilder = effectiveValue(
|
||||
(ButtonStyle? style) => style?.backgroundBuilder,
|
||||
);
|
||||
final ButtonLayerBuilder? resolvedForegroundBuilder = effectiveValue(
|
||||
(ButtonStyle? style) => style?.foregroundBuilder,
|
||||
);
|
||||
|
||||
final Clip effectiveClipBehavior =
|
||||
widget.clipBehavior ??
|
||||
((resolvedBackgroundBuilder ?? resolvedForegroundBuilder) != null
|
||||
? Clip.antiAlias
|
||||
: Clip.none);
|
||||
|
||||
BoxConstraints effectiveConstraints = resolvedVisualDensity
|
||||
.effectiveConstraints(
|
||||
BoxConstraints(
|
||||
minWidth: resolvedMinimumSize!.width,
|
||||
minHeight: resolvedMinimumSize.height,
|
||||
maxWidth: resolvedMaximumSize!.width,
|
||||
maxHeight: resolvedMaximumSize.height,
|
||||
),
|
||||
);
|
||||
if (resolvedFixedSize != null) {
|
||||
final Size size = effectiveConstraints.constrain(resolvedFixedSize);
|
||||
if (size.width.isFinite) {
|
||||
effectiveConstraints = effectiveConstraints.copyWith(
|
||||
minWidth: size.width,
|
||||
maxWidth: size.width,
|
||||
);
|
||||
}
|
||||
if (size.height.isFinite) {
|
||||
effectiveConstraints = effectiveConstraints.copyWith(
|
||||
minHeight: size.height,
|
||||
maxHeight: size.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Per the Material Design team: don't allow the VisualDensity
|
||||
// adjustment to reduce the width of the left/right padding. If we
|
||||
// did, VisualDensity.compact, the default for desktop/web, would
|
||||
// reduce the horizontal padding to zero.
|
||||
final double dy = densityAdjustment.dy;
|
||||
final double dx = math.max(0, densityAdjustment.dx);
|
||||
final EdgeInsetsGeometry padding = resolvedPadding!
|
||||
.add(EdgeInsets.fromLTRB(dx, dy, dx, dy))
|
||||
.clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity);
|
||||
|
||||
// If an opaque button's background is becoming translucent while its
|
||||
// elevation is changing, change the elevation first. Material implicitly
|
||||
// animates its elevation but not its color. SKIA renders non-zero
|
||||
// elevations as a shadow colored fill behind the Material's background.
|
||||
if (resolvedAnimationDuration! > Duration.zero &&
|
||||
elevation != null &&
|
||||
backgroundColor != null &&
|
||||
elevation != resolvedElevation &&
|
||||
backgroundColor!.value != resolvedBackgroundColor!.value &&
|
||||
backgroundColor!.opacity == 1 &&
|
||||
resolvedBackgroundColor.opacity < 1 &&
|
||||
resolvedElevation == 0) {
|
||||
if (controller?.duration != resolvedAnimationDuration) {
|
||||
controller?.dispose();
|
||||
controller =
|
||||
AnimationController(
|
||||
duration: resolvedAnimationDuration,
|
||||
vsync: this,
|
||||
)..addStatusListener((AnimationStatus status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
setState(() {}); // Rebuild with the final background color.
|
||||
}
|
||||
});
|
||||
}
|
||||
resolvedBackgroundColor =
|
||||
backgroundColor; // Defer changing the background color.
|
||||
controller!.value = 0;
|
||||
controller!.forward();
|
||||
}
|
||||
elevation = resolvedElevation;
|
||||
backgroundColor = resolvedBackgroundColor;
|
||||
|
||||
Widget result = Padding(
|
||||
padding: padding,
|
||||
child: Align(
|
||||
alignment: resolvedAlignment!,
|
||||
widthFactor: 1.0,
|
||||
heightFactor: 1.0,
|
||||
child: resolvedForegroundBuilder != null
|
||||
? resolvedForegroundBuilder(
|
||||
context,
|
||||
statesController.value,
|
||||
widget.child,
|
||||
)
|
||||
: widget.child,
|
||||
),
|
||||
);
|
||||
if (resolvedBackgroundBuilder != null) {
|
||||
result = resolvedBackgroundBuilder(
|
||||
context,
|
||||
statesController.value,
|
||||
result,
|
||||
);
|
||||
}
|
||||
|
||||
result = AnimatedTheme(
|
||||
duration: resolvedAnimationDuration,
|
||||
data: theme.copyWith(
|
||||
iconTheme: iconTheme.merge(
|
||||
IconThemeData(color: resolvedIconColor, size: resolvedIconSize),
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: widget.onPressed,
|
||||
onLongPress: widget.onLongPress,
|
||||
onHover: widget.onHover,
|
||||
mouseCursor: mouseCursor,
|
||||
enableFeedback: resolvedEnableFeedback,
|
||||
focusNode: widget.focusNode,
|
||||
canRequestFocus: widget.enabled,
|
||||
onFocusChange: widget.onFocusChange,
|
||||
autofocus: widget.autofocus,
|
||||
splashFactory: resolvedSplashFactory,
|
||||
overlayColor: overlayColor,
|
||||
highlightColor: Colors.transparent,
|
||||
customBorder: resolvedShape!.copyWith(side: resolvedSide),
|
||||
statesController: statesController,
|
||||
child: result,
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.tooltip != null) {
|
||||
result = Tooltip(message: widget.tooltip, child: result);
|
||||
}
|
||||
|
||||
final Size minSize;
|
||||
switch (resolvedTapTargetSize!) {
|
||||
case MaterialTapTargetSize.padded:
|
||||
minSize = Size(
|
||||
kMinInteractiveDimension + densityAdjustment.dx,
|
||||
kMinInteractiveDimension + densityAdjustment.dy,
|
||||
);
|
||||
assert(minSize.width >= 0.0);
|
||||
assert(minSize.height >= 0.0);
|
||||
case MaterialTapTargetSize.shrinkWrap:
|
||||
minSize = Size.zero;
|
||||
}
|
||||
|
||||
return Semantics(
|
||||
container: true,
|
||||
button: widget.isSemanticButton,
|
||||
enabled: widget.enabled,
|
||||
child: _InputPadding(
|
||||
minSize: minSize,
|
||||
child: ConstrainedBox(
|
||||
constraints: effectiveConstraints,
|
||||
child: Material(
|
||||
elevation: resolvedElevation!,
|
||||
textStyle: resolvedTextStyle?.copyWith(
|
||||
color: resolvedForegroundColor,
|
||||
),
|
||||
shape: resolvedShape.copyWith(side: resolvedSide),
|
||||
color: resolvedBackgroundColor,
|
||||
shadowColor: resolvedShadowColor,
|
||||
surfaceTintColor: resolvedSurfaceTintColor,
|
||||
type: resolvedBackgroundColor == null
|
||||
? MaterialType.transparency
|
||||
: MaterialType.button,
|
||||
animationDuration: resolvedAnimationDuration,
|
||||
clipBehavior: effectiveClipBehavior,
|
||||
borderOnForeground: false,
|
||||
child: result,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MouseCursor extends WidgetStateMouseCursor {
|
||||
const _MouseCursor(this.resolveCallback);
|
||||
|
||||
final WidgetPropertyResolver<MouseCursor?> resolveCallback;
|
||||
|
||||
@override
|
||||
MouseCursor resolve(Set<WidgetState> states) => resolveCallback(states)!;
|
||||
|
||||
@override
|
||||
String get debugDescription => 'ButtonStyleButton_MouseCursor';
|
||||
}
|
||||
|
||||
/// A widget to pad the area around a [ButtonStyleButton]'s inner [Material].
|
||||
///
|
||||
/// Redirect taps that occur in the padded area around the child to the center
|
||||
/// of the child. This increases the size of the button and the button's
|
||||
/// "tap target", but not its material or its ink splashes.
|
||||
class _InputPadding extends SingleChildRenderObjectWidget {
|
||||
const _InputPadding({super.child, required this.minSize});
|
||||
|
||||
final Size minSize;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return _RenderInputPadding(minSize);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context,
|
||||
covariant _RenderInputPadding renderObject,
|
||||
) {
|
||||
renderObject.minSize = minSize;
|
||||
}
|
||||
}
|
||||
|
||||
class _RenderInputPadding extends RenderShiftedBox {
|
||||
_RenderInputPadding(this._minSize, [RenderBox? child]) : super(child);
|
||||
|
||||
Size get minSize => _minSize;
|
||||
Size _minSize;
|
||||
set minSize(Size value) {
|
||||
if (_minSize == value) {
|
||||
return;
|
||||
}
|
||||
_minSize = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
if (child != null) {
|
||||
return math.max(child!.getMinIntrinsicWidth(height), minSize.width);
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicHeight(double width) {
|
||||
if (child != null) {
|
||||
return math.max(child!.getMinIntrinsicHeight(width), minSize.height);
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicWidth(double height) {
|
||||
if (child != null) {
|
||||
return math.max(child!.getMaxIntrinsicWidth(height), minSize.width);
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicHeight(double width) {
|
||||
if (child != null) {
|
||||
return math.max(child!.getMaxIntrinsicHeight(width), minSize.height);
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
Size _computeSize({
|
||||
required BoxConstraints constraints,
|
||||
required ChildLayouter layoutChild,
|
||||
}) {
|
||||
if (child != null) {
|
||||
final Size childSize = layoutChild(child!, constraints);
|
||||
final double height = math.max(childSize.width, minSize.width);
|
||||
final double width = math.max(childSize.height, minSize.height);
|
||||
return constraints.constrain(Size(height, width));
|
||||
}
|
||||
return Size.zero;
|
||||
}
|
||||
|
||||
@override
|
||||
Size computeDryLayout(BoxConstraints constraints) {
|
||||
return _computeSize(
|
||||
constraints: constraints,
|
||||
layoutChild: ChildLayoutHelper.dryLayoutChild,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
double? computeDryBaseline(
|
||||
covariant BoxConstraints constraints,
|
||||
TextBaseline baseline,
|
||||
) {
|
||||
final RenderBox? child = this.child;
|
||||
if (child == null) {
|
||||
return null;
|
||||
}
|
||||
final double? result = child.getDryBaseline(constraints, baseline);
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
final Size childSize = child.getDryLayout(constraints);
|
||||
return result +
|
||||
Alignment.center
|
||||
.alongOffset(getDryLayout(constraints) - childSize as Offset)
|
||||
.dy;
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
size = _computeSize(
|
||||
constraints: constraints,
|
||||
layoutChild: ChildLayoutHelper.layoutChild,
|
||||
);
|
||||
if (child != null) {
|
||||
final BoxParentData childParentData = child!.parentData! as BoxParentData;
|
||||
childParentData.offset = Alignment.center.alongOffset(
|
||||
size - child!.size as Offset,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTest(BoxHitTestResult result, {required Offset position}) {
|
||||
if (super.hitTest(result, position: position)) {
|
||||
return true;
|
||||
}
|
||||
final Offset center = child!.size.center(Offset.zero);
|
||||
return result.addWithRawTransform(
|
||||
transform: MatrixUtils.forceToPoint(center),
|
||||
position: center,
|
||||
hitTest: (BoxHitTestResult result, Offset position) {
|
||||
assert(position == center);
|
||||
return child!.hitTest(result, position: center);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
1418
lib/common/widgets/flutter/dyn/ink_well.dart
Normal file
1418
lib/common/widgets/flutter/dyn/ink_well.dart
Normal file
File diff suppressed because it is too large
Load Diff
676
lib/common/widgets/flutter/dyn/text_button.dart
Normal file
676
lib/common/widgets/flutter/dyn/text_button.dart
Normal file
@@ -0,0 +1,676 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// ignore_for_file: uri_does_not_exist_in_doc_import
|
||||
|
||||
/// @docImport 'elevated_button.dart';
|
||||
/// @docImport 'filled_button.dart';
|
||||
/// @docImport 'material.dart';
|
||||
/// @docImport 'outlined_button.dart';
|
||||
library;
|
||||
|
||||
import 'dart:ui' show lerpDouble;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/dyn/button.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide InkWell, ButtonStyleButton;
|
||||
|
||||
/// A Material Design "Text Button".
|
||||
///
|
||||
/// Use text buttons on toolbars, in dialogs, or inline with other
|
||||
/// content but offset from that content with padding so that the
|
||||
/// button's presence is obvious. Text buttons do not have visible
|
||||
/// borders and must therefore rely on their position relative to
|
||||
/// other content for context. In dialogs and cards, they should be
|
||||
/// grouped together in one of the bottom corners. Avoid using text
|
||||
/// buttons where they would blend in with other content, for example
|
||||
/// in the middle of lists.
|
||||
///
|
||||
/// A text button is a label [child] displayed on a (zero elevation)
|
||||
/// [Material] widget. The label's [Text] and [Icon] widgets are
|
||||
/// displayed in the [style]'s [ButtonStyle.foregroundColor]. The
|
||||
/// button reacts to touches by filling with the [style]'s
|
||||
/// [ButtonStyle.backgroundColor].
|
||||
///
|
||||
/// The text button's default style is defined by [defaultStyleOf].
|
||||
/// The style of this text button can be overridden with its [style]
|
||||
/// parameter. The style of all text buttons in a subtree can be
|
||||
/// overridden with the [TextButtonTheme] and the style of all of the
|
||||
/// text buttons in an app can be overridden with the [Theme]'s
|
||||
/// [ThemeData.textButtonTheme] property.
|
||||
///
|
||||
/// The static [styleFrom] method is a convenient way to create a
|
||||
/// text button [ButtonStyle] from simple values.
|
||||
///
|
||||
/// If the [onPressed] and [onLongPress] callbacks are null, then this
|
||||
/// button will be disabled, it will not react to touch.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample shows various ways to configure TextButtons, from the
|
||||
/// simplest default appearance to versions that don't resemble
|
||||
/// Material Design at all.
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/text_button/text_button.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample demonstrates using the [statesController] parameter to create a button
|
||||
/// that adds support for [WidgetState.selected].
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/text_button/text_button.1.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ElevatedButton], a filled button whose material elevates when pressed.
|
||||
/// * [FilledButton], a filled button that doesn't elevate when pressed.
|
||||
/// * [FilledButton.tonal], a filled button variant that uses a secondary fill color.
|
||||
/// * [OutlinedButton], a button with an outlined border and no fill color.
|
||||
/// * <https://material.io/design/components/buttons.html>
|
||||
/// * <https://m3.material.io/components/buttons>
|
||||
class TextButton extends ButtonStyleButton {
|
||||
/// Create a [TextButton].
|
||||
const TextButton({
|
||||
super.key,
|
||||
required super.onPressed,
|
||||
super.onLongPress,
|
||||
super.onHover,
|
||||
super.onFocusChange,
|
||||
super.style,
|
||||
super.focusNode,
|
||||
super.autofocus = false,
|
||||
super.clipBehavior,
|
||||
super.statesController,
|
||||
super.isSemanticButton,
|
||||
required Widget super.child,
|
||||
});
|
||||
|
||||
/// Create a text button from a pair of widgets that serve as the button's
|
||||
/// [icon] and [label].
|
||||
///
|
||||
/// The icon and label are arranged in a row and padded by 8 logical pixels
|
||||
/// at the ends, with an 8 pixel gap in between.
|
||||
///
|
||||
/// If [icon] is null, will create a [TextButton] instead.
|
||||
///
|
||||
/// {@macro flutter.material.ButtonStyleButton.iconAlignment}
|
||||
///
|
||||
factory TextButton.icon({
|
||||
Key? key,
|
||||
required VoidCallback? onPressed,
|
||||
VoidCallback? onLongPress,
|
||||
ValueChanged<bool>? onHover,
|
||||
ValueChanged<bool>? onFocusChange,
|
||||
ButtonStyle? style,
|
||||
FocusNode? focusNode,
|
||||
bool? autofocus,
|
||||
Clip? clipBehavior,
|
||||
WidgetStatesController? statesController,
|
||||
Widget? icon,
|
||||
required Widget label,
|
||||
IconAlignment? iconAlignment,
|
||||
}) {
|
||||
if (icon == null) {
|
||||
return TextButton(
|
||||
key: key,
|
||||
onPressed: onPressed,
|
||||
onLongPress: onLongPress,
|
||||
onHover: onHover,
|
||||
onFocusChange: onFocusChange,
|
||||
style: style,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus ?? false,
|
||||
clipBehavior: clipBehavior ?? Clip.none,
|
||||
statesController: statesController,
|
||||
child: label,
|
||||
);
|
||||
}
|
||||
return _TextButtonWithIcon(
|
||||
key: key,
|
||||
onPressed: onPressed,
|
||||
onLongPress: onLongPress,
|
||||
onHover: onHover,
|
||||
onFocusChange: onFocusChange,
|
||||
style: style,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus ?? false,
|
||||
clipBehavior: clipBehavior ?? Clip.none,
|
||||
statesController: statesController,
|
||||
icon: icon,
|
||||
label: label,
|
||||
iconAlignment: iconAlignment,
|
||||
);
|
||||
}
|
||||
|
||||
/// A static convenience method that constructs a text button
|
||||
/// [ButtonStyle] given simple values.
|
||||
///
|
||||
/// The [foregroundColor] and [disabledForegroundColor] colors are used
|
||||
/// to create a [WidgetStateProperty] [ButtonStyle.foregroundColor], and
|
||||
/// a derived [ButtonStyle.overlayColor] if [overlayColor] isn't specified.
|
||||
///
|
||||
/// The [backgroundColor] and [disabledBackgroundColor] colors are
|
||||
/// used to create a [WidgetStateProperty] [ButtonStyle.backgroundColor].
|
||||
///
|
||||
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
|
||||
/// parameters are used to construct [ButtonStyle.mouseCursor].
|
||||
///
|
||||
/// The [iconColor], [disabledIconColor] are used to construct
|
||||
/// [ButtonStyle.iconColor] and [iconSize] is used to construct
|
||||
/// [ButtonStyle.iconSize].
|
||||
///
|
||||
/// If [iconColor] is null, the button icon will use [foregroundColor]. If [foregroundColor] is also
|
||||
/// null, the button icon will use the default icon color.
|
||||
///
|
||||
/// If [overlayColor] is specified and its value is [Colors.transparent]
|
||||
/// then the pressed/focused/hovered highlights are effectively defeated.
|
||||
/// Otherwise a [WidgetStateProperty] with the same opacities as the
|
||||
/// default is created.
|
||||
///
|
||||
/// All of the other parameters are either used directly or used to
|
||||
/// create a [WidgetStateProperty] with a single value for all
|
||||
/// states.
|
||||
///
|
||||
/// All parameters default to null. By default this method returns
|
||||
/// a [ButtonStyle] that doesn't override anything.
|
||||
///
|
||||
/// For example, to override the default text and icon colors for a
|
||||
/// [TextButton], as well as its overlay color, with all of the
|
||||
/// standard opacity adjustments for the pressed, focused, and
|
||||
/// hovered states, one could write:
|
||||
///
|
||||
/// ```dart
|
||||
/// TextButton(
|
||||
/// style: TextButton.styleFrom(foregroundColor: Colors.green),
|
||||
/// child: const Text('Give Kate a mix tape'),
|
||||
/// onPressed: () {
|
||||
/// // ...
|
||||
/// },
|
||||
/// ),
|
||||
/// ```
|
||||
static ButtonStyle styleFrom({
|
||||
Color? foregroundColor,
|
||||
Color? backgroundColor,
|
||||
Color? disabledForegroundColor,
|
||||
Color? disabledBackgroundColor,
|
||||
Color? shadowColor,
|
||||
Color? surfaceTintColor,
|
||||
Color? iconColor,
|
||||
double? iconSize,
|
||||
IconAlignment? iconAlignment,
|
||||
Color? disabledIconColor,
|
||||
Color? overlayColor,
|
||||
double? elevation,
|
||||
TextStyle? textStyle,
|
||||
EdgeInsetsGeometry? padding,
|
||||
Size? minimumSize,
|
||||
Size? fixedSize,
|
||||
Size? maximumSize,
|
||||
BorderSide? side,
|
||||
OutlinedBorder? shape,
|
||||
MouseCursor? enabledMouseCursor,
|
||||
MouseCursor? disabledMouseCursor,
|
||||
VisualDensity? visualDensity,
|
||||
MaterialTapTargetSize? tapTargetSize,
|
||||
Duration? animationDuration,
|
||||
bool? enableFeedback,
|
||||
AlignmentGeometry? alignment,
|
||||
InteractiveInkFeatureFactory? splashFactory,
|
||||
ButtonLayerBuilder? backgroundBuilder,
|
||||
ButtonLayerBuilder? foregroundBuilder,
|
||||
}) {
|
||||
final WidgetStateProperty<Color?>? backgroundColorProp = switch ((
|
||||
backgroundColor,
|
||||
disabledBackgroundColor,
|
||||
)) {
|
||||
(_?, null) => WidgetStatePropertyAll<Color?>(backgroundColor),
|
||||
(_, _) => ButtonStyleButton.defaultColor(
|
||||
backgroundColor,
|
||||
disabledBackgroundColor,
|
||||
),
|
||||
};
|
||||
final WidgetStateProperty<Color?>? iconColorProp = switch ((
|
||||
iconColor,
|
||||
disabledIconColor,
|
||||
)) {
|
||||
(_?, null) => WidgetStatePropertyAll<Color?>(iconColor),
|
||||
(_, _) => ButtonStyleButton.defaultColor(iconColor, disabledIconColor),
|
||||
};
|
||||
final WidgetStateProperty<Color?>? overlayColorProp = switch ((
|
||||
foregroundColor,
|
||||
overlayColor,
|
||||
)) {
|
||||
(null, null) => null,
|
||||
(_, Color(a: 0.0)) => WidgetStatePropertyAll<Color?>(overlayColor),
|
||||
(_, final Color color) || (final Color color, _) =>
|
||||
WidgetStateProperty<Color?>.fromMap(<WidgetState, Color?>{
|
||||
WidgetState.pressed: color.withValues(alpha: 0.1),
|
||||
WidgetState.hovered: color.withValues(alpha: 0.08),
|
||||
WidgetState.focused: color.withValues(alpha: 0.1),
|
||||
}),
|
||||
};
|
||||
|
||||
return ButtonStyle(
|
||||
textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle),
|
||||
foregroundColor: ButtonStyleButton.defaultColor(
|
||||
foregroundColor,
|
||||
disabledForegroundColor,
|
||||
),
|
||||
backgroundColor: backgroundColorProp,
|
||||
overlayColor: overlayColorProp,
|
||||
shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
|
||||
surfaceTintColor: ButtonStyleButton.allOrNull<Color>(surfaceTintColor),
|
||||
iconColor: iconColorProp,
|
||||
iconSize: ButtonStyleButton.allOrNull<double>(iconSize),
|
||||
iconAlignment: iconAlignment,
|
||||
elevation: ButtonStyleButton.allOrNull<double>(elevation),
|
||||
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
|
||||
minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
|
||||
fixedSize: ButtonStyleButton.allOrNull<Size>(fixedSize),
|
||||
maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize),
|
||||
side: ButtonStyleButton.allOrNull<BorderSide>(side),
|
||||
shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
|
||||
mouseCursor: WidgetStateProperty<MouseCursor?>.fromMap(
|
||||
<WidgetStatesConstraint, MouseCursor?>{
|
||||
WidgetState.disabled: disabledMouseCursor,
|
||||
WidgetState.any: enabledMouseCursor,
|
||||
},
|
||||
),
|
||||
visualDensity: visualDensity,
|
||||
tapTargetSize: tapTargetSize,
|
||||
animationDuration: animationDuration,
|
||||
enableFeedback: enableFeedback,
|
||||
alignment: alignment,
|
||||
splashFactory: splashFactory,
|
||||
backgroundBuilder: backgroundBuilder,
|
||||
foregroundBuilder: foregroundBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
/// Defines the button's default appearance.
|
||||
///
|
||||
/// {@template flutter.material.text_button.default_style_of}
|
||||
/// The button [child]'s [Text] and [Icon] widgets are rendered with
|
||||
/// the [ButtonStyle]'s foreground color. The button's [InkWell] adds
|
||||
/// the style's overlay color when the button is focused, hovered
|
||||
/// or pressed. The button's background color becomes its [Material]
|
||||
/// color and is transparent by default.
|
||||
///
|
||||
/// All of the [ButtonStyle]'s defaults appear below.
|
||||
///
|
||||
/// In this list "Theme.foo" is shorthand for
|
||||
/// `Theme.of(context).foo`. Color scheme values like
|
||||
/// "onSurface(0.38)" are shorthand for
|
||||
/// `onSurface.withValues(alpha: 0.38)`. [WidgetStateProperty] valued
|
||||
/// properties that are not followed by a sublist have the same
|
||||
/// value for all states, otherwise the values are as specified for
|
||||
/// each state and "others" means all other states.
|
||||
///
|
||||
/// The "default font size" below refers to the font size specified in the
|
||||
/// [defaultStyleOf] method (or 14.0 if unspecified), scaled by the
|
||||
/// `MediaQuery.textScalerOf(context).scale` method. And the names of the
|
||||
/// EdgeInsets constructors and `EdgeInsetsGeometry.lerp` have been abbreviated
|
||||
/// for readability.
|
||||
///
|
||||
/// The color of the [ButtonStyle.textStyle] is not used, the
|
||||
/// [ButtonStyle.foregroundColor] color is used instead.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// ## Material 2 defaults
|
||||
///
|
||||
/// * `textStyle` - Theme.textTheme.button
|
||||
/// * `backgroundColor` - transparent
|
||||
/// * `foregroundColor`
|
||||
/// * disabled - Theme.colorScheme.onSurface(0.38)
|
||||
/// * others - Theme.colorScheme.primary
|
||||
/// * `overlayColor`
|
||||
/// * hovered - Theme.colorScheme.primary(0.08)
|
||||
/// * focused or pressed - Theme.colorScheme.primary(0.12)
|
||||
/// * `shadowColor` - Theme.shadowColor
|
||||
/// * `elevation` - 0
|
||||
/// * `padding`
|
||||
/// * `default font size <= 14` - (horizontal(12), vertical(8))
|
||||
/// * `14 < default font size <= 28` - lerp(all(8), horizontal(8))
|
||||
/// * `28 < default font size <= 36` - lerp(horizontal(8), horizontal(4))
|
||||
/// * `36 < default font size` - horizontal(4)
|
||||
/// * `minimumSize` - Size(64, 36)
|
||||
/// * `fixedSize` - null
|
||||
/// * `maximumSize` - Size.infinite
|
||||
/// * `side` - null
|
||||
/// * `shape` - RoundedRectangleBorder(borderRadius: BorderRadius.circular(4))
|
||||
/// * `mouseCursor`
|
||||
/// * disabled - SystemMouseCursors.basic
|
||||
/// * others - SystemMouseCursors.click
|
||||
/// * `visualDensity` - theme.visualDensity
|
||||
/// * `tapTargetSize` - theme.materialTapTargetSize
|
||||
/// * `animationDuration` - kThemeChangeDuration
|
||||
/// * `enableFeedback` - true
|
||||
/// * `alignment` - Alignment.center
|
||||
/// * `splashFactory` - InkRipple.splashFactory
|
||||
///
|
||||
/// The default padding values for the [TextButton.icon] factory are slightly different:
|
||||
///
|
||||
/// * `padding`
|
||||
/// * `default font size <= 14` - all(8)
|
||||
/// * `14 < default font size <= 28 `- lerp(all(8), horizontal(4))
|
||||
/// * `28 < default font size` - horizontal(4)
|
||||
///
|
||||
/// The default value for `side`, which defines the appearance of the button's
|
||||
/// outline, is null. That means that the outline is defined by the button
|
||||
/// shape's [OutlinedBorder.side]. Typically the default value of an
|
||||
/// [OutlinedBorder]'s side is [BorderSide.none], so an outline is not drawn.
|
||||
///
|
||||
/// ## Material 3 defaults
|
||||
///
|
||||
/// If [ThemeData.useMaterial3] is set to true the following defaults will
|
||||
/// be used:
|
||||
///
|
||||
/// {@template flutter.material.text_button.material3_defaults}
|
||||
/// * `textStyle` - Theme.textTheme.labelLarge
|
||||
/// * `backgroundColor` - transparent
|
||||
/// * `foregroundColor`
|
||||
/// * disabled - Theme.colorScheme.onSurface(0.38)
|
||||
/// * others - Theme.colorScheme.primary
|
||||
/// * `overlayColor`
|
||||
/// * hovered - Theme.colorScheme.primary(0.08)
|
||||
/// * focused or pressed - Theme.colorScheme.primary(0.1)
|
||||
/// * others - null
|
||||
/// * `shadowColor` - Colors.transparent,
|
||||
/// * `surfaceTintColor` - null
|
||||
/// * `elevation` - 0
|
||||
/// * `padding`
|
||||
/// * `default font size <= 14` - lerp(horizontal(12), horizontal(4))
|
||||
/// * `14 < default font size <= 28` - lerp(all(8), horizontal(8))
|
||||
/// * `28 < default font size <= 36` - lerp(horizontal(8), horizontal(4))
|
||||
/// * `36 < default font size` - horizontal(4)
|
||||
/// * `minimumSize` - Size(64, 40)
|
||||
/// * `fixedSize` - null
|
||||
/// * `maximumSize` - Size.infinite
|
||||
/// * `side` - null
|
||||
/// * `shape` - StadiumBorder()
|
||||
/// * `mouseCursor`
|
||||
/// * disabled - SystemMouseCursors.basic
|
||||
/// * others - SystemMouseCursors.click
|
||||
/// * `visualDensity` - theme.visualDensity
|
||||
/// * `tapTargetSize` - theme.materialTapTargetSize
|
||||
/// * `animationDuration` - kThemeChangeDuration
|
||||
/// * `enableFeedback` - true
|
||||
/// * `alignment` - Alignment.center
|
||||
/// * `splashFactory` - Theme.splashFactory
|
||||
///
|
||||
/// For the [TextButton.icon] factory, the end (generally the right) value of
|
||||
/// `padding` is increased from 12 to 16.
|
||||
/// {@endtemplate}
|
||||
@override
|
||||
ButtonStyle defaultStyleOf(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final ColorScheme colorScheme = theme.colorScheme;
|
||||
|
||||
return Theme.of(context).useMaterial3
|
||||
? _TextButtonDefaultsM3(context)
|
||||
: styleFrom(
|
||||
foregroundColor: colorScheme.primary,
|
||||
disabledForegroundColor: colorScheme.onSurface.withValues(
|
||||
alpha: 0.38,
|
||||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
disabledBackgroundColor: Colors.transparent,
|
||||
shadowColor: theme.shadowColor,
|
||||
elevation: 0,
|
||||
textStyle: theme.textTheme.labelLarge,
|
||||
padding: _scaledPadding(context),
|
||||
minimumSize: const Size(64, 36),
|
||||
maximumSize: Size.infinite,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
enabledMouseCursor: SystemMouseCursors.click,
|
||||
disabledMouseCursor: SystemMouseCursors.basic,
|
||||
visualDensity: theme.visualDensity,
|
||||
tapTargetSize: theme.materialTapTargetSize,
|
||||
animationDuration: kThemeChangeDuration,
|
||||
enableFeedback: true,
|
||||
alignment: Alignment.center,
|
||||
splashFactory: InkRipple.splashFactory,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the [TextButtonThemeData.style] of the closest
|
||||
/// [TextButtonTheme] ancestor.
|
||||
@override
|
||||
ButtonStyle? themeStyleOf(BuildContext context) {
|
||||
return TextButtonTheme.of(context).style;
|
||||
}
|
||||
}
|
||||
|
||||
EdgeInsetsGeometry _scaledPadding(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final double defaultFontSize = theme.textTheme.labelLarge?.fontSize ?? 14.0;
|
||||
final double effectiveTextScale =
|
||||
MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0;
|
||||
return ButtonStyleButton.scaledPadding(
|
||||
theme.useMaterial3
|
||||
? const EdgeInsets.symmetric(horizontal: 12, vertical: 8)
|
||||
: const EdgeInsets.all(8),
|
||||
const EdgeInsets.symmetric(horizontal: 8),
|
||||
const EdgeInsets.symmetric(horizontal: 4),
|
||||
effectiveTextScale,
|
||||
);
|
||||
}
|
||||
|
||||
class _TextButtonWithIcon extends TextButton {
|
||||
_TextButtonWithIcon({
|
||||
super.key,
|
||||
required super.onPressed,
|
||||
super.onLongPress,
|
||||
super.onHover,
|
||||
super.onFocusChange,
|
||||
super.style,
|
||||
super.focusNode,
|
||||
bool? autofocus,
|
||||
super.clipBehavior,
|
||||
super.statesController,
|
||||
required Widget icon,
|
||||
required Widget label,
|
||||
IconAlignment? iconAlignment,
|
||||
}) : super(
|
||||
autofocus: autofocus ?? false,
|
||||
child: _TextButtonWithIconChild(
|
||||
icon: icon,
|
||||
label: label,
|
||||
buttonStyle: style,
|
||||
iconAlignment: iconAlignment,
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
ButtonStyle defaultStyleOf(BuildContext context) {
|
||||
final bool useMaterial3 = Theme.of(context).useMaterial3;
|
||||
final ButtonStyle buttonStyle = super.defaultStyleOf(context);
|
||||
final double defaultFontSize =
|
||||
buttonStyle.textStyle?.resolve(const <WidgetState>{})?.fontSize ?? 14.0;
|
||||
final double effectiveTextScale =
|
||||
MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0;
|
||||
final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
|
||||
useMaterial3
|
||||
? const EdgeInsetsDirectional.fromSTEB(12, 8, 16, 8)
|
||||
: const EdgeInsets.all(8),
|
||||
const EdgeInsets.symmetric(horizontal: 4),
|
||||
const EdgeInsets.symmetric(horizontal: 4),
|
||||
effectiveTextScale,
|
||||
);
|
||||
return buttonStyle.copyWith(
|
||||
padding: WidgetStatePropertyAll<EdgeInsetsGeometry>(scaledPadding),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TextButtonWithIconChild extends StatelessWidget {
|
||||
const _TextButtonWithIconChild({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.buttonStyle,
|
||||
required this.iconAlignment,
|
||||
});
|
||||
|
||||
final Widget label;
|
||||
final Widget icon;
|
||||
final ButtonStyle? buttonStyle;
|
||||
final IconAlignment? iconAlignment;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double defaultFontSize =
|
||||
buttonStyle?.textStyle?.resolve(const <WidgetState>{})?.fontSize ??
|
||||
14.0;
|
||||
final double scale =
|
||||
clampDouble(
|
||||
MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0,
|
||||
1.0,
|
||||
2.0,
|
||||
) -
|
||||
1.0;
|
||||
final TextButtonThemeData textButtonTheme = TextButtonTheme.of(context);
|
||||
final IconAlignment effectiveIconAlignment =
|
||||
iconAlignment ??
|
||||
textButtonTheme.style?.iconAlignment ??
|
||||
buttonStyle?.iconAlignment ??
|
||||
IconAlignment.start;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: lerpDouble(8, 4, scale)!,
|
||||
children: effectiveIconAlignment == IconAlignment.start
|
||||
? <Widget>[icon, Flexible(child: label)]
|
||||
: <Widget>[Flexible(child: label), icon],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// BEGIN GENERATED TOKEN PROPERTIES - TextButton
|
||||
|
||||
// Do not edit by hand. The code between the "BEGIN GENERATED" and
|
||||
// "END GENERATED" comments are generated from data in the Material
|
||||
// Design token database by the script:
|
||||
// dev/tools/gen_defaults/bin/gen_defaults.dart.
|
||||
|
||||
// dart format off
|
||||
class _TextButtonDefaultsM3 extends ButtonStyle {
|
||||
_TextButtonDefaultsM3(this.context)
|
||||
: super(
|
||||
animationDuration: kThemeChangeDuration,
|
||||
enableFeedback: true,
|
||||
alignment: Alignment.center,
|
||||
);
|
||||
|
||||
final BuildContext context;
|
||||
late final ColorScheme _colors = Theme.of(context).colorScheme;
|
||||
|
||||
@override
|
||||
WidgetStateProperty<TextStyle?> get textStyle =>
|
||||
WidgetStatePropertyAll<TextStyle?>(Theme.of(context).textTheme.labelLarge);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color?>? get backgroundColor =>
|
||||
const WidgetStatePropertyAll<Color>(Colors.transparent);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color?>? get foregroundColor =>
|
||||
WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return _colors.onSurface.withValues(alpha: 0.38);
|
||||
}
|
||||
return _colors.primary;
|
||||
});
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color?>? get overlayColor =>
|
||||
WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return _colors.primary.withValues(alpha: 0.1);
|
||||
}
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return _colors.primary.withValues(alpha: 0.08);
|
||||
}
|
||||
if (states.contains(WidgetState.focused)) {
|
||||
return _colors.primary.withValues(alpha: 0.1);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color>? get shadowColor =>
|
||||
const WidgetStatePropertyAll<Color>(Colors.transparent);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color>? get surfaceTintColor =>
|
||||
const WidgetStatePropertyAll<Color>(Colors.transparent);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<double>? get elevation =>
|
||||
const WidgetStatePropertyAll<double>(0.0);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<EdgeInsetsGeometry>? get padding =>
|
||||
WidgetStatePropertyAll<EdgeInsetsGeometry>(_scaledPadding(context));
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Size>? get minimumSize =>
|
||||
const WidgetStatePropertyAll<Size>(Size(64.0, 40.0));
|
||||
|
||||
// No default fixedSize
|
||||
|
||||
@override
|
||||
WidgetStateProperty<double>? get iconSize =>
|
||||
const WidgetStatePropertyAll<double>(18.0);
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Color>? get iconColor {
|
||||
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return _colors.onSurface.withValues(alpha: 0.38);
|
||||
}
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return _colors.primary;
|
||||
}
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return _colors.primary;
|
||||
}
|
||||
if (states.contains(WidgetState.focused)) {
|
||||
return _colors.primary;
|
||||
}
|
||||
return _colors.primary;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
WidgetStateProperty<Size>? get maximumSize =>
|
||||
const WidgetStatePropertyAll<Size>(Size.infinite);
|
||||
|
||||
// No default side
|
||||
|
||||
@override
|
||||
WidgetStateProperty<OutlinedBorder>? get shape =>
|
||||
const WidgetStatePropertyAll<OutlinedBorder>(StadiumBorder());
|
||||
|
||||
@override
|
||||
WidgetStateProperty<MouseCursor?>? get mouseCursor =>
|
||||
WidgetStateProperty.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return SystemMouseCursors.basic;
|
||||
}
|
||||
return SystemMouseCursors.click;
|
||||
});
|
||||
|
||||
@override
|
||||
VisualDensity? get visualDensity => Theme.of(context).visualDensity;
|
||||
|
||||
@override
|
||||
MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize;
|
||||
|
||||
@override
|
||||
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
|
||||
}
|
||||
// dart format on
|
||||
|
||||
// END GENERATED TOKEN PROPERTIES - TextButton
|
||||
1874
lib/common/widgets/flutter/list_tile.dart
Normal file
1874
lib/common/widgets/flutter/list_tile.dart
Normal file
File diff suppressed because it is too large
Load Diff
461
lib/common/widgets/flutter/page/page_view.dart
Normal file
461
lib/common/widgets/flutter/page/page_view.dart
Normal file
@@ -0,0 +1,461 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// ignore_for_file: uri_does_not_exist_in_doc_import
|
||||
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
///
|
||||
/// @docImport 'single_child_scroll_view.dart';
|
||||
/// @docImport 'text.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/page/scrollable.dart';
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
import 'package:flutter/material.dart' hide Scrollable, ScrollableState;
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class _ForceImplicitScrollPhysics extends ScrollPhysics {
|
||||
const _ForceImplicitScrollPhysics({
|
||||
required this.allowImplicitScrolling,
|
||||
super.parent,
|
||||
});
|
||||
|
||||
@override
|
||||
_ForceImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return _ForceImplicitScrollPhysics(
|
||||
allowImplicitScrolling: allowImplicitScrolling,
|
||||
parent: buildParent(ancestor),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
final bool allowImplicitScrolling;
|
||||
}
|
||||
|
||||
const PageScrollPhysics _kPagePhysics = PageScrollPhysics();
|
||||
|
||||
/// A scrollable list that works page by page.
|
||||
///
|
||||
/// Each child of a page view is forced to be the same size as the viewport.
|
||||
///
|
||||
/// You can use a [PageController] to control which page is visible in the view.
|
||||
/// In addition to being able to control the pixel offset of the content inside
|
||||
/// the [CustomPageView], a [PageController] also lets you control the offset in terms
|
||||
/// of pages, which are increments of the viewport size.
|
||||
///
|
||||
/// The [PageController] can also be used to control the
|
||||
/// [PageController.initialPage], which determines which page is shown when the
|
||||
/// [CustomPageView] is first constructed, and the [PageController.viewportFraction],
|
||||
/// which determines the size of the pages as a fraction of the viewport size.
|
||||
///
|
||||
/// {@youtube 560 315 https://www.youtube.com/watch?v=J1gE9xvph-A}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// Here is an example of [CustomPageView]. It creates a centered [Text] in each of the three pages
|
||||
/// which scroll horizontally.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/page_view/page_view.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// ## Persisting the scroll position during a session
|
||||
///
|
||||
/// Scroll views attempt to persist their scroll position using [PageStorage].
|
||||
/// For a [CustomPageView], this can be disabled by setting [PageController.keepPage]
|
||||
/// to false on the [controller]. If it is enabled, using a [PageStorageKey] for
|
||||
/// the [key] of this widget is recommended to help disambiguate different
|
||||
/// scroll views from each other.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [PageController], which controls which page is visible in the view.
|
||||
/// * [SingleChildScrollView], when you need to make a single child scrollable.
|
||||
/// * [ListView], for a scrollable list of boxes.
|
||||
/// * [GridView], for a scrollable grid of boxes.
|
||||
/// * [ScrollNotification] and [NotificationListener], which can be used to watch
|
||||
/// the scroll position without using a [ScrollController].
|
||||
class CustomPageView extends StatefulWidget {
|
||||
/// Creates a scrollable list that works page by page from an explicit [List]
|
||||
/// of widgets.
|
||||
///
|
||||
/// This constructor is appropriate for page views with a small number of
|
||||
/// children because constructing the [List] requires doing work for every
|
||||
/// child that could possibly be displayed in the page view, instead of just
|
||||
/// those children that are actually visible.
|
||||
///
|
||||
/// Like other widgets in the framework, this widget expects that
|
||||
/// the [children] list will not be mutated after it has been passed in here.
|
||||
/// See the documentation at [SliverChildListDelegate.children] for more details.
|
||||
///
|
||||
/// {@template flutter.widgets.PageView.allowImplicitScrolling}
|
||||
/// If [allowImplicitScrolling] is true, the [CustomPageView] will participate in
|
||||
/// accessibility scrolling more like a [ListView], where implicit scroll
|
||||
/// actions will move to the next page rather than into the contents of the
|
||||
/// [CustomPageView].
|
||||
/// {@endtemplate}
|
||||
CustomPageView({
|
||||
super.key,
|
||||
this.scrollDirection = Axis.horizontal,
|
||||
this.reverse = false,
|
||||
this.controller,
|
||||
this.physics,
|
||||
this.pageSnapping = true,
|
||||
this.onPageChanged,
|
||||
List<Widget> children = const <Widget>[],
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.allowImplicitScrolling = false,
|
||||
this.restorationId,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.hitTestBehavior = HitTestBehavior.opaque,
|
||||
this.scrollBehavior,
|
||||
this.padEnds = true,
|
||||
this.header,
|
||||
this.bgColor = Colors.transparent,
|
||||
}) : childrenDelegate = SliverChildListDelegate(children);
|
||||
|
||||
final Widget? header;
|
||||
final Color bgColor;
|
||||
|
||||
/// Creates a scrollable list that works page by page using widgets that are
|
||||
/// created on demand.
|
||||
///
|
||||
/// This constructor is appropriate for page views with a large (or infinite)
|
||||
/// number of children because the builder is called only for those children
|
||||
/// that are actually visible.
|
||||
///
|
||||
/// Providing a non-null [itemCount] lets the [CustomPageView] compute the maximum
|
||||
/// scroll extent.
|
||||
///
|
||||
/// [itemBuilder] will be called only with indices greater than or equal to
|
||||
/// zero and less than [itemCount].
|
||||
///
|
||||
/// {@macro flutter.widgets.ListView.builder.itemBuilder}
|
||||
///
|
||||
/// {@template flutter.widgets.PageView.findChildIndexCallback}
|
||||
/// The [findChildIndexCallback] corresponds to the
|
||||
/// [SliverChildBuilderDelegate.findChildIndexCallback] property. If null,
|
||||
/// a child widget may not map to its existing [RenderObject] when the order
|
||||
/// of children returned from the children builder changes.
|
||||
/// This may result in state-loss. This callback needs to be implemented if
|
||||
/// the order of the children may change at a later time.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// {@macro flutter.widgets.PageView.allowImplicitScrolling}
|
||||
CustomPageView.builder({
|
||||
super.key,
|
||||
this.scrollDirection = Axis.horizontal,
|
||||
this.reverse = false,
|
||||
this.controller,
|
||||
this.physics,
|
||||
this.pageSnapping = true,
|
||||
this.onPageChanged,
|
||||
required NullableIndexedWidgetBuilder itemBuilder,
|
||||
ChildIndexGetter? findChildIndexCallback,
|
||||
int? itemCount,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.allowImplicitScrolling = false,
|
||||
this.restorationId,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.hitTestBehavior = HitTestBehavior.opaque,
|
||||
this.scrollBehavior,
|
||||
this.padEnds = true,
|
||||
this.header,
|
||||
this.bgColor = Colors.transparent,
|
||||
}) : childrenDelegate = SliverChildBuilderDelegate(
|
||||
itemBuilder,
|
||||
findChildIndexCallback: findChildIndexCallback,
|
||||
childCount: itemCount,
|
||||
);
|
||||
|
||||
/// Creates a scrollable list that works page by page with a custom child
|
||||
/// model.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows a [CustomPageView] that uses a custom [SliverChildBuilderDelegate] to support child
|
||||
/// reordering.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/page_view/page_view.1.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@macro flutter.widgets.PageView.allowImplicitScrolling}
|
||||
const CustomPageView.custom({
|
||||
super.key,
|
||||
this.scrollDirection = Axis.horizontal,
|
||||
this.reverse = false,
|
||||
this.controller,
|
||||
this.physics,
|
||||
this.pageSnapping = true,
|
||||
this.onPageChanged,
|
||||
required this.childrenDelegate,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.allowImplicitScrolling = false,
|
||||
this.restorationId,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.hitTestBehavior = HitTestBehavior.opaque,
|
||||
this.scrollBehavior,
|
||||
this.padEnds = true,
|
||||
this.header,
|
||||
this.bgColor = Colors.transparent,
|
||||
});
|
||||
|
||||
/// Controls whether the widget's pages will respond to
|
||||
/// [RenderObject.showOnScreen], which will allow for implicit accessibility
|
||||
/// scrolling.
|
||||
///
|
||||
/// With this flag set to false, when accessibility focus reaches the end of
|
||||
/// the current page and the user attempts to move it to the next element, the
|
||||
/// focus will traverse to the next widget outside of the page view.
|
||||
///
|
||||
/// With this flag set to true, when accessibility focus reaches the end of
|
||||
/// the current page and user attempts to move it to the next element, focus
|
||||
/// will traverse to the next page in the page view.
|
||||
final bool allowImplicitScrolling;
|
||||
|
||||
/// {@macro flutter.widgets.scrollable.restorationId}
|
||||
final String? restorationId;
|
||||
|
||||
/// The [Axis] along which the scroll view's offset increases with each page.
|
||||
///
|
||||
/// For the direction in which active scrolling may be occurring, see
|
||||
/// [ScrollDirection].
|
||||
///
|
||||
/// Defaults to [Axis.horizontal].
|
||||
final Axis scrollDirection;
|
||||
|
||||
/// Whether the page view scrolls in the reading direction.
|
||||
///
|
||||
/// For example, if the reading direction is left-to-right and
|
||||
/// [scrollDirection] is [Axis.horizontal], then the page view scrolls from
|
||||
/// left to right when [reverse] is false and from right to left when
|
||||
/// [reverse] is true.
|
||||
///
|
||||
/// Similarly, if [scrollDirection] is [Axis.vertical], then the page view
|
||||
/// scrolls from top to bottom when [reverse] is false and from bottom to top
|
||||
/// when [reverse] is true.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool reverse;
|
||||
|
||||
/// An object that can be used to control the position to which this page
|
||||
/// view is scrolled.
|
||||
final PageController? controller;
|
||||
|
||||
/// How the page view should respond to user input.
|
||||
///
|
||||
/// For example, determines how the page view continues to animate after the
|
||||
/// user stops dragging the page view.
|
||||
///
|
||||
/// The physics are modified to snap to page boundaries using
|
||||
/// [PageScrollPhysics] prior to being used.
|
||||
///
|
||||
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
|
||||
/// [ScrollPhysics] provided by that behavior will take precedence after
|
||||
/// [physics].
|
||||
///
|
||||
/// Defaults to matching platform conventions.
|
||||
final ScrollPhysics? physics;
|
||||
|
||||
/// Set to false to disable page snapping, useful for custom scroll behavior.
|
||||
///
|
||||
/// If the [padEnds] is false and [PageController.viewportFraction] < 1.0,
|
||||
/// the page will snap to the beginning of the viewport; otherwise, the page
|
||||
/// will snap to the center of the viewport.
|
||||
final bool pageSnapping;
|
||||
|
||||
/// Called whenever the page in the center of the viewport changes.
|
||||
final ValueChanged<int>? onPageChanged;
|
||||
|
||||
/// A delegate that provides the children for the [CustomPageView].
|
||||
///
|
||||
/// The [PageView.custom] constructor lets you specify this delegate
|
||||
/// explicitly. The [CustomPageView] and [PageView.builder] constructors create a
|
||||
/// [childrenDelegate] that wraps the given [List] and [IndexedWidgetBuilder],
|
||||
/// respectively.
|
||||
final SliverChildDelegate childrenDelegate;
|
||||
|
||||
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
||||
final DragStartBehavior dragStartBehavior;
|
||||
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// Defaults to [Clip.hardEdge].
|
||||
final Clip clipBehavior;
|
||||
|
||||
/// {@macro flutter.widgets.scrollable.hitTestBehavior}
|
||||
///
|
||||
/// Defaults to [HitTestBehavior.opaque].
|
||||
final HitTestBehavior hitTestBehavior;
|
||||
|
||||
/// {@macro flutter.widgets.scrollable.scrollBehavior}
|
||||
///
|
||||
/// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be
|
||||
/// modified by default to not apply a [Scrollbar].
|
||||
final ScrollBehavior? scrollBehavior;
|
||||
|
||||
/// Whether to add padding to both ends of the list.
|
||||
///
|
||||
/// If this is set to true and [PageController.viewportFraction] < 1.0, padding will be added
|
||||
/// such that the first and last child slivers will be in the center of
|
||||
/// the viewport when scrolled all the way to the start or end, respectively.
|
||||
///
|
||||
/// If [PageController.viewportFraction] >= 1.0, this property has no effect.
|
||||
///
|
||||
/// This property defaults to true.
|
||||
final bool padEnds;
|
||||
|
||||
@override
|
||||
State<CustomPageView> createState() => _CustomPageViewState();
|
||||
}
|
||||
|
||||
class _CustomPageViewState extends State<CustomPageView> {
|
||||
int _lastReportedPage = 0;
|
||||
|
||||
late PageController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initController();
|
||||
_lastReportedPage = _controller.initialPage;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (widget.controller == null) {
|
||||
_controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _initController() {
|
||||
_controller = widget.controller ?? PageController();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CustomPageView oldWidget) {
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
if (oldWidget.controller == null) {
|
||||
_controller.dispose();
|
||||
}
|
||||
_initController();
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
AxisDirection _getDirection(BuildContext context) {
|
||||
switch (widget.scrollDirection) {
|
||||
case Axis.horizontal:
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
final TextDirection textDirection = Directionality.of(context);
|
||||
final AxisDirection axisDirection = textDirectionToAxisDirection(
|
||||
textDirection,
|
||||
);
|
||||
return widget.reverse
|
||||
? flipAxisDirection(axisDirection)
|
||||
: axisDirection;
|
||||
case Axis.vertical:
|
||||
return widget.reverse ? AxisDirection.up : AxisDirection.down;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final AxisDirection axisDirection = _getDirection(context);
|
||||
final ScrollPhysics physics =
|
||||
_ForceImplicitScrollPhysics(
|
||||
allowImplicitScrolling: widget.allowImplicitScrolling,
|
||||
).applyTo(
|
||||
widget.pageSnapping
|
||||
? _kPagePhysics.applyTo(
|
||||
widget.physics ??
|
||||
widget.scrollBehavior?.getScrollPhysics(context),
|
||||
)
|
||||
: widget.physics ??
|
||||
widget.scrollBehavior?.getScrollPhysics(context),
|
||||
);
|
||||
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: (ScrollNotification notification) {
|
||||
if (notification.depth == 0 &&
|
||||
widget.onPageChanged != null &&
|
||||
notification is ScrollUpdateNotification) {
|
||||
final PageMetrics metrics = notification.metrics as PageMetrics;
|
||||
final int currentPage = metrics.page!.round();
|
||||
if (currentPage != _lastReportedPage) {
|
||||
_lastReportedPage = currentPage;
|
||||
widget.onPageChanged!(currentPage);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: CustomScrollable(
|
||||
header: widget.header,
|
||||
bgColor: widget.bgColor,
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
axisDirection: axisDirection,
|
||||
controller: _controller,
|
||||
physics: physics,
|
||||
restorationId: widget.restorationId,
|
||||
hitTestBehavior: widget.hitTestBehavior,
|
||||
scrollBehavior:
|
||||
widget.scrollBehavior ??
|
||||
ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||
viewportBuilder: (BuildContext context, ViewportOffset position) {
|
||||
return Viewport(
|
||||
// TODO(dnfield): we should provide a way to set cacheExtent
|
||||
// independent of implicit scrolling:
|
||||
// https://github.com/flutter/flutter/issues/45632
|
||||
cacheExtent: widget.allowImplicitScrolling ? 1.0 : 0.0,
|
||||
cacheExtentStyle: CacheExtentStyle.viewport,
|
||||
axisDirection: axisDirection,
|
||||
offset: position,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
slivers: <Widget>[
|
||||
SliverFillViewport(
|
||||
viewportFraction: _controller.viewportFraction,
|
||||
delegate: widget.childrenDelegate,
|
||||
padEnds: widget.padEnds,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
||||
super.debugFillProperties(description);
|
||||
description
|
||||
..add(EnumProperty<Axis>('scrollDirection', widget.scrollDirection))
|
||||
..add(FlagProperty('reverse', value: widget.reverse, ifTrue: 'reversed'))
|
||||
..add(
|
||||
DiagnosticsProperty<PageController>(
|
||||
'controller',
|
||||
_controller,
|
||||
showName: false,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<ScrollPhysics>(
|
||||
'physics',
|
||||
widget.physics,
|
||||
showName: false,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
FlagProperty(
|
||||
'pageSnapping',
|
||||
value: widget.pageSnapping,
|
||||
ifFalse: 'snapping disabled',
|
||||
),
|
||||
)
|
||||
..add(
|
||||
FlagProperty(
|
||||
'allowImplicitScrolling',
|
||||
value: widget.allowImplicitScrolling,
|
||||
ifTrue: 'allow implicit scrolling',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
2203
lib/common/widgets/flutter/page/scrollable.dart
Normal file
2203
lib/common/widgets/flutter/page/scrollable.dart
Normal file
File diff suppressed because it is too large
Load Diff
378
lib/common/widgets/flutter/page/tabs.dart
Normal file
378
lib/common/widgets/flutter/page/tabs.dart
Normal file
@@ -0,0 +1,378 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui' show SemanticsRole;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart';
|
||||
import 'package:flutter/foundation.dart' show clampDouble;
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
import 'package:flutter/material.dart' hide TabBarView, PageView;
|
||||
|
||||
/// A page view that displays the widget which corresponds to the currently
|
||||
/// selected tab.
|
||||
///
|
||||
/// This widget is typically used in conjunction with a [TabBar].
|
||||
///
|
||||
/// {@youtube 560 315 https://www.youtube.com/watch?v=POtoEH-5l40}
|
||||
///
|
||||
/// If a [TabController] is not provided, then there must be a [DefaultTabController]
|
||||
/// ancestor.
|
||||
///
|
||||
/// The tab controller's [TabController.length] must equal the length of the
|
||||
/// [children] list and the length of the [TabBar.tabs] list.
|
||||
///
|
||||
/// To see a sample implementation, visit the [TabController] documentation.
|
||||
class CustomTabBarView extends StatefulWidget {
|
||||
/// Creates a page view with one child per tab.
|
||||
///
|
||||
/// The length of [children] must be the same as the [controller]'s length.
|
||||
const CustomTabBarView({
|
||||
super.key,
|
||||
required this.children,
|
||||
this.controller,
|
||||
this.physics,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.viewportFraction = 1.0,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.scrollDirection = Axis.horizontal,
|
||||
this.header,
|
||||
this.bgColor = Colors.transparent,
|
||||
});
|
||||
|
||||
final Widget? header;
|
||||
final Color bgColor;
|
||||
|
||||
/// This widget's selection and animation state.
|
||||
///
|
||||
/// If [TabController] is not provided, then the value of [DefaultTabController.of]
|
||||
/// will be used.
|
||||
final TabController? controller;
|
||||
|
||||
/// One widget per tab.
|
||||
///
|
||||
/// Its length must match the length of the [TabBar.tabs]
|
||||
/// list, as well as the [controller]'s [TabController.length].
|
||||
final List<Widget> children;
|
||||
|
||||
/// How the page view should respond to user input.
|
||||
///
|
||||
/// For example, determines how the page view continues to animate after the
|
||||
/// user stops dragging the page view.
|
||||
///
|
||||
/// The physics are modified to snap to page boundaries using
|
||||
/// [PageScrollPhysics] prior to being used.
|
||||
///
|
||||
/// Defaults to matching platform conventions.
|
||||
final ScrollPhysics? physics;
|
||||
|
||||
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
||||
final DragStartBehavior dragStartBehavior;
|
||||
|
||||
/// {@macro flutter.widgets.pageview.viewportFraction}
|
||||
final double viewportFraction;
|
||||
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// Defaults to [Clip.hardEdge].
|
||||
final Clip clipBehavior;
|
||||
|
||||
final Axis scrollDirection;
|
||||
|
||||
@override
|
||||
State<CustomTabBarView> createState() => _CustomTabBarViewState();
|
||||
}
|
||||
|
||||
class _CustomTabBarViewState extends State<CustomTabBarView> {
|
||||
TabController? _controller;
|
||||
PageController? _pageController;
|
||||
late List<Widget> _childrenWithKey;
|
||||
int? _currentIndex;
|
||||
int _warpUnderwayCount = 0;
|
||||
int _scrollUnderwayCount = 0;
|
||||
bool _debugHasScheduledValidChildrenCountCheck = false;
|
||||
|
||||
// If the TabBarView is rebuilt with a new tab controller, the caller should
|
||||
// dispose the old one. In that case the old controller's animation will be
|
||||
// null and should not be accessed.
|
||||
bool get _controllerIsValid => _controller?.animation != null;
|
||||
|
||||
void _updateTabController() {
|
||||
final TabController? newController =
|
||||
widget.controller ?? DefaultTabController.maybeOf(context);
|
||||
assert(() {
|
||||
if (newController == null) {
|
||||
throw FlutterError(
|
||||
'No TabController for ${widget.runtimeType}.\n'
|
||||
'When creating a ${widget.runtimeType}, you must either provide an explicit '
|
||||
'TabController using the "controller" property, or you must ensure that there '
|
||||
'is a DefaultTabController above the ${widget.runtimeType}.\n'
|
||||
'In this case, there was neither an explicit controller nor a default controller.',
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
|
||||
if (newController == _controller) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_controllerIsValid) {
|
||||
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
|
||||
}
|
||||
_controller = newController;
|
||||
if (_controller != null) {
|
||||
_controller!.animation!.addListener(_handleTabControllerAnimationTick);
|
||||
}
|
||||
}
|
||||
|
||||
void _jumpToPage(int page) {
|
||||
_warpUnderwayCount += 1;
|
||||
_pageController!.jumpToPage(page);
|
||||
_warpUnderwayCount -= 1;
|
||||
}
|
||||
|
||||
Future<void> _animateToPage(
|
||||
int page, {
|
||||
required Duration duration,
|
||||
required Curve curve,
|
||||
}) async {
|
||||
_warpUnderwayCount += 1;
|
||||
await _pageController!.animateToPage(
|
||||
page,
|
||||
duration: duration,
|
||||
curve: curve,
|
||||
);
|
||||
_warpUnderwayCount -= 1;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateChildren();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_updateTabController();
|
||||
_currentIndex = _controller!.index;
|
||||
if (_pageController == null) {
|
||||
_pageController = PageController(
|
||||
initialPage: _currentIndex!,
|
||||
viewportFraction: widget.viewportFraction,
|
||||
);
|
||||
} else {
|
||||
_pageController!.jumpToPage(_currentIndex!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CustomTabBarView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.controller != oldWidget.controller) {
|
||||
_updateTabController();
|
||||
_currentIndex = _controller!.index;
|
||||
_jumpToPage(_currentIndex!);
|
||||
}
|
||||
if (widget.viewportFraction != oldWidget.viewportFraction) {
|
||||
_pageController?.dispose();
|
||||
_pageController = PageController(
|
||||
initialPage: _currentIndex!,
|
||||
viewportFraction: widget.viewportFraction,
|
||||
);
|
||||
}
|
||||
// While a warp is under way, we stop updating the tab page contents.
|
||||
// This is tracked in https://github.com/flutter/flutter/issues/31269.
|
||||
if (widget.children != oldWidget.children && _warpUnderwayCount == 0) {
|
||||
_updateChildren();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_controllerIsValid) {
|
||||
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
|
||||
}
|
||||
_controller = null;
|
||||
_pageController?.dispose();
|
||||
// We don't own the _controller Animation, so it's not disposed here.
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateChildren() {
|
||||
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(
|
||||
widget.children.map<Widget>((Widget child) {
|
||||
return Semantics(role: SemanticsRole.tabPanel, child: child);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleTabControllerAnimationTick() {
|
||||
if (_scrollUnderwayCount > 0 || !_controller!.indexIsChanging) {
|
||||
return;
|
||||
} // This widget is driving the controller's animation.
|
||||
|
||||
if (_controller!.index != _currentIndex) {
|
||||
_currentIndex = _controller!.index;
|
||||
_warpToCurrentIndex();
|
||||
}
|
||||
}
|
||||
|
||||
void _warpToCurrentIndex() {
|
||||
if (!mounted || _pageController!.page == _currentIndex!.toDouble()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final bool adjacentDestination =
|
||||
(_currentIndex! - _controller!.previousIndex).abs() == 1;
|
||||
if (adjacentDestination) {
|
||||
_warpToAdjacentTab(_controller!.animationDuration);
|
||||
} else {
|
||||
_warpToNonAdjacentTab(_controller!.animationDuration);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _warpToAdjacentTab(Duration duration) async {
|
||||
if (duration == Duration.zero) {
|
||||
_jumpToPage(_currentIndex!);
|
||||
} else {
|
||||
await _animateToPage(
|
||||
_currentIndex!,
|
||||
duration: duration,
|
||||
curve: Curves.ease,
|
||||
);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(_updateChildren);
|
||||
}
|
||||
return Future<void>.value();
|
||||
}
|
||||
|
||||
Future<void> _warpToNonAdjacentTab(Duration duration) async {
|
||||
final int previousIndex = _controller!.previousIndex;
|
||||
assert((_currentIndex! - previousIndex).abs() > 1);
|
||||
|
||||
// initialPage defines which page is shown when starting the animation.
|
||||
// This page is adjacent to the destination page.
|
||||
final int initialPage = _currentIndex! > previousIndex
|
||||
? _currentIndex! - 1
|
||||
: _currentIndex! + 1;
|
||||
|
||||
setState(() {
|
||||
// Needed for `RenderSliverMultiBoxAdaptor.move` and kept alive children.
|
||||
// For motivation, see https://github.com/flutter/flutter/pull/29188 and
|
||||
// https://github.com/flutter/flutter/issues/27010#issuecomment-486475152.
|
||||
_childrenWithKey = List<Widget>.of(_childrenWithKey, growable: false);
|
||||
final Widget temp = _childrenWithKey[initialPage];
|
||||
_childrenWithKey[initialPage] = _childrenWithKey[previousIndex];
|
||||
_childrenWithKey[previousIndex] = temp;
|
||||
});
|
||||
|
||||
// Make a first jump to the adjacent page.
|
||||
_jumpToPage(initialPage);
|
||||
|
||||
// Jump or animate to the destination page.
|
||||
if (duration == Duration.zero) {
|
||||
_jumpToPage(_currentIndex!);
|
||||
} else {
|
||||
await _animateToPage(
|
||||
_currentIndex!,
|
||||
duration: duration,
|
||||
curve: Curves.ease,
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(_updateChildren);
|
||||
}
|
||||
}
|
||||
|
||||
void _syncControllerOffset() {
|
||||
_controller!.offset = clampDouble(
|
||||
_pageController!.page! - _controller!.index,
|
||||
-1.0,
|
||||
1.0,
|
||||
);
|
||||
}
|
||||
|
||||
// Called when the PageView scrolls
|
||||
bool _handleScrollNotification(ScrollNotification notification) {
|
||||
if (_warpUnderwayCount > 0 || _scrollUnderwayCount > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (notification.depth != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_controllerIsValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_scrollUnderwayCount += 1;
|
||||
final double page = _pageController!.page!;
|
||||
if (notification is ScrollUpdateNotification &&
|
||||
!_controller!.indexIsChanging) {
|
||||
final bool pageChanged = (page - _controller!.index).abs() > 1.0;
|
||||
if (pageChanged) {
|
||||
_controller!.index = page.round();
|
||||
_currentIndex = _controller!.index;
|
||||
}
|
||||
_syncControllerOffset();
|
||||
} else if (notification is ScrollEndNotification) {
|
||||
_controller!.index = page.round();
|
||||
_currentIndex = _controller!.index;
|
||||
if (!_controller!.indexIsChanging) {
|
||||
_syncControllerOffset();
|
||||
}
|
||||
}
|
||||
_scrollUnderwayCount -= 1;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _debugScheduleCheckHasValidChildrenCount() {
|
||||
if (_debugHasScheduledValidChildrenCountCheck) {
|
||||
return true;
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
|
||||
_debugHasScheduledValidChildrenCountCheck = false;
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
assert(() {
|
||||
if (_controller!.length != widget.children.length) {
|
||||
throw FlutterError(
|
||||
"Controller's length property (${_controller!.length}) does not match the "
|
||||
"number of children (${widget.children.length}) present in TabBarView's children property.",
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
}, debugLabel: 'TabBarView.validChildrenCountCheck');
|
||||
_debugHasScheduledValidChildrenCountCheck = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(_debugScheduleCheckHasValidChildrenCount());
|
||||
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: _handleScrollNotification,
|
||||
child: CustomPageView(
|
||||
scrollDirection: widget.scrollDirection,
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
controller: _pageController,
|
||||
physics: widget.physics == null
|
||||
? const PageScrollPhysics().applyTo(const ClampingScrollPhysics())
|
||||
: const PageScrollPhysics().applyTo(widget.physics),
|
||||
header: widget.header,
|
||||
bgColor: widget.bgColor,
|
||||
children: _childrenWithKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
3821
lib/common/widgets/flutter/text/paragraph.dart
Normal file
3821
lib/common/widgets/flutter/text/paragraph.dart
Normal file
File diff suppressed because it is too large
Load Diff
347
lib/common/widgets/flutter/text/rich_text.dart
Normal file
347
lib/common/widgets/flutter/text/rich_text.dart
Normal file
@@ -0,0 +1,347 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart' hide RenderParagraph;
|
||||
|
||||
/// A paragraph of rich text.
|
||||
///
|
||||
/// {@youtube 560 315 https://www.youtube.com/watch?v=rykDVh-QFfw}
|
||||
///
|
||||
/// The [RichText] widget displays text that uses multiple different styles. The
|
||||
/// text to display is described using a tree of [TextSpan] objects, each of
|
||||
/// which has an associated style that is used for that subtree. The text might
|
||||
/// break across multiple lines or might all be displayed on the same line
|
||||
/// depending on the layout constraints.
|
||||
///
|
||||
/// Text displayed in a [RichText] widget must be explicitly styled. When
|
||||
/// picking which style to use, consider using [DefaultTextStyle.of] the current
|
||||
/// [BuildContext] to provide defaults. For more details on how to style text in
|
||||
/// a [RichText] widget, see the documentation for [TextStyle].
|
||||
///
|
||||
/// Consider using the [Text] widget to integrate with the [DefaultTextStyle]
|
||||
/// automatically. When all the text uses the same style, the default constructor
|
||||
/// is less verbose. The [Text.rich] constructor allows you to style multiple
|
||||
/// spans with the default text style while still allowing specified styles per
|
||||
/// span.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
///
|
||||
/// This sample demonstrates how to mix and match text with different text
|
||||
/// styles using the [RichText] Widget. It displays the text "Hello bold world,"
|
||||
/// emphasizing the word "bold" using a bold font weight.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// ```dart
|
||||
/// RichText(
|
||||
/// text: TextSpan(
|
||||
/// text: 'Hello ',
|
||||
/// style: DefaultTextStyle.of(context).style,
|
||||
/// children: const <TextSpan>[
|
||||
/// TextSpan(text: 'bold', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
/// TextSpan(text: ' world!'),
|
||||
/// ],
|
||||
/// ),
|
||||
/// )
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// ## Selections
|
||||
///
|
||||
/// To make this [RichText] Selectable, the [RichText] needs to be in the
|
||||
/// subtree of a [SelectionArea] or [SelectableRegion] and a
|
||||
/// [SelectionRegistrar] needs to be assigned to the
|
||||
/// [RichText.selectionRegistrar]. One can use
|
||||
/// [SelectionContainer.maybeOf] to get the [SelectionRegistrar] from a
|
||||
/// context. This enables users to select the text in [RichText]s with mice or
|
||||
/// touch events.
|
||||
///
|
||||
/// The [selectionColor] also needs to be set if the selection is enabled to
|
||||
/// draw the selection highlights.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
///
|
||||
/// This sample demonstrates how to assign a [SelectionRegistrar] for RichTexts
|
||||
/// in the SelectionArea subtree.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// ```dart
|
||||
/// RichText(
|
||||
/// text: const TextSpan(text: 'Hello'),
|
||||
/// selectionRegistrar: SelectionContainer.maybeOf(context),
|
||||
/// selectionColor: const Color(0xAF6694e8),
|
||||
/// )
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextStyle], which discusses how to style text.
|
||||
/// * [TextSpan], which is used to describe the text in a paragraph.
|
||||
/// * [Text], which automatically applies the ambient styles described by a
|
||||
/// [DefaultTextStyle] to a single string.
|
||||
/// * [Text.rich], a const text widget that provides similar functionality
|
||||
/// as [RichText]. [Text.rich] will inherit [TextStyle] from [DefaultTextStyle].
|
||||
/// * [SelectableRegion], which provides an overview of the selection system.
|
||||
class RichText extends MultiChildRenderObjectWidget {
|
||||
/// Creates a paragraph of rich text.
|
||||
///
|
||||
/// The [maxLines] property may be null (and indeed defaults to null), but if
|
||||
/// it is not null, it must be greater than zero.
|
||||
///
|
||||
/// The [textDirection], if null, defaults to the ambient [Directionality],
|
||||
/// which in that case must not be null.
|
||||
RichText({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.textAlign = TextAlign.start,
|
||||
this.textDirection,
|
||||
this.softWrap = true,
|
||||
this.overflow = TextOverflow.clip,
|
||||
@Deprecated(
|
||||
'Use textScaler instead. '
|
||||
'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
|
||||
'This feature was deprecated after v3.12.0-2.0.pre.',
|
||||
)
|
||||
double textScaleFactor = 1.0,
|
||||
TextScaler textScaler = TextScaler.noScaling,
|
||||
this.maxLines,
|
||||
this.locale,
|
||||
this.strutStyle,
|
||||
this.textWidthBasis = TextWidthBasis.parent,
|
||||
this.textHeightBehavior,
|
||||
this.selectionRegistrar,
|
||||
this.selectionColor,
|
||||
this.onShowMore,
|
||||
required this.primary,
|
||||
}) : assert(maxLines == null || maxLines > 0),
|
||||
assert(selectionRegistrar == null || selectionColor != null),
|
||||
assert(
|
||||
textScaleFactor == 1.0 || identical(textScaler, TextScaler.noScaling),
|
||||
'Use textScaler instead.',
|
||||
),
|
||||
textScaler = _effectiveTextScalerFrom(textScaler, textScaleFactor),
|
||||
super(
|
||||
children: WidgetSpan.extractFromInlineSpan(
|
||||
text,
|
||||
_effectiveTextScalerFrom(textScaler, textScaleFactor),
|
||||
),
|
||||
);
|
||||
|
||||
static TextScaler _effectiveTextScalerFrom(
|
||||
TextScaler textScaler,
|
||||
double textScaleFactor,
|
||||
) {
|
||||
return switch ((textScaler, textScaleFactor)) {
|
||||
(final TextScaler scaler, 1.0) => scaler,
|
||||
(TextScaler.noScaling, final double textScaleFactor) => TextScaler.linear(
|
||||
textScaleFactor,
|
||||
),
|
||||
(final TextScaler scaler, _) => scaler,
|
||||
};
|
||||
}
|
||||
|
||||
/// The text to display in this widget.
|
||||
final InlineSpan text;
|
||||
|
||||
/// How the text should be aligned horizontally.
|
||||
final TextAlign textAlign;
|
||||
|
||||
/// The directionality of the text.
|
||||
///
|
||||
/// This decides how [textAlign] values like [TextAlign.start] and
|
||||
/// [TextAlign.end] are interpreted.
|
||||
///
|
||||
/// This is also used to disambiguate how to render bidirectional text. For
|
||||
/// example, if the [text] is an English phrase followed by a Hebrew phrase,
|
||||
/// in a [TextDirection.ltr] context the English phrase will be on the left
|
||||
/// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
|
||||
/// context, the English phrase will be on the right and the Hebrew phrase on
|
||||
/// its left.
|
||||
///
|
||||
/// Defaults to the ambient [Directionality], if any. If there is no ambient
|
||||
/// [Directionality], then this must not be null.
|
||||
final TextDirection? textDirection;
|
||||
|
||||
/// Whether the text should break at soft line breaks.
|
||||
///
|
||||
/// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space.
|
||||
final bool softWrap;
|
||||
|
||||
/// How visual overflow should be handled.
|
||||
final TextOverflow overflow;
|
||||
|
||||
/// Deprecated. Will be removed in a future version of Flutter. Use
|
||||
/// [textScaler] instead.
|
||||
///
|
||||
/// The number of font pixels for each logical pixel.
|
||||
///
|
||||
/// For example, if the text scale factor is 1.5, text will be 50% larger than
|
||||
/// the specified font size.
|
||||
@Deprecated(
|
||||
'Use textScaler instead. '
|
||||
'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
|
||||
'This feature was deprecated after v3.12.0-2.0.pre.',
|
||||
)
|
||||
double get textScaleFactor => textScaler.textScaleFactor;
|
||||
|
||||
/// {@macro flutter.painting.textPainter.textScaler}
|
||||
final TextScaler textScaler;
|
||||
|
||||
/// An optional maximum number of lines for the text to span, wrapping if necessary.
|
||||
/// If the text exceeds the given number of lines, it will be truncated according
|
||||
/// to [overflow].
|
||||
///
|
||||
/// If this is 1, text will not wrap. Otherwise, text will be wrapped at the
|
||||
/// edge of the box.
|
||||
final int? maxLines;
|
||||
|
||||
/// Used to select a font when the same Unicode character can
|
||||
/// be rendered differently, depending on the locale.
|
||||
///
|
||||
/// It's rarely necessary to set this property. By default its value
|
||||
/// is inherited from the enclosing app with `Localizations.localeOf(context)`.
|
||||
///
|
||||
/// See [RenderParagraph.locale] for more information.
|
||||
final Locale? locale;
|
||||
|
||||
/// {@macro flutter.painting.textPainter.strutStyle}
|
||||
final StrutStyle? strutStyle;
|
||||
|
||||
/// {@macro flutter.painting.textPainter.textWidthBasis}
|
||||
final TextWidthBasis textWidthBasis;
|
||||
|
||||
/// {@macro dart.ui.textHeightBehavior}
|
||||
final ui.TextHeightBehavior? textHeightBehavior;
|
||||
|
||||
/// The [SelectionRegistrar] this rich text is subscribed to.
|
||||
///
|
||||
/// If this is set, [selectionColor] must be non-null.
|
||||
final SelectionRegistrar? selectionRegistrar;
|
||||
|
||||
/// The color to use when painting the selection.
|
||||
///
|
||||
/// This is ignored if [selectionRegistrar] is null.
|
||||
///
|
||||
/// See the section on selections in the [RichText] top-level API
|
||||
/// documentation for more details on enabling selection in [RichText]
|
||||
/// widgets.
|
||||
final Color? selectionColor;
|
||||
|
||||
final Color primary;
|
||||
|
||||
final VoidCallback? onShowMore;
|
||||
|
||||
@override
|
||||
RenderParagraph createRenderObject(BuildContext context) {
|
||||
assert(textDirection != null || debugCheckHasDirectionality(context));
|
||||
return RenderParagraph(
|
||||
text,
|
||||
textAlign: textAlign,
|
||||
textDirection: textDirection ?? Directionality.of(context),
|
||||
softWrap: softWrap,
|
||||
overflow: overflow,
|
||||
textScaler: textScaler,
|
||||
maxLines: maxLines,
|
||||
strutStyle: strutStyle,
|
||||
textWidthBasis: textWidthBasis,
|
||||
textHeightBehavior: textHeightBehavior,
|
||||
locale: locale ?? Localizations.maybeLocaleOf(context),
|
||||
registrar: selectionRegistrar,
|
||||
selectionColor: selectionColor,
|
||||
primary: primary,
|
||||
onShowMore: onShowMore,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderParagraph renderObject) {
|
||||
assert(textDirection != null || debugCheckHasDirectionality(context));
|
||||
renderObject
|
||||
..text = text
|
||||
..textAlign = textAlign
|
||||
..textDirection = textDirection ?? Directionality.of(context)
|
||||
..softWrap = softWrap
|
||||
..overflow = overflow
|
||||
..textScaler = textScaler
|
||||
..maxLines = maxLines
|
||||
..strutStyle = strutStyle
|
||||
..textWidthBasis = textWidthBasis
|
||||
..textHeightBehavior = textHeightBehavior
|
||||
..locale = locale ?? Localizations.maybeLocaleOf(context)
|
||||
..registrar = selectionRegistrar
|
||||
..selectionColor = selectionColor
|
||||
..primary = primary
|
||||
..onShowMore = onShowMore;
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(
|
||||
EnumProperty<TextAlign>(
|
||||
'textAlign',
|
||||
textAlign,
|
||||
defaultValue: TextAlign.start,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
EnumProperty<TextDirection>(
|
||||
'textDirection',
|
||||
textDirection,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
FlagProperty(
|
||||
'softWrap',
|
||||
value: softWrap,
|
||||
ifTrue: 'wrapping at box width',
|
||||
ifFalse: 'no wrapping except at line break characters',
|
||||
showName: true,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
EnumProperty<TextOverflow>(
|
||||
'overflow',
|
||||
overflow,
|
||||
defaultValue: TextOverflow.clip,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<TextScaler>(
|
||||
'textScaler',
|
||||
textScaler,
|
||||
defaultValue: TextScaler.noScaling,
|
||||
),
|
||||
);
|
||||
properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
|
||||
properties.add(
|
||||
EnumProperty<TextWidthBasis>(
|
||||
'textWidthBasis',
|
||||
textWidthBasis,
|
||||
defaultValue: TextWidthBasis.parent,
|
||||
),
|
||||
);
|
||||
properties.add(StringProperty('text', text.toPlainText()));
|
||||
properties.add(
|
||||
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<StrutStyle>(
|
||||
'strutStyle',
|
||||
strutStyle,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<TextHeightBehavior>(
|
||||
'textHeightBehavior',
|
||||
textHeightBehavior,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1260
lib/common/widgets/flutter/text/text.dart
Normal file
1260
lib/common/widgets/flutter/text/text.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,354 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// ignore_for_file: uri_does_not_exist_in_doc_import
|
||||
|
||||
/// @docImport 'selectable_text.dart';
|
||||
/// @docImport 'selection_area.dart';
|
||||
/// @docImport 'text_field.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/material.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
/// The default context menu for text selection for the current platform.
|
||||
///
|
||||
/// {@template flutter.material.AdaptiveTextSelectionToolbar.contextMenuBuilders}
|
||||
/// Typically, this widget would be passed to `contextMenuBuilder` in a
|
||||
/// supported parent widget, such as:
|
||||
///
|
||||
/// * [EditableText.contextMenuBuilder]
|
||||
/// * [TextField.contextMenuBuilder]
|
||||
/// * [CupertinoTextField.contextMenuBuilder]
|
||||
/// * [SelectionArea.contextMenuBuilder]
|
||||
/// * [SelectableText.contextMenuBuilder]
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [EditableText.getEditableButtonItems], which returns the default
|
||||
/// [ContextMenuButtonItem]s for [EditableText] on the platform.
|
||||
/// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the button
|
||||
/// Widgets for the current platform given [ContextMenuButtonItem]s.
|
||||
/// * [CupertinoAdaptiveTextSelectionToolbar], which does the same thing as this
|
||||
/// widget but only for Cupertino context menus.
|
||||
/// * [TextSelectionToolbar], the default toolbar for Android.
|
||||
/// * [DesktopTextSelectionToolbar], the default toolbar for desktop platforms
|
||||
/// other than MacOS.
|
||||
/// * [CupertinoTextSelectionToolbar], the default toolbar for iOS.
|
||||
/// * [CupertinoDesktopTextSelectionToolbar], the default toolbar for MacOS.
|
||||
class AdaptiveTextSelectionToolbar extends StatelessWidget {
|
||||
/// Create an instance of [AdaptiveTextSelectionToolbar] with the
|
||||
/// given [children].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// {@template flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
|
||||
/// * [AdaptiveTextSelectionToolbar.buttonItems], which takes a list of
|
||||
/// [ContextMenuButtonItem]s instead of [children] widgets.
|
||||
/// {@endtemplate}
|
||||
/// {@template flutter.material.AdaptiveTextSelectionToolbar.editable}
|
||||
/// * [AdaptiveTextSelectionToolbar.editable], which builds the default
|
||||
/// children for an editable field.
|
||||
/// {@endtemplate}
|
||||
/// {@template flutter.material.AdaptiveTextSelectionToolbar.editableText}
|
||||
/// * [AdaptiveTextSelectionToolbar.editableText], which builds the default
|
||||
/// children for an [EditableText].
|
||||
/// {@endtemplate}
|
||||
/// {@template flutter.material.AdaptiveTextSelectionToolbar.selectable}
|
||||
/// * [AdaptiveTextSelectionToolbar.selectable], which builds the default
|
||||
/// children for content that is selectable but not editable.
|
||||
/// {@endtemplate}
|
||||
const AdaptiveTextSelectionToolbar({
|
||||
super.key,
|
||||
required this.children,
|
||||
required this.anchors,
|
||||
}) : buttonItems = null;
|
||||
|
||||
/// Create an instance of [AdaptiveTextSelectionToolbar] whose children will
|
||||
/// be built from the given [buttonItems].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// {@template flutter.material.AdaptiveTextSelectionToolbar.new}
|
||||
/// * [AdaptiveTextSelectionToolbar.new], which takes the children directly as
|
||||
/// a list of widgets.
|
||||
/// {@endtemplate}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editable}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editableText}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.selectable}
|
||||
const AdaptiveTextSelectionToolbar.buttonItems({
|
||||
super.key,
|
||||
required this.buttonItems,
|
||||
required this.anchors,
|
||||
}) : children = null;
|
||||
|
||||
/// Create an instance of [AdaptiveTextSelectionToolbar] with the default
|
||||
/// children for an editable field.
|
||||
///
|
||||
/// If an on* callback parameter is null, then its corresponding button will
|
||||
/// not be built.
|
||||
///
|
||||
/// These callbacks are called when their corresponding button is activated
|
||||
/// and only then. For example, `onPaste` is called when the user taps the
|
||||
/// "Paste" button in the context menu and not when the user pastes with the
|
||||
/// keyboard.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.new}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editableText}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.selectable}
|
||||
AdaptiveTextSelectionToolbar.editable({
|
||||
super.key,
|
||||
required ClipboardStatus clipboardStatus,
|
||||
required VoidCallback? onCopy,
|
||||
required VoidCallback? onCut,
|
||||
required VoidCallback? onPaste,
|
||||
required VoidCallback? onSelectAll,
|
||||
required VoidCallback? onLookUp,
|
||||
required VoidCallback? onSearchWeb,
|
||||
required VoidCallback? onShare,
|
||||
required VoidCallback? onLiveTextInput,
|
||||
required this.anchors,
|
||||
}) : children = null,
|
||||
buttonItems = EditableText.getEditableButtonItems(
|
||||
clipboardStatus: clipboardStatus,
|
||||
onCopy: onCopy,
|
||||
onCut: onCut,
|
||||
onPaste: onPaste,
|
||||
onSelectAll: onSelectAll,
|
||||
onLookUp: onLookUp,
|
||||
onSearchWeb: onSearchWeb,
|
||||
onShare: onShare,
|
||||
onLiveTextInput: onLiveTextInput,
|
||||
);
|
||||
|
||||
/// Create an instance of [AdaptiveTextSelectionToolbar] with the default
|
||||
/// children for an [EditableText].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.new}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editable}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.selectable}
|
||||
AdaptiveTextSelectionToolbar.editableText({
|
||||
super.key,
|
||||
required EditableTextState editableTextState,
|
||||
}) : children = null,
|
||||
buttonItems = editableTextState.contextMenuButtonItems,
|
||||
anchors = editableTextState.contextMenuAnchors;
|
||||
|
||||
/// Create an instance of [AdaptiveTextSelectionToolbar] with the default
|
||||
/// children for selectable, but not editable, content.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.new}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editable}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editableText}
|
||||
AdaptiveTextSelectionToolbar.selectable({
|
||||
super.key,
|
||||
required VoidCallback onCopy,
|
||||
required VoidCallback onSelectAll,
|
||||
required VoidCallback? onShare,
|
||||
required SelectionGeometry selectionGeometry,
|
||||
required this.anchors,
|
||||
}) : children = null,
|
||||
buttonItems = SelectableRegion.getSelectableButtonItems(
|
||||
selectionGeometry: selectionGeometry,
|
||||
onCopy: onCopy,
|
||||
onSelectAll: onSelectAll,
|
||||
onShare: onShare,
|
||||
);
|
||||
|
||||
/// Create an instance of [AdaptiveTextSelectionToolbar] with the default
|
||||
/// children for a [SelectableRegion].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.new}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editable}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.editableText}
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.selectable}
|
||||
AdaptiveTextSelectionToolbar.selectableRegion({
|
||||
super.key,
|
||||
required SelectableRegionState selectableRegionState,
|
||||
}) : children = null,
|
||||
buttonItems = selectableRegionState.contextMenuButtonItems,
|
||||
anchors = selectableRegionState.contextMenuAnchors;
|
||||
|
||||
/// {@template flutter.material.AdaptiveTextSelectionToolbar.buttonItems}
|
||||
/// The [ContextMenuButtonItem]s that will be turned into the correct button
|
||||
/// widgets for the current platform.
|
||||
/// {@endtemplate}
|
||||
final List<ContextMenuButtonItem>? buttonItems;
|
||||
|
||||
/// The children of the toolbar, typically buttons.
|
||||
final List<Widget>? children;
|
||||
|
||||
/// {@template flutter.material.AdaptiveTextSelectionToolbar.anchors}
|
||||
/// The location on which to anchor the menu.
|
||||
/// {@endtemplate}
|
||||
final TextSelectionToolbarAnchors anchors;
|
||||
|
||||
/// Returns the default button label String for the button of the given
|
||||
/// [ContextMenuButtonType] on any platform.
|
||||
static String getButtonLabel(
|
||||
BuildContext context,
|
||||
ContextMenuButtonItem buttonItem,
|
||||
) {
|
||||
if (buttonItem.label != null) {
|
||||
return buttonItem.label!;
|
||||
}
|
||||
|
||||
switch (Theme.of(context).platform) {
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
return CupertinoTextSelectionToolbarButton.getButtonLabel(
|
||||
context,
|
||||
buttonItem,
|
||||
);
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
assert(debugCheckHasMaterialLocalizations(context));
|
||||
final MaterialLocalizations localizations = MaterialLocalizations.of(
|
||||
context,
|
||||
);
|
||||
return switch (buttonItem.type) {
|
||||
ContextMenuButtonType.cut => localizations.cutButtonLabel,
|
||||
ContextMenuButtonType.copy => localizations.copyButtonLabel,
|
||||
ContextMenuButtonType.paste => localizations.pasteButtonLabel,
|
||||
ContextMenuButtonType.selectAll => localizations.selectAllButtonLabel,
|
||||
ContextMenuButtonType.delete =>
|
||||
localizations.deleteButtonTooltip.toUpperCase(),
|
||||
ContextMenuButtonType.lookUp => localizations.lookUpButtonLabel,
|
||||
ContextMenuButtonType.searchWeb => localizations.searchWebButtonLabel,
|
||||
ContextMenuButtonType.share => localizations.shareButtonLabel,
|
||||
ContextMenuButtonType.liveTextInput =>
|
||||
localizations.scanTextButtonLabel,
|
||||
ContextMenuButtonType.custom => '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a List of Widgets generated by turning [buttonItems] into the
|
||||
/// default context menu buttons for the current platform.
|
||||
///
|
||||
/// This is useful when building a text selection toolbar with the default
|
||||
/// button appearance for the given platform, but where the toolbar and/or the
|
||||
/// button actions and labels may be custom.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample demonstrates how to use `getAdaptiveButtons` to generate
|
||||
/// default button widgets in a custom toolbar.
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/context_menu/editable_text_toolbar_builder.2.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CupertinoAdaptiveTextSelectionToolbar.getAdaptiveButtons], which is the
|
||||
/// Cupertino equivalent of this class and builds only the Cupertino
|
||||
/// buttons.
|
||||
static Iterable<Widget> getAdaptiveButtons(
|
||||
BuildContext context,
|
||||
List<ContextMenuButtonItem> buttonItems,
|
||||
) {
|
||||
switch (Theme.of(context).platform) {
|
||||
case TargetPlatform.iOS:
|
||||
return buttonItems.map((ContextMenuButtonItem buttonItem) {
|
||||
return CupertinoTextSelectionToolbarButton.buttonItem(
|
||||
buttonItem: buttonItem,
|
||||
);
|
||||
});
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.android:
|
||||
final List<Widget> buttons = <Widget>[];
|
||||
for (int i = 0; i < buttonItems.length; i++) {
|
||||
final ContextMenuButtonItem buttonItem = buttonItems[i];
|
||||
buttons.add(
|
||||
TextSelectionToolbarTextButton(
|
||||
padding: TextSelectionToolbarTextButton.getPadding(
|
||||
i,
|
||||
buttonItems.length,
|
||||
),
|
||||
onPressed: buttonItem.onPressed,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(getButtonLabel(context, buttonItem)),
|
||||
),
|
||||
);
|
||||
}
|
||||
return buttons;
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
return buttonItems.map((ContextMenuButtonItem buttonItem) {
|
||||
return DesktopTextSelectionToolbarButton.text(
|
||||
context: context,
|
||||
onPressed: buttonItem.onPressed,
|
||||
text: getButtonLabel(context, buttonItem),
|
||||
);
|
||||
});
|
||||
case TargetPlatform.macOS:
|
||||
return buttonItems.map((ContextMenuButtonItem buttonItem) {
|
||||
return CupertinoDesktopTextSelectionToolbarButton.text(
|
||||
onPressed: buttonItem.onPressed,
|
||||
text: getButtonLabel(context, buttonItem),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// If there aren't any buttons to build, build an empty toolbar.
|
||||
if ((children != null && children!.isEmpty) ||
|
||||
(buttonItems != null && buttonItems!.isEmpty)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final List<Widget> resultChildren = children != null
|
||||
? children!
|
||||
: getAdaptiveButtons(context, buttonItems!).toList();
|
||||
|
||||
switch (Theme.of(context).platform) {
|
||||
case TargetPlatform.iOS:
|
||||
return CupertinoTextSelectionToolbar(
|
||||
anchorAbove: anchors.primaryAnchor,
|
||||
anchorBelow: anchors.secondaryAnchor == null
|
||||
? anchors.primaryAnchor
|
||||
: anchors.secondaryAnchor!,
|
||||
children: resultChildren,
|
||||
);
|
||||
case TargetPlatform.android:
|
||||
return TextSelectionToolbar(
|
||||
anchorAbove: anchors.primaryAnchor,
|
||||
anchorBelow: anchors.secondaryAnchor == null
|
||||
? anchors.primaryAnchor
|
||||
: anchors.secondaryAnchor!,
|
||||
children: resultChildren,
|
||||
);
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
return DesktopTextSelectionToolbar(
|
||||
anchor: anchors.primaryAnchor,
|
||||
children: resultChildren,
|
||||
);
|
||||
case TargetPlatform.macOS:
|
||||
return CupertinoDesktopTextSelectionToolbar(
|
||||
anchor: anchors.primaryAnchor,
|
||||
children: resultChildren,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
1076
lib/common/widgets/flutter/text_field/controller.dart
Normal file
1076
lib/common/widgets/flutter/text_field/controller.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,237 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/foundation.dart' show defaultTargetPlatform;
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
/// The default Cupertino context menu for text selection for the current
|
||||
/// platform with the given children.
|
||||
///
|
||||
/// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.platforms}
|
||||
/// Builds the mobile Cupertino context menu on all mobile platforms, not just
|
||||
/// iOS, and builds the desktop Cupertino context menu on all desktop platforms,
|
||||
/// not just MacOS. For a widget that builds the native-looking context menu for
|
||||
/// all platforms, see [AdaptiveTextSelectionToolbar].
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [AdaptiveTextSelectionToolbar], which does the same thing as this widget
|
||||
/// but for all platforms, not just the Cupertino-styled platforms.
|
||||
/// * [CupertinoAdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds
|
||||
/// the Cupertino button Widgets for the current platform given
|
||||
/// [ContextMenuButtonItem]s.
|
||||
class CupertinoAdaptiveTextSelectionToolbar extends StatelessWidget {
|
||||
/// Create an instance of [CupertinoAdaptiveTextSelectionToolbar] with the
|
||||
/// given [children].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.buttonItems}
|
||||
/// * [CupertinoAdaptiveTextSelectionToolbar.buttonItems], which takes a list
|
||||
/// of [ContextMenuButtonItem]s instead of [children] widgets.
|
||||
/// {@endtemplate}
|
||||
/// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editable}
|
||||
/// * [CupertinoAdaptiveTextSelectionToolbar.editable], which builds the
|
||||
/// default Cupertino children for an editable field.
|
||||
/// {@endtemplate}
|
||||
/// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editableText}
|
||||
/// * [CupertinoAdaptiveTextSelectionToolbar.editableText], which builds the
|
||||
/// default Cupertino children for an [EditableText].
|
||||
/// {@endtemplate}
|
||||
/// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.selectable}
|
||||
/// * [CupertinoAdaptiveTextSelectionToolbar.selectable], which builds the
|
||||
/// Cupertino children for content that is selectable but not editable.
|
||||
/// {@endtemplate}
|
||||
const CupertinoAdaptiveTextSelectionToolbar({
|
||||
super.key,
|
||||
required this.children,
|
||||
required this.anchors,
|
||||
}) : buttonItems = null;
|
||||
|
||||
/// Create an instance of [CupertinoAdaptiveTextSelectionToolbar] whose
|
||||
/// children will be built from the given [buttonItems].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// {@template flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.new}
|
||||
/// * [CupertinoAdaptiveTextSelectionToolbar.new], which takes the children
|
||||
/// directly as a list of widgets.
|
||||
/// {@endtemplate}
|
||||
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editable}
|
||||
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editableText}
|
||||
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.selectable}
|
||||
const CupertinoAdaptiveTextSelectionToolbar.buttonItems({
|
||||
super.key,
|
||||
required this.buttonItems,
|
||||
required this.anchors,
|
||||
}) : children = null;
|
||||
|
||||
/// Create an instance of [CupertinoAdaptiveTextSelectionToolbar] with the
|
||||
/// default children for an editable field.
|
||||
///
|
||||
/// If a callback is null, then its corresponding button will not be built.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [AdaptiveTextSelectionToolbar.editable], which is similar to this but
|
||||
/// includes Material and Cupertino toolbars.
|
||||
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.new}
|
||||
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editableText}
|
||||
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.buttonItems}
|
||||
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.selectable}
|
||||
CupertinoAdaptiveTextSelectionToolbar.editable({
|
||||
super.key,
|
||||
required ClipboardStatus clipboardStatus,
|
||||
required VoidCallback? onCopy,
|
||||
required VoidCallback? onCut,
|
||||
required VoidCallback? onPaste,
|
||||
required VoidCallback? onSelectAll,
|
||||
required VoidCallback? onLookUp,
|
||||
required VoidCallback? onSearchWeb,
|
||||
required VoidCallback? onShare,
|
||||
required VoidCallback? onLiveTextInput,
|
||||
required this.anchors,
|
||||
}) : children = null,
|
||||
buttonItems = EditableText.getEditableButtonItems(
|
||||
clipboardStatus: clipboardStatus,
|
||||
onCopy: onCopy,
|
||||
onCut: onCut,
|
||||
onPaste: onPaste,
|
||||
onSelectAll: onSelectAll,
|
||||
onLookUp: onLookUp,
|
||||
onSearchWeb: onSearchWeb,
|
||||
onShare: onShare,
|
||||
onLiveTextInput: onLiveTextInput,
|
||||
);
|
||||
|
||||
/// Create an instance of [CupertinoAdaptiveTextSelectionToolbar] with the
|
||||
/// default children for an [EditableText].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [AdaptiveTextSelectionToolbar.editableText], which is similar to this
|
||||
/// but includes Material and Cupertino toolbars.
|
||||
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.new}
|
||||
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editable}
|
||||
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.buttonItems}
|
||||
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.selectable}
|
||||
CupertinoAdaptiveTextSelectionToolbar.editableText({
|
||||
super.key,
|
||||
required EditableTextState editableTextState,
|
||||
}) : children = null,
|
||||
buttonItems = editableTextState.contextMenuButtonItems,
|
||||
anchors = editableTextState.contextMenuAnchors;
|
||||
|
||||
/// Create an instance of [CupertinoAdaptiveTextSelectionToolbar] with the
|
||||
/// default children for selectable, but not editable, content.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [AdaptiveTextSelectionToolbar.selectable], which is similar to this but
|
||||
/// includes Material and Cupertino toolbars.
|
||||
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.new}
|
||||
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.buttonItems}
|
||||
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editable}
|
||||
/// {@macro flutter.cupertino.CupertinoAdaptiveTextSelectionToolbar.editableText}
|
||||
CupertinoAdaptiveTextSelectionToolbar.selectable({
|
||||
super.key,
|
||||
required VoidCallback onCopy,
|
||||
required VoidCallback onSelectAll,
|
||||
required SelectionGeometry selectionGeometry,
|
||||
required this.anchors,
|
||||
}) : children = null,
|
||||
buttonItems = SelectableRegion.getSelectableButtonItems(
|
||||
selectionGeometry: selectionGeometry,
|
||||
onCopy: onCopy,
|
||||
onSelectAll: onSelectAll,
|
||||
onShare: null, // See https://github.com/flutter/flutter/issues/141775.
|
||||
);
|
||||
|
||||
/// {@macro flutter.material.AdaptiveTextSelectionToolbar.anchors}
|
||||
final TextSelectionToolbarAnchors anchors;
|
||||
|
||||
/// The children of the toolbar, typically buttons.
|
||||
final List<Widget>? children;
|
||||
|
||||
/// The [ContextMenuButtonItem]s that will be turned into the correct button
|
||||
/// widgets for the current platform.
|
||||
final List<ContextMenuButtonItem>? buttonItems;
|
||||
|
||||
/// Returns a List of Widgets generated by turning [buttonItems] into the
|
||||
/// default context menu buttons for Cupertino on the current platform.
|
||||
///
|
||||
/// This is useful when building a text selection toolbar with the default
|
||||
/// button appearance for the given platform, but where the toolbar and/or the
|
||||
/// button actions and labels may be custom.
|
||||
///
|
||||
/// Does not build Material buttons. On non-Apple platforms, Cupertino buttons
|
||||
/// will still be used, because the Cupertino library does not access the
|
||||
/// Material library. To get the native-looking buttons on every platform,
|
||||
/// use [AdaptiveTextSelectionToolbar.getAdaptiveButtons] in the Material
|
||||
/// library.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which is the Material
|
||||
/// equivalent of this class and builds only the Material buttons. It
|
||||
/// includes a live example of using `getAdaptiveButtons`.
|
||||
static Iterable<Widget> getAdaptiveButtons(
|
||||
BuildContext context,
|
||||
List<ContextMenuButtonItem> buttonItems,
|
||||
) {
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.iOS:
|
||||
return buttonItems.map((ContextMenuButtonItem buttonItem) {
|
||||
return CupertinoTextSelectionToolbarButton.buttonItem(
|
||||
buttonItem: buttonItem,
|
||||
);
|
||||
});
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
case TargetPlatform.macOS:
|
||||
return buttonItems.map((ContextMenuButtonItem buttonItem) {
|
||||
return CupertinoDesktopTextSelectionToolbarButton.buttonItem(
|
||||
buttonItem: buttonItem,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// If there aren't any buttons to build, build an empty toolbar.
|
||||
if ((children?.isEmpty ?? false) || (buttonItems?.isEmpty ?? false)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final List<Widget> resultChildren =
|
||||
children ?? getAdaptiveButtons(context, buttonItems!).toList();
|
||||
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.fuchsia:
|
||||
return CupertinoTextSelectionToolbar(
|
||||
anchorAbove: anchors.primaryAnchor,
|
||||
anchorBelow: anchors.secondaryAnchor ?? anchors.primaryAnchor,
|
||||
children: resultChildren,
|
||||
);
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
case TargetPlatform.macOS:
|
||||
return CupertinoDesktopTextSelectionToolbar(
|
||||
anchor: anchors.primaryAnchor,
|
||||
children: resultChildren,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart'
|
||||
show SelectionChangedCause, SuggestionSpan;
|
||||
|
||||
/// iOS only shows 3 spell check suggestions in the toolbar.
|
||||
const int _kMaxSuggestions = 3;
|
||||
|
||||
/// The default spell check suggestions toolbar for iOS.
|
||||
///
|
||||
/// Tries to position itself below the [anchors], but if it doesn't fit, then it
|
||||
/// readjusts to fit above bottom view insets.
|
||||
///
|
||||
/// See also:
|
||||
/// * [SpellCheckSuggestionsToolbar], which is similar but for both the
|
||||
/// Material and Cupertino libraries.
|
||||
class CupertinoSpellCheckSuggestionsToolbar extends StatelessWidget {
|
||||
/// Constructs a [CupertinoSpellCheckSuggestionsToolbar].
|
||||
///
|
||||
/// [buttonItems] must not contain more than three items.
|
||||
const CupertinoSpellCheckSuggestionsToolbar({
|
||||
super.key,
|
||||
required this.anchors,
|
||||
required this.buttonItems,
|
||||
}) : assert(buttonItems.length <= _kMaxSuggestions);
|
||||
|
||||
/// Constructs a [CupertinoSpellCheckSuggestionsToolbar] with the default
|
||||
/// children for an [EditableText].
|
||||
///
|
||||
/// See also:
|
||||
/// * [SpellCheckSuggestionsToolbar.editableText], which is similar but
|
||||
/// builds an Android-style toolbar.
|
||||
CupertinoSpellCheckSuggestionsToolbar.editableText({
|
||||
super.key,
|
||||
required EditableTextState editableTextState,
|
||||
}) : buttonItems =
|
||||
buildButtonItems(editableTextState) ?? <ContextMenuButtonItem>[],
|
||||
anchors = editableTextState.contextMenuAnchors;
|
||||
|
||||
/// The location on which to anchor the menu.
|
||||
final TextSelectionToolbarAnchors anchors;
|
||||
|
||||
/// The [ContextMenuButtonItem]s that will be turned into the correct button
|
||||
/// widgets and displayed in the spell check suggestions toolbar.
|
||||
///
|
||||
/// Must not contain more than three items.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [AdaptiveTextSelectionToolbar.buttonItems], the list of
|
||||
/// [ContextMenuButtonItem]s that are used to build the buttons of the
|
||||
/// text selection toolbar.
|
||||
/// * [SpellCheckSuggestionsToolbar.buttonItems], the list of
|
||||
/// [ContextMenuButtonItem]s used to build the Material style spell check
|
||||
/// suggestions toolbar.
|
||||
final List<ContextMenuButtonItem> buttonItems;
|
||||
|
||||
/// Builds the button items for the toolbar based on the available
|
||||
/// spell check suggestions.
|
||||
static List<ContextMenuButtonItem>? buildButtonItems(
|
||||
EditableTextState editableTextState,
|
||||
) {
|
||||
// Determine if composing region is misspelled.
|
||||
final SuggestionSpan? spanAtCursorIndex = editableTextState
|
||||
.findSuggestionSpanAtCursorIndex(
|
||||
editableTextState.currentTextEditingValue.selection.baseOffset,
|
||||
);
|
||||
|
||||
if (spanAtCursorIndex == null) {
|
||||
return null;
|
||||
}
|
||||
if (spanAtCursorIndex.suggestions.isEmpty) {
|
||||
assert(debugCheckHasCupertinoLocalizations(editableTextState.context));
|
||||
final CupertinoLocalizations localizations = CupertinoLocalizations.of(
|
||||
editableTextState.context,
|
||||
);
|
||||
return <ContextMenuButtonItem>[
|
||||
ContextMenuButtonItem(
|
||||
onPressed: null,
|
||||
label: localizations.noSpellCheckReplacementsLabel,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
final List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[];
|
||||
|
||||
// Build suggestion buttons.
|
||||
for (final String suggestion in spanAtCursorIndex.suggestions.take(
|
||||
_kMaxSuggestions,
|
||||
)) {
|
||||
buttonItems.add(
|
||||
ContextMenuButtonItem(
|
||||
onPressed: () {
|
||||
if (!editableTextState.mounted) {
|
||||
return;
|
||||
}
|
||||
_replaceText(
|
||||
editableTextState,
|
||||
suggestion,
|
||||
spanAtCursorIndex.range,
|
||||
);
|
||||
},
|
||||
label: suggestion,
|
||||
),
|
||||
);
|
||||
}
|
||||
return buttonItems;
|
||||
}
|
||||
|
||||
static void _replaceText(
|
||||
EditableTextState editableTextState,
|
||||
String text,
|
||||
TextRange replacementRange,
|
||||
) {
|
||||
// Replacement cannot be performed if the text is read only or obscured.
|
||||
assert(
|
||||
!editableTextState.widget.readOnly &&
|
||||
!editableTextState.widget.obscureText,
|
||||
);
|
||||
|
||||
final TextEditingValue newValue = editableTextState.textEditingValue
|
||||
.replaced(replacementRange, text)
|
||||
.copyWith(
|
||||
selection: TextSelection.collapsed(
|
||||
offset: replacementRange.start + text.length,
|
||||
),
|
||||
);
|
||||
editableTextState.userUpdateTextEditingValue(
|
||||
newValue,
|
||||
SelectionChangedCause.toolbar,
|
||||
);
|
||||
|
||||
// Schedule a call to bringIntoView() after renderEditable updates.
|
||||
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
|
||||
if (editableTextState.mounted) {
|
||||
editableTextState.bringIntoView(
|
||||
editableTextState.textEditingValue.selection.extent,
|
||||
);
|
||||
}
|
||||
}, debugLabel: 'SpellCheckSuggestions.bringIntoView');
|
||||
editableTextState.hideToolbar();
|
||||
}
|
||||
|
||||
/// Builds the toolbar buttons based on the [buttonItems].
|
||||
List<Widget> _buildToolbarButtons(BuildContext context) {
|
||||
return buttonItems.map((ContextMenuButtonItem buttonItem) {
|
||||
return CupertinoTextSelectionToolbarButton.buttonItem(
|
||||
buttonItem: buttonItem,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (buttonItems.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final List<Widget> children = _buildToolbarButtons(context);
|
||||
return CupertinoTextSelectionToolbar(
|
||||
anchorAbove: anchors.primaryAnchor,
|
||||
anchorBelow: anchors.secondaryAnchor == null
|
||||
? anchors.primaryAnchor
|
||||
: anchors.secondaryAnchor!,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
3402
lib/common/widgets/flutter/text_field/editable.dart
Normal file
3402
lib/common/widgets/flutter/text_field/editable.dart
Normal file
File diff suppressed because it is too large
Load Diff
6850
lib/common/widgets/flutter/text_field/editable_text.dart
Normal file
6850
lib/common/widgets/flutter/text_field/editable_text.dart
Normal file
File diff suppressed because it is too large
Load Diff
471
lib/common/widgets/flutter/text_field/spell_check.dart
Normal file
471
lib/common/widgets/flutter/text_field/spell_check.dart
Normal file
@@ -0,0 +1,471 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
/// @docImport 'editable_text.dart';
|
||||
library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter/services.dart'
|
||||
show SpellCheckResults, SpellCheckService, SuggestionSpan, TextEditingValue;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'
|
||||
show EditableTextContextMenuBuilder;
|
||||
|
||||
/// Controls how spell check is performed for text input.
|
||||
///
|
||||
/// This configuration determines the [SpellCheckService] used to fetch the
|
||||
/// [List<SuggestionSpan>] spell check results and the [TextStyle] used to
|
||||
/// mark misspelled words within text input.
|
||||
@immutable
|
||||
class SpellCheckConfiguration {
|
||||
/// Creates a configuration that specifies the service and suggestions handler
|
||||
/// for spell check.
|
||||
const SpellCheckConfiguration({
|
||||
this.spellCheckService,
|
||||
this.misspelledSelectionColor,
|
||||
this.misspelledTextStyle,
|
||||
this.spellCheckSuggestionsToolbarBuilder,
|
||||
}) : _spellCheckEnabled = true;
|
||||
|
||||
/// Creates a configuration that disables spell check.
|
||||
const SpellCheckConfiguration.disabled()
|
||||
: _spellCheckEnabled = false,
|
||||
spellCheckService = null,
|
||||
spellCheckSuggestionsToolbarBuilder = null,
|
||||
misspelledTextStyle = null,
|
||||
misspelledSelectionColor = null;
|
||||
|
||||
/// The service used to fetch spell check results for text input.
|
||||
final SpellCheckService? spellCheckService;
|
||||
|
||||
/// The color the paint the selection highlight when spell check is showing
|
||||
/// suggestions for a misspelled word.
|
||||
///
|
||||
/// For example, on iOS, the selection appears red while the spell check menu
|
||||
/// is showing.
|
||||
final Color? misspelledSelectionColor;
|
||||
|
||||
/// Style used to indicate misspelled words.
|
||||
///
|
||||
/// This is nullable to allow style-specific wrappers of [EditableText]
|
||||
/// to infer this, but this must be specified if this configuration is
|
||||
/// provided directly to [EditableText] or its construction will fail with an
|
||||
/// assertion error.
|
||||
final TextStyle? misspelledTextStyle;
|
||||
|
||||
/// Builds the toolbar used to display spell check suggestions for misspelled
|
||||
/// words.
|
||||
final EditableTextContextMenuBuilder? spellCheckSuggestionsToolbarBuilder;
|
||||
|
||||
final bool _spellCheckEnabled;
|
||||
|
||||
/// Whether or not the configuration should enable or disable spell check.
|
||||
bool get spellCheckEnabled => _spellCheckEnabled;
|
||||
|
||||
/// Returns a copy of the current [SpellCheckConfiguration] instance with
|
||||
/// specified overrides.
|
||||
SpellCheckConfiguration copyWith({
|
||||
SpellCheckService? spellCheckService,
|
||||
Color? misspelledSelectionColor,
|
||||
TextStyle? misspelledTextStyle,
|
||||
EditableTextContextMenuBuilder? spellCheckSuggestionsToolbarBuilder,
|
||||
}) {
|
||||
if (!_spellCheckEnabled) {
|
||||
// A new configuration should be constructed to enable spell check.
|
||||
return const SpellCheckConfiguration.disabled();
|
||||
}
|
||||
|
||||
return SpellCheckConfiguration(
|
||||
spellCheckService: spellCheckService ?? this.spellCheckService,
|
||||
misspelledSelectionColor:
|
||||
misspelledSelectionColor ?? this.misspelledSelectionColor,
|
||||
misspelledTextStyle: misspelledTextStyle ?? this.misspelledTextStyle,
|
||||
spellCheckSuggestionsToolbarBuilder:
|
||||
spellCheckSuggestionsToolbarBuilder ??
|
||||
this.spellCheckSuggestionsToolbarBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '${objectRuntimeType(this, 'SpellCheckConfiguration')}('
|
||||
'${_spellCheckEnabled ? 'enabled' : 'disabled'}, '
|
||||
'service: $spellCheckService, '
|
||||
'text style: $misspelledTextStyle, '
|
||||
'toolbar builder: $spellCheckSuggestionsToolbarBuilder'
|
||||
')';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
return other is SpellCheckConfiguration &&
|
||||
other.spellCheckService == spellCheckService &&
|
||||
other.misspelledTextStyle == misspelledTextStyle &&
|
||||
other.spellCheckSuggestionsToolbarBuilder ==
|
||||
spellCheckSuggestionsToolbarBuilder &&
|
||||
other._spellCheckEnabled == _spellCheckEnabled;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
spellCheckService,
|
||||
misspelledTextStyle,
|
||||
spellCheckSuggestionsToolbarBuilder,
|
||||
_spellCheckEnabled,
|
||||
);
|
||||
}
|
||||
|
||||
// Methods for displaying spell check results:
|
||||
|
||||
/// Adjusts spell check results to correspond to [newText] if the only results
|
||||
/// that the handler has access to are the [results] corresponding to
|
||||
/// [resultsText].
|
||||
///
|
||||
/// Used in the case where the request for the spell check results of the
|
||||
/// [newText] is lagging in order to avoid display of incorrect results.
|
||||
List<SuggestionSpan> _correctSpellCheckResults(
|
||||
String newText,
|
||||
String resultsText,
|
||||
List<SuggestionSpan> results,
|
||||
) {
|
||||
final List<SuggestionSpan> correctedSpellCheckResults = <SuggestionSpan>[];
|
||||
int spanPointer = 0;
|
||||
int offset = 0;
|
||||
|
||||
// Assumes that the order of spans has not been jumbled for optimization
|
||||
// purposes, and will only search since the previously found span.
|
||||
int searchStart = 0;
|
||||
|
||||
while (spanPointer < results.length) {
|
||||
final SuggestionSpan currentSpan = results[spanPointer];
|
||||
final String currentSpanText = resultsText.substring(
|
||||
currentSpan.range.start,
|
||||
currentSpan.range.end,
|
||||
);
|
||||
final int spanLength = currentSpan.range.end - currentSpan.range.start;
|
||||
|
||||
// Try finding SuggestionSpan from resultsText in new text.
|
||||
final String escapedText = RegExp.escape(currentSpanText);
|
||||
final RegExp currentSpanTextRegexp = RegExp('\\b$escapedText\\b');
|
||||
final int foundIndex = newText
|
||||
.substring(searchStart)
|
||||
.indexOf(currentSpanTextRegexp);
|
||||
|
||||
// Check whether word was found exactly where expected or elsewhere in the newText.
|
||||
final bool currentSpanFoundExactly =
|
||||
currentSpan.range.start == foundIndex + searchStart;
|
||||
final bool currentSpanFoundExactlyWithOffset =
|
||||
currentSpan.range.start + offset == foundIndex + searchStart;
|
||||
final bool currentSpanFoundElsewhere = foundIndex >= 0;
|
||||
|
||||
if (currentSpanFoundExactly || currentSpanFoundExactlyWithOffset) {
|
||||
// currentSpan was found at the same index in newText and resultsText
|
||||
// or at the same index with the previously calculated adjustment by
|
||||
// the offset value, so apply it to new text by adding it to the list of
|
||||
// corrected results.
|
||||
final SuggestionSpan adjustedSpan = SuggestionSpan(
|
||||
TextRange(
|
||||
start: currentSpan.range.start + offset,
|
||||
end: currentSpan.range.end + offset,
|
||||
),
|
||||
currentSpan.suggestions,
|
||||
);
|
||||
|
||||
// Start search for the next misspelled word at the end of currentSpan.
|
||||
searchStart = math.min(
|
||||
currentSpan.range.end + 1 + offset,
|
||||
newText.length,
|
||||
);
|
||||
correctedSpellCheckResults.add(adjustedSpan);
|
||||
} else if (currentSpanFoundElsewhere) {
|
||||
// Word was pushed forward but not modified.
|
||||
final int adjustedSpanStart = searchStart + foundIndex;
|
||||
final int adjustedSpanEnd = adjustedSpanStart + spanLength;
|
||||
final SuggestionSpan adjustedSpan = SuggestionSpan(
|
||||
TextRange(start: adjustedSpanStart, end: adjustedSpanEnd),
|
||||
currentSpan.suggestions,
|
||||
);
|
||||
|
||||
// Start search for the next misspelled word at the end of the
|
||||
// adjusted currentSpan.
|
||||
searchStart = math.min(adjustedSpanEnd + 1, newText.length);
|
||||
// Adjust offset to reflect the difference between where currentSpan
|
||||
// was positioned in resultsText versus in newText.
|
||||
offset = adjustedSpanStart - currentSpan.range.start;
|
||||
correctedSpellCheckResults.add(adjustedSpan);
|
||||
}
|
||||
spanPointer++;
|
||||
}
|
||||
return correctedSpellCheckResults;
|
||||
}
|
||||
|
||||
/// Builds the [TextSpan] tree given the current state of the text input and
|
||||
/// spell check results.
|
||||
///
|
||||
/// The [value] is the current [TextEditingValue] requested to be rendered
|
||||
/// by a text input widget. The [composingWithinCurrentTextRange] value
|
||||
/// represents whether or not there is a valid composing region in the
|
||||
/// [value]. The [style] is the [TextStyle] to render the [value]'s text with,
|
||||
/// and the [misspelledTextStyle] is the [TextStyle] to render misspelled
|
||||
/// words within the [value]'s text with. The [spellCheckResults] are the
|
||||
/// results of spell checking the [value]'s text.
|
||||
TextSpan buildTextSpanWithSpellCheckSuggestions(
|
||||
TextEditingValue value,
|
||||
bool composingWithinCurrentTextRange,
|
||||
TextStyle? style,
|
||||
TextStyle misspelledTextStyle,
|
||||
SpellCheckResults spellCheckResults,
|
||||
) {
|
||||
List<SuggestionSpan> spellCheckResultsSpans =
|
||||
spellCheckResults.suggestionSpans;
|
||||
final String spellCheckResultsText = spellCheckResults.spellCheckedText;
|
||||
|
||||
if (spellCheckResultsText != value.text) {
|
||||
spellCheckResultsSpans = _correctSpellCheckResults(
|
||||
value.text,
|
||||
spellCheckResultsText,
|
||||
spellCheckResultsSpans,
|
||||
);
|
||||
}
|
||||
|
||||
// We will draw the TextSpan tree based on the composing region, if it is
|
||||
// available.
|
||||
// TODO(camsim99): The two separate strategies for building TextSpan trees
|
||||
// based on the availability of a composing region should be merged:
|
||||
// https://github.com/flutter/flutter/issues/124142.
|
||||
final bool shouldConsiderComposingRegion =
|
||||
defaultTargetPlatform == TargetPlatform.android;
|
||||
if (shouldConsiderComposingRegion) {
|
||||
return TextSpan(
|
||||
style: style,
|
||||
children: _buildSubtreesWithComposingRegion(
|
||||
spellCheckResultsSpans,
|
||||
value,
|
||||
style,
|
||||
misspelledTextStyle,
|
||||
composingWithinCurrentTextRange,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return TextSpan(
|
||||
style: style,
|
||||
children: _buildSubtreesWithoutComposingRegion(
|
||||
spellCheckResultsSpans,
|
||||
value,
|
||||
style,
|
||||
misspelledTextStyle,
|
||||
value.selection.baseOffset,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds the [TextSpan] tree for spell check without considering the composing
|
||||
/// region. Instead, uses the cursor to identify the word that's actively being
|
||||
/// edited and shouldn't be spell checked. This is useful for platforms and IMEs
|
||||
/// that don't use the composing region for the active word.
|
||||
List<TextSpan> _buildSubtreesWithoutComposingRegion(
|
||||
List<SuggestionSpan>? spellCheckSuggestions,
|
||||
TextEditingValue value,
|
||||
TextStyle? style,
|
||||
TextStyle misspelledStyle,
|
||||
int cursorIndex,
|
||||
) {
|
||||
final List<TextSpan> textSpanTreeChildren = <TextSpan>[];
|
||||
|
||||
int textPointer = 0;
|
||||
int currentSpanPointer = 0;
|
||||
int endIndex;
|
||||
final String text = value.text;
|
||||
final TextStyle misspelledJointStyle =
|
||||
style?.merge(misspelledStyle) ?? misspelledStyle;
|
||||
bool cursorInCurrentSpan = false;
|
||||
|
||||
// Add text interwoven with any misspelled words to the tree.
|
||||
if (spellCheckSuggestions != null) {
|
||||
while (textPointer < text.length &&
|
||||
currentSpanPointer < spellCheckSuggestions.length) {
|
||||
final SuggestionSpan currentSpan =
|
||||
spellCheckSuggestions[currentSpanPointer];
|
||||
|
||||
if (currentSpan.range.start > textPointer) {
|
||||
endIndex = currentSpan.range.start < text.length
|
||||
? currentSpan.range.start
|
||||
: text.length;
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(style: style, text: text.substring(textPointer, endIndex)),
|
||||
);
|
||||
textPointer = endIndex;
|
||||
} else {
|
||||
endIndex = currentSpan.range.end < text.length
|
||||
? currentSpan.range.end
|
||||
: text.length;
|
||||
cursorInCurrentSpan =
|
||||
currentSpan.range.start <= cursorIndex &&
|
||||
currentSpan.range.end >= cursorIndex;
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(
|
||||
style: cursorInCurrentSpan ? style : misspelledJointStyle,
|
||||
text: text.substring(currentSpan.range.start, endIndex),
|
||||
),
|
||||
);
|
||||
|
||||
textPointer = endIndex;
|
||||
currentSpanPointer++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining text to the tree if applicable.
|
||||
if (textPointer < text.length) {
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(style: style, text: text.substring(textPointer, text.length)),
|
||||
);
|
||||
}
|
||||
|
||||
return textSpanTreeChildren;
|
||||
}
|
||||
|
||||
/// Builds [TextSpan] subtree for text with misspelled words with logic based on
|
||||
/// a valid composing region.
|
||||
List<TextSpan> _buildSubtreesWithComposingRegion(
|
||||
List<SuggestionSpan>? spellCheckSuggestions,
|
||||
TextEditingValue value,
|
||||
TextStyle? style,
|
||||
TextStyle misspelledStyle,
|
||||
bool composingWithinCurrentTextRange,
|
||||
) {
|
||||
final List<TextSpan> textSpanTreeChildren = <TextSpan>[];
|
||||
|
||||
int textPointer = 0;
|
||||
int currentSpanPointer = 0;
|
||||
int endIndex;
|
||||
SuggestionSpan currentSpan;
|
||||
final String text = value.text;
|
||||
final TextRange composingRegion = value.composing;
|
||||
final TextStyle composingTextStyle =
|
||||
style?.merge(const TextStyle(decoration: TextDecoration.underline)) ??
|
||||
const TextStyle(decoration: TextDecoration.underline);
|
||||
final TextStyle misspelledJointStyle =
|
||||
style?.merge(misspelledStyle) ?? misspelledStyle;
|
||||
bool textPointerWithinComposingRegion = false;
|
||||
bool currentSpanIsComposingRegion = false;
|
||||
|
||||
// Add text interwoven with any misspelled words to the tree.
|
||||
if (spellCheckSuggestions != null) {
|
||||
while (textPointer < text.length &&
|
||||
currentSpanPointer < spellCheckSuggestions.length) {
|
||||
currentSpan = spellCheckSuggestions[currentSpanPointer];
|
||||
|
||||
if (currentSpan.range.start > textPointer) {
|
||||
endIndex = currentSpan.range.start < text.length
|
||||
? currentSpan.range.start
|
||||
: text.length;
|
||||
textPointerWithinComposingRegion =
|
||||
composingRegion.start >= textPointer &&
|
||||
composingRegion.end <= endIndex &&
|
||||
!composingWithinCurrentTextRange;
|
||||
|
||||
if (textPointerWithinComposingRegion) {
|
||||
_addComposingRegionTextSpans(
|
||||
textSpanTreeChildren,
|
||||
text,
|
||||
textPointer,
|
||||
composingRegion,
|
||||
style,
|
||||
composingTextStyle,
|
||||
);
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(
|
||||
style: style,
|
||||
text: text.substring(composingRegion.end, endIndex),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(style: style, text: text.substring(textPointer, endIndex)),
|
||||
);
|
||||
}
|
||||
|
||||
textPointer = endIndex;
|
||||
} else {
|
||||
endIndex = currentSpan.range.end < text.length
|
||||
? currentSpan.range.end
|
||||
: text.length;
|
||||
currentSpanIsComposingRegion =
|
||||
textPointer >= composingRegion.start &&
|
||||
endIndex <= composingRegion.end &&
|
||||
!composingWithinCurrentTextRange;
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(
|
||||
style: currentSpanIsComposingRegion
|
||||
? composingTextStyle
|
||||
: misspelledJointStyle,
|
||||
text: text.substring(currentSpan.range.start, endIndex),
|
||||
),
|
||||
);
|
||||
|
||||
textPointer = endIndex;
|
||||
currentSpanPointer++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining text to the tree if applicable.
|
||||
if (textPointer < text.length) {
|
||||
if (textPointer < composingRegion.start &&
|
||||
!composingWithinCurrentTextRange) {
|
||||
_addComposingRegionTextSpans(
|
||||
textSpanTreeChildren,
|
||||
text,
|
||||
textPointer,
|
||||
composingRegion,
|
||||
style,
|
||||
composingTextStyle,
|
||||
);
|
||||
|
||||
if (composingRegion.end != text.length) {
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(
|
||||
style: style,
|
||||
text: text.substring(composingRegion.end, text.length),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
textSpanTreeChildren.add(
|
||||
TextSpan(style: style, text: text.substring(textPointer, text.length)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return textSpanTreeChildren;
|
||||
}
|
||||
|
||||
/// Helper method to create [TextSpan] tree children for specified range of
|
||||
/// text up to and including the composing region.
|
||||
void _addComposingRegionTextSpans(
|
||||
List<TextSpan> treeChildren,
|
||||
String text,
|
||||
int start,
|
||||
TextRange composingRegion,
|
||||
TextStyle? style,
|
||||
TextStyle composingTextStyle,
|
||||
) {
|
||||
treeChildren.add(
|
||||
TextSpan(style: style, text: text.substring(start, composingRegion.start)),
|
||||
);
|
||||
treeChildren.add(
|
||||
TextSpan(
|
||||
style: composingTextStyle,
|
||||
text: text.substring(composingRegion.start, composingRegion.end),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/material.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart'
|
||||
show SelectionChangedCause, SuggestionSpan;
|
||||
|
||||
// The default height of the SpellCheckSuggestionsToolbar, which
|
||||
// assumes there are the maximum number of spell check suggestions available, 3.
|
||||
// Size eyeballed on Pixel 4 emulator running Android API 31.
|
||||
const double _kDefaultToolbarHeight = 193.0;
|
||||
|
||||
/// The maximum number of suggestions in the toolbar is 3, plus a delete button.
|
||||
const int _kMaxSuggestions = 3;
|
||||
|
||||
/// The default spell check suggestions toolbar for Android.
|
||||
///
|
||||
/// Tries to position itself below the [anchor], but if it doesn't fit, then it
|
||||
/// readjusts to fit above bottom view insets.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CupertinoSpellCheckSuggestionsToolbar], which is similar but builds an
|
||||
/// iOS-style spell check toolbar.
|
||||
class SpellCheckSuggestionsToolbar extends StatelessWidget {
|
||||
/// Constructs a [SpellCheckSuggestionsToolbar].
|
||||
///
|
||||
/// [buttonItems] must not contain more than four items, generally three
|
||||
/// suggestions and one delete button.
|
||||
const SpellCheckSuggestionsToolbar({
|
||||
super.key,
|
||||
required this.anchor,
|
||||
required this.buttonItems,
|
||||
}) : assert(buttonItems.length <= _kMaxSuggestions + 1);
|
||||
|
||||
/// Constructs a [SpellCheckSuggestionsToolbar] with the default children for
|
||||
/// an [EditableText].
|
||||
///
|
||||
/// See also:
|
||||
/// * [CupertinoSpellCheckSuggestionsToolbar.editableText], which is similar
|
||||
/// but builds an iOS-style toolbar.
|
||||
SpellCheckSuggestionsToolbar.editableText({
|
||||
super.key,
|
||||
required EditableTextState editableTextState,
|
||||
}) : buttonItems =
|
||||
buildButtonItems(editableTextState) ?? <ContextMenuButtonItem>[],
|
||||
anchor = getToolbarAnchor(editableTextState.contextMenuAnchors);
|
||||
|
||||
/// {@template flutter.material.SpellCheckSuggestionsToolbar.anchor}
|
||||
/// The focal point below which the toolbar attempts to position itself.
|
||||
/// {@endtemplate}
|
||||
final Offset anchor;
|
||||
|
||||
/// The [ContextMenuButtonItem]s that will be turned into the correct button
|
||||
/// widgets and displayed in the spell check suggestions toolbar.
|
||||
///
|
||||
/// Must not contain more than four items, typically three suggestions and a
|
||||
/// delete button.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [AdaptiveTextSelectionToolbar.buttonItems], the list of
|
||||
/// [ContextMenuButtonItem]s that are used to build the buttons of the
|
||||
/// text selection toolbar.
|
||||
/// * [CupertinoSpellCheckSuggestionsToolbar.buttonItems], the list of
|
||||
/// [ContextMenuButtonItem]s used to build the Cupertino style spell check
|
||||
/// suggestions toolbar.
|
||||
final List<ContextMenuButtonItem> buttonItems;
|
||||
|
||||
/// Builds the button items for the toolbar based on the available
|
||||
/// spell check suggestions.
|
||||
static List<ContextMenuButtonItem>? buildButtonItems(
|
||||
EditableTextState editableTextState,
|
||||
) {
|
||||
// Determine if composing region is misspelled.
|
||||
final SuggestionSpan? spanAtCursorIndex = editableTextState
|
||||
.findSuggestionSpanAtCursorIndex(
|
||||
editableTextState.currentTextEditingValue.selection.baseOffset,
|
||||
);
|
||||
|
||||
if (spanAtCursorIndex == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[];
|
||||
|
||||
// Build suggestion buttons.
|
||||
for (final String suggestion in spanAtCursorIndex.suggestions.take(
|
||||
_kMaxSuggestions,
|
||||
)) {
|
||||
buttonItems.add(
|
||||
ContextMenuButtonItem(
|
||||
onPressed: () {
|
||||
if (!editableTextState.mounted) {
|
||||
return;
|
||||
}
|
||||
_replaceText(
|
||||
editableTextState,
|
||||
suggestion,
|
||||
spanAtCursorIndex.range,
|
||||
);
|
||||
},
|
||||
label: suggestion,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Build delete button.
|
||||
final ContextMenuButtonItem deleteButton = ContextMenuButtonItem(
|
||||
onPressed: () {
|
||||
if (!editableTextState.mounted) {
|
||||
return;
|
||||
}
|
||||
_replaceText(
|
||||
editableTextState,
|
||||
'',
|
||||
editableTextState.currentTextEditingValue.composing,
|
||||
);
|
||||
},
|
||||
type: ContextMenuButtonType.delete,
|
||||
);
|
||||
buttonItems.add(deleteButton);
|
||||
|
||||
return buttonItems;
|
||||
}
|
||||
|
||||
static void _replaceText(
|
||||
EditableTextState editableTextState,
|
||||
String text,
|
||||
TextRange replacementRange,
|
||||
) {
|
||||
// Replacement cannot be performed if the text is read only or obscured.
|
||||
assert(
|
||||
!editableTextState.widget.readOnly &&
|
||||
!editableTextState.widget.obscureText,
|
||||
);
|
||||
|
||||
final TextEditingValue newValue = editableTextState.textEditingValue
|
||||
.replaced(
|
||||
replacementRange,
|
||||
text,
|
||||
);
|
||||
editableTextState.userUpdateTextEditingValue(
|
||||
newValue,
|
||||
SelectionChangedCause.toolbar,
|
||||
);
|
||||
|
||||
// Schedule a call to bringIntoView() after renderEditable updates.
|
||||
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
|
||||
if (editableTextState.mounted) {
|
||||
editableTextState.bringIntoView(
|
||||
editableTextState.textEditingValue.selection.extent,
|
||||
);
|
||||
}
|
||||
}, debugLabel: 'SpellCheckerSuggestionsToolbar.bringIntoView');
|
||||
editableTextState.hideToolbar();
|
||||
}
|
||||
|
||||
/// Determines the Offset that the toolbar will be anchored to.
|
||||
static Offset getToolbarAnchor(TextSelectionToolbarAnchors anchors) {
|
||||
// Since this will be positioned below the anchor point, use the secondary
|
||||
// anchor by default.
|
||||
return anchors.secondaryAnchor == null
|
||||
? anchors.primaryAnchor
|
||||
: anchors.secondaryAnchor!;
|
||||
}
|
||||
|
||||
/// Builds the toolbar buttons based on the [buttonItems].
|
||||
List<Widget> _buildToolbarButtons(BuildContext context) {
|
||||
return buttonItems.map((ContextMenuButtonItem buttonItem) {
|
||||
final TextSelectionToolbarTextButton button =
|
||||
TextSelectionToolbarTextButton(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 0, 0),
|
||||
onPressed: buttonItem.onPressed,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
AdaptiveTextSelectionToolbar.getButtonLabel(context, buttonItem),
|
||||
style: buttonItem.type == ContextMenuButtonType.delete
|
||||
? const TextStyle(color: Colors.blue)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
|
||||
if (buttonItem.type != ContextMenuButtonType.delete) {
|
||||
return button;
|
||||
}
|
||||
return DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(top: BorderSide(color: Colors.grey)),
|
||||
),
|
||||
child: button,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (buttonItems.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
// Adjust toolbar height if needed.
|
||||
final double spellCheckSuggestionsToolbarHeight =
|
||||
_kDefaultToolbarHeight - (48.0 * (4 - buttonItems.length));
|
||||
// Incorporate the padding distance between the content and toolbar.
|
||||
final MediaQueryData mediaQueryData = MediaQuery.of(context);
|
||||
final double softKeyboardViewInsetsBottom =
|
||||
mediaQueryData.viewInsets.bottom;
|
||||
final double paddingAbove =
|
||||
mediaQueryData.padding.top +
|
||||
CupertinoTextSelectionToolbar.kToolbarScreenPadding;
|
||||
// Makes up for the Padding.
|
||||
final Offset localAdjustment = Offset(
|
||||
CupertinoTextSelectionToolbar.kToolbarScreenPadding,
|
||||
paddingAbove,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
CupertinoTextSelectionToolbar.kToolbarScreenPadding,
|
||||
paddingAbove,
|
||||
CupertinoTextSelectionToolbar.kToolbarScreenPadding,
|
||||
CupertinoTextSelectionToolbar.kToolbarScreenPadding +
|
||||
softKeyboardViewInsetsBottom,
|
||||
),
|
||||
child: CustomSingleChildLayout(
|
||||
delegate: SpellCheckSuggestionsToolbarLayoutDelegate(
|
||||
anchor: anchor - localAdjustment,
|
||||
),
|
||||
child: AnimatedSize(
|
||||
// This duration was eyeballed on a Pixel 2 emulator running Android
|
||||
// API 28 for the Material TextSelectionToolbar.
|
||||
duration: const Duration(milliseconds: 140),
|
||||
child: _SpellCheckSuggestionsToolbarContainer(
|
||||
height: spellCheckSuggestionsToolbarHeight,
|
||||
children: <Widget>[..._buildToolbarButtons(context)],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The Material-styled toolbar outline for the spell check suggestions
|
||||
/// toolbar.
|
||||
class _SpellCheckSuggestionsToolbarContainer extends StatelessWidget {
|
||||
const _SpellCheckSuggestionsToolbarContainer({
|
||||
required this.height,
|
||||
required this.children,
|
||||
});
|
||||
|
||||
final double height;
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
// This elevation was eyeballed on a Pixel 4 emulator running Android
|
||||
// API 31 for the SpellCheckSuggestionsToolbar.
|
||||
elevation: 2.0,
|
||||
type: MaterialType.card,
|
||||
child: SizedBox(
|
||||
// This width was eyeballed on a Pixel 4 emulator running Android
|
||||
// API 31 for the SpellCheckSuggestionsToolbar.
|
||||
width: 165.0,
|
||||
height: height,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
435
lib/common/widgets/flutter/text_field/system_context_menu.dart
Normal file
435
lib/common/widgets/flutter/text_field/system_context_menu.dart
Normal file
@@ -0,0 +1,435 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
/// @docImport 'package:flutter/material.dart';
|
||||
library;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart';
|
||||
import 'package:flutter/material.dart' hide EditableText, EditableTextState;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Displays the system context menu on top of the Flutter view.
|
||||
///
|
||||
/// Currently, only supports iOS 16.0 and above and displays nothing on other
|
||||
/// platforms.
|
||||
///
|
||||
/// The context menu is the menu that appears, for example, when doing text
|
||||
/// selection. Flutter typically draws this menu itself, but this class deals
|
||||
/// with the platform-rendered context menu instead.
|
||||
///
|
||||
/// There can only be one system context menu visible at a time. Building this
|
||||
/// widget when the system context menu is already visible will hide the old one
|
||||
/// and display this one. A system context menu that is hidden is informed via
|
||||
/// [onSystemHide].
|
||||
///
|
||||
/// Pass [items] to specify the buttons that will appear in the menu. Any items
|
||||
/// without a title will be given a default title from [WidgetsLocalizations].
|
||||
///
|
||||
/// By default, [items] will be set to the result of [getDefaultItems]. This
|
||||
/// method considers the state of the [EditableTextState] so that, for example,
|
||||
/// it will only include [IOSSystemContextMenuItemCopy] if there is currently a
|
||||
/// selection to copy.
|
||||
///
|
||||
/// To check if the current device supports showing the system context menu,
|
||||
/// call [isSupported].
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows how to create a [TextField] that uses the system context
|
||||
/// menu where supported and does not show a system notification when the user
|
||||
/// presses the "Paste" button.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/system_context_menu/system_context_menu.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenuController], which directly controls the hiding and
|
||||
/// showing of the system context menu.
|
||||
class SystemContextMenu extends StatefulWidget {
|
||||
/// Creates an instance of [SystemContextMenu] that points to the given
|
||||
/// [anchor].
|
||||
const SystemContextMenu._({
|
||||
super.key,
|
||||
required this.anchor,
|
||||
required this.items,
|
||||
this.onSystemHide,
|
||||
});
|
||||
|
||||
/// Creates an instance of [SystemContextMenu] for the field indicated by the
|
||||
/// given [EditableTextState].
|
||||
factory SystemContextMenu.editableText({
|
||||
Key? key,
|
||||
required EditableTextState editableTextState,
|
||||
List<IOSSystemContextMenuItem>? items,
|
||||
}) {
|
||||
final (
|
||||
startGlyphHeight: double startGlyphHeight,
|
||||
endGlyphHeight: double endGlyphHeight,
|
||||
) = editableTextState
|
||||
.getGlyphHeights();
|
||||
|
||||
return SystemContextMenu._(
|
||||
key: key,
|
||||
anchor: TextSelectionToolbarAnchors.getSelectionRect(
|
||||
editableTextState.renderEditable,
|
||||
startGlyphHeight,
|
||||
endGlyphHeight,
|
||||
editableTextState.renderEditable.getEndpointsForSelection(
|
||||
editableTextState.textEditingValue.selection,
|
||||
),
|
||||
),
|
||||
items: items ?? getDefaultItems(editableTextState),
|
||||
onSystemHide: editableTextState.hideToolbar,
|
||||
);
|
||||
}
|
||||
|
||||
/// The [Rect] that the context menu should point to.
|
||||
final Rect anchor;
|
||||
|
||||
/// A list of the items to be displayed in the system context menu.
|
||||
///
|
||||
/// When passed, items will be shown regardless of the state of text input.
|
||||
/// For example, [IOSSystemContextMenuItemCopy] will produce a copy button
|
||||
/// even when there is no selection to copy. Use [EditableTextState] and/or
|
||||
/// the result of [getDefaultItems] to add and remove items based on the state
|
||||
/// of the input.
|
||||
///
|
||||
/// Defaults to the result of [getDefaultItems].
|
||||
final List<IOSSystemContextMenuItem> items;
|
||||
|
||||
/// Called when the system hides this context menu.
|
||||
///
|
||||
/// For example, tapping outside of the context menu typically causes the
|
||||
/// system to hide the menu.
|
||||
///
|
||||
/// This is not called when showing a new system context menu causes another
|
||||
/// to be hidden.
|
||||
final VoidCallback? onSystemHide;
|
||||
|
||||
/// Whether the current device supports showing the system context menu.
|
||||
///
|
||||
/// Currently, this is only supported on newer versions of iOS.
|
||||
static bool isSupported(BuildContext context) {
|
||||
return MediaQuery.maybeSupportsShowingSystemContextMenu(context) ?? false;
|
||||
}
|
||||
|
||||
/// The default [items] for the given [EditableTextState].
|
||||
///
|
||||
/// For example, [IOSSystemContextMenuItemCopy] will only be included when the
|
||||
/// field represented by the [EditableTextState] has a selection.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [EditableTextState.contextMenuButtonItems], which provides the default
|
||||
/// [ContextMenuButtonItem]s for the Flutter-rendered context menu.
|
||||
static List<IOSSystemContextMenuItem> getDefaultItems(
|
||||
EditableTextState editableTextState,
|
||||
) {
|
||||
return <IOSSystemContextMenuItem>[
|
||||
if (editableTextState.copyEnabled) const IOSSystemContextMenuItemCopy(),
|
||||
if (editableTextState.cutEnabled) const IOSSystemContextMenuItemCut(),
|
||||
if (editableTextState.pasteEnabled) const IOSSystemContextMenuItemPaste(),
|
||||
if (editableTextState.selectAllEnabled)
|
||||
const IOSSystemContextMenuItemSelectAll(),
|
||||
if (editableTextState.lookUpEnabled)
|
||||
const IOSSystemContextMenuItemLookUp(),
|
||||
if (editableTextState.searchWebEnabled)
|
||||
const IOSSystemContextMenuItemSearchWeb(),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
State<SystemContextMenu> createState() => _SystemContextMenuState();
|
||||
}
|
||||
|
||||
class _SystemContextMenuState extends State<SystemContextMenu> {
|
||||
late final SystemContextMenuController _systemContextMenuController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_systemContextMenuController = SystemContextMenuController(
|
||||
onSystemHide: widget.onSystemHide,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_systemContextMenuController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(SystemContextMenu.isSupported(context));
|
||||
|
||||
if (widget.items.isNotEmpty) {
|
||||
final WidgetsLocalizations localizations = WidgetsLocalizations.of(
|
||||
context,
|
||||
);
|
||||
final List<IOSSystemContextMenuItemData> itemDatas = widget.items
|
||||
.map((IOSSystemContextMenuItem item) => item.getData(localizations))
|
||||
.toList();
|
||||
_systemContextMenuController.showWithItems(widget.anchor, itemDatas);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a context menu button that will be rendered in the iOS system
|
||||
/// context menu and not by Flutter itself.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemData], which performs a similar role but at the
|
||||
/// method channel level and mirrors the requirements of the method channel
|
||||
/// API.
|
||||
/// * [ContextMenuButtonItem], which performs a similar role for Flutter-drawn
|
||||
/// context menus.
|
||||
@immutable
|
||||
sealed class IOSSystemContextMenuItem {
|
||||
const IOSSystemContextMenuItem();
|
||||
|
||||
/// The text to display to the user.
|
||||
///
|
||||
/// Not exposed for some built-in menu items whose title is always set by the
|
||||
/// platform.
|
||||
String? get title => null;
|
||||
|
||||
/// Returns the representation of this class used by method channels.
|
||||
IOSSystemContextMenuItemData getData(WidgetsLocalizations localizations);
|
||||
|
||||
@override
|
||||
int get hashCode => title.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
return other is IOSSystemContextMenuItem && other.title == title;
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in
|
||||
/// copy button.
|
||||
///
|
||||
/// Should only appear when there is a selection that can be copied.
|
||||
///
|
||||
/// The title and action are both handled by the platform.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemDataCopy], which specifies the data to be sent to
|
||||
/// the platform for this same button.
|
||||
final class IOSSystemContextMenuItemCopy extends IOSSystemContextMenuItem {
|
||||
/// Creates an instance of [IOSSystemContextMenuItemCopy].
|
||||
const IOSSystemContextMenuItemCopy();
|
||||
|
||||
@override
|
||||
IOSSystemContextMenuItemDataCopy getData(WidgetsLocalizations localizations) {
|
||||
return const IOSSystemContextMenuItemDataCopy();
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in
|
||||
/// cut button.
|
||||
///
|
||||
/// Should only appear when there is a selection that can be cut.
|
||||
///
|
||||
/// The title and action are both handled by the platform.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemDataCut], which specifies the data to be sent to
|
||||
/// the platform for this same button.
|
||||
final class IOSSystemContextMenuItemCut extends IOSSystemContextMenuItem {
|
||||
/// Creates an instance of [IOSSystemContextMenuItemCut].
|
||||
const IOSSystemContextMenuItemCut();
|
||||
|
||||
@override
|
||||
IOSSystemContextMenuItemDataCut getData(WidgetsLocalizations localizations) {
|
||||
return const IOSSystemContextMenuItemDataCut();
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in
|
||||
/// paste button.
|
||||
///
|
||||
/// Should only appear when the field can receive pasted content.
|
||||
///
|
||||
/// The title and action are both handled by the platform.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemDataPaste], which specifies the data to be sent
|
||||
/// to the platform for this same button.
|
||||
final class IOSSystemContextMenuItemPaste extends IOSSystemContextMenuItem {
|
||||
/// Creates an instance of [IOSSystemContextMenuItemPaste].
|
||||
const IOSSystemContextMenuItemPaste();
|
||||
|
||||
@override
|
||||
IOSSystemContextMenuItemDataPaste getData(
|
||||
WidgetsLocalizations localizations,
|
||||
) {
|
||||
return const IOSSystemContextMenuItemDataPaste();
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an instance of [IOSSystemContextMenuItem] for the system's built-in
|
||||
/// select all button.
|
||||
///
|
||||
/// Should only appear when the field can have its selection changed.
|
||||
///
|
||||
/// The title and action are both handled by the platform.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemDataSelectAll], which specifies the data to be
|
||||
/// sent to the platform for this same button.
|
||||
final class IOSSystemContextMenuItemSelectAll extends IOSSystemContextMenuItem {
|
||||
/// Creates an instance of [IOSSystemContextMenuItemSelectAll].
|
||||
const IOSSystemContextMenuItemSelectAll();
|
||||
|
||||
@override
|
||||
IOSSystemContextMenuItemDataSelectAll getData(
|
||||
WidgetsLocalizations localizations,
|
||||
) {
|
||||
return const IOSSystemContextMenuItemDataSelectAll();
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an instance of [IOSSystemContextMenuItem] for the
|
||||
/// system's built-in look up button.
|
||||
///
|
||||
/// Should only appear when content is selected.
|
||||
///
|
||||
/// The [title] is optional, but it must be specified before being sent to the
|
||||
/// platform. Typically it should be set to
|
||||
/// [WidgetsLocalizations.lookUpButtonLabel].
|
||||
///
|
||||
/// The action is handled by the platform.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemDataLookUp], which specifies the data to be sent
|
||||
/// to the platform for this same button.
|
||||
final class IOSSystemContextMenuItemLookUp extends IOSSystemContextMenuItem {
|
||||
/// Creates an instance of [IOSSystemContextMenuItemLookUp].
|
||||
const IOSSystemContextMenuItemLookUp({this.title});
|
||||
|
||||
@override
|
||||
final String? title;
|
||||
|
||||
@override
|
||||
IOSSystemContextMenuItemDataLookUp getData(
|
||||
WidgetsLocalizations localizations,
|
||||
) {
|
||||
return IOSSystemContextMenuItemDataLookUp(
|
||||
title: title ?? localizations.lookUpButtonLabel,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'IOSSystemContextMenuItemLookUp(title: $title)';
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an instance of [IOSSystemContextMenuItem] for the
|
||||
/// system's built-in search web button.
|
||||
///
|
||||
/// Should only appear when content is selected.
|
||||
///
|
||||
/// The [title] is optional, but it must be specified before being sent to the
|
||||
/// platform. Typically it should be set to
|
||||
/// [WidgetsLocalizations.searchWebButtonLabel].
|
||||
///
|
||||
/// The action is handled by the platform.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemDataSearchWeb], which specifies the data to be
|
||||
/// sent to the platform for this same button.
|
||||
final class IOSSystemContextMenuItemSearchWeb extends IOSSystemContextMenuItem {
|
||||
/// Creates an instance of [IOSSystemContextMenuItemSearchWeb].
|
||||
const IOSSystemContextMenuItemSearchWeb({this.title});
|
||||
|
||||
@override
|
||||
final String? title;
|
||||
|
||||
@override
|
||||
IOSSystemContextMenuItemDataSearchWeb getData(
|
||||
WidgetsLocalizations localizations,
|
||||
) {
|
||||
return IOSSystemContextMenuItemDataSearchWeb(
|
||||
title: title ?? localizations.searchWebButtonLabel,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'IOSSystemContextMenuItemSearchWeb(title: $title)';
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an instance of [IOSSystemContextMenuItem] for the
|
||||
/// system's built-in share button.
|
||||
///
|
||||
/// Opens the system share dialog.
|
||||
///
|
||||
/// Should only appear when shareable content is selected.
|
||||
///
|
||||
/// The [title] is optional, but it must be specified before being sent to the
|
||||
/// platform. Typically it should be set to
|
||||
/// [WidgetsLocalizations.shareButtonLabel].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SystemContextMenu], a widget that can be used to display the system
|
||||
/// context menu.
|
||||
/// * [IOSSystemContextMenuItemDataShare], which specifies the data to be sent
|
||||
/// to the platform for this same button.
|
||||
final class IOSSystemContextMenuItemShare extends IOSSystemContextMenuItem {
|
||||
/// Creates an instance of [IOSSystemContextMenuItemShare].
|
||||
const IOSSystemContextMenuItemShare({this.title});
|
||||
|
||||
@override
|
||||
final String? title;
|
||||
|
||||
@override
|
||||
IOSSystemContextMenuItemDataShare getData(
|
||||
WidgetsLocalizations localizations,
|
||||
) {
|
||||
return IOSSystemContextMenuItemDataShare(
|
||||
title: title ?? localizations.shareButtonLabel,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'IOSSystemContextMenuItemShare(title: $title)';
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(justinmc): Support the "custom" type.
|
||||
// https://github.com/flutter/flutter/issues/103163
|
||||
2051
lib/common/widgets/flutter/text_field/text_field.dart
Normal file
2051
lib/common/widgets/flutter/text_field/text_field.dart
Normal file
File diff suppressed because it is too large
Load Diff
3313
lib/common/widgets/flutter/text_field/text_selection.dart
Normal file
3313
lib/common/widgets/flutter/text_field/text_selection.dart
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user