mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-07-24 02:37:41 +08:00
Markdown Toolbar: Add tab/backtab on long press of > or <
This commit is contained in:
422
lib/forks/icon_button_more_gestures.dart
Normal file
422
lib/forks/icon_button_more_gestures.dart
Normal file
@ -0,0 +1,422 @@
|
|||||||
|
// ignore_for_file: unnecessary_null_comparison, curly_braces_in_flow_control_structures
|
||||||
|
// vHanda: Added onLongPressed
|
||||||
|
|
||||||
|
// 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:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
// Minimum logical pixel size of the IconButton.
|
||||||
|
// See: <https://material.io/design/usability/accessibility.html#layout-typography>.
|
||||||
|
const double _kMinButtonSize = kMinInteractiveDimension;
|
||||||
|
|
||||||
|
/// A material design icon button.
|
||||||
|
///
|
||||||
|
/// An icon button is a picture printed on a [Material] widget that reacts to
|
||||||
|
/// touches by filling with color (ink).
|
||||||
|
///
|
||||||
|
/// Icon buttons are commonly used in the [AppBar.actions] field, but they can
|
||||||
|
/// be used in many other places as well.
|
||||||
|
///
|
||||||
|
/// If the [onPressed] callback is null, then the button will be disabled and
|
||||||
|
/// will not react to touch.
|
||||||
|
///
|
||||||
|
/// Requires one of its ancestors to be a [Material] widget.
|
||||||
|
///
|
||||||
|
/// The hit region of an icon button will, if possible, be at least
|
||||||
|
/// kMinInteractiveDimension pixels in size, regardless of the actual
|
||||||
|
/// [iconSize], to satisfy the [touch target size](https://material.io/design/layout/spacing-methods.html#touch-targets)
|
||||||
|
/// requirements in the Material Design specification. The [alignment] controls
|
||||||
|
/// how the icon itself is positioned within the hit region.
|
||||||
|
///
|
||||||
|
/// {@tool dartpad --template=stateful_widget_scaffold_center}
|
||||||
|
///
|
||||||
|
/// This sample shows an `IconButton` that uses the Material icon "volume_up" to
|
||||||
|
/// increase the volume.
|
||||||
|
///
|
||||||
|
/// 
|
||||||
|
///
|
||||||
|
/// ```dart preamble
|
||||||
|
/// double _volume = 0.0;
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// @override
|
||||||
|
/// Widget build(BuildContext context) {
|
||||||
|
/// return Column(
|
||||||
|
/// mainAxisSize: MainAxisSize.min,
|
||||||
|
/// children: <Widget>[
|
||||||
|
/// IconButton(
|
||||||
|
/// icon: const Icon(Icons.volume_up),
|
||||||
|
/// tooltip: 'Increase volume by 10',
|
||||||
|
/// onPressed: () {
|
||||||
|
/// setState(() {
|
||||||
|
/// _volume += 10;
|
||||||
|
/// });
|
||||||
|
/// },
|
||||||
|
/// ),
|
||||||
|
/// Text('Volume : $_volume')
|
||||||
|
/// ],
|
||||||
|
/// );
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// ### Adding a filled background
|
||||||
|
///
|
||||||
|
/// Icon buttons don't support specifying a background color or other
|
||||||
|
/// background decoration because typically the icon is just displayed
|
||||||
|
/// on top of the parent widget's background. Icon buttons that appear
|
||||||
|
/// in [AppBar.actions] are an example of this.
|
||||||
|
///
|
||||||
|
/// It's easy enough to create an icon button with a filled background
|
||||||
|
/// using the [Ink] widget. The [Ink] widget renders a decoration on
|
||||||
|
/// the underlying [Material] along with the splash and highlight
|
||||||
|
/// [InkResponse] contributed by descendant widgets.
|
||||||
|
///
|
||||||
|
/// {@tool dartpad --template=stateless_widget_scaffold}
|
||||||
|
///
|
||||||
|
/// In this sample the icon button's background color is defined with an [Ink]
|
||||||
|
/// widget whose child is an [IconButton]. The icon button's filled background
|
||||||
|
/// is a light shade of blue, it's a filled circle, and it's as big as the
|
||||||
|
/// button is.
|
||||||
|
///
|
||||||
|
/// 
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// @override
|
||||||
|
/// Widget build(BuildContext context) {
|
||||||
|
/// return Material(
|
||||||
|
/// color: Colors.white,
|
||||||
|
/// child: Center(
|
||||||
|
/// child: Ink(
|
||||||
|
/// decoration: const ShapeDecoration(
|
||||||
|
/// color: Colors.lightBlue,
|
||||||
|
/// shape: CircleBorder(),
|
||||||
|
/// ),
|
||||||
|
/// child: IconButton(
|
||||||
|
/// icon: const Icon(Icons.android),
|
||||||
|
/// color: Colors.white,
|
||||||
|
/// onPressed: () {},
|
||||||
|
/// ),
|
||||||
|
/// ),
|
||||||
|
/// ),
|
||||||
|
/// );
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Icons], a library of predefined icons.
|
||||||
|
/// * [BackButton], an icon button for a "back" affordance which adapts to the
|
||||||
|
/// current platform's conventions.
|
||||||
|
/// * [CloseButton], an icon button for closing pages.
|
||||||
|
/// * [AppBar], to show a toolbar at the top of an application.
|
||||||
|
/// * [TextButton], [ElevatedButton], [OutlinedButton], for buttons with text labels and an optional icon.
|
||||||
|
/// * [InkResponse] and [InkWell], for the ink splash effect itself.
|
||||||
|
class IconButton extends StatelessWidget {
|
||||||
|
/// Creates an icon button.
|
||||||
|
///
|
||||||
|
/// Icon buttons are commonly used in the [AppBar.actions] field, but they can
|
||||||
|
/// be used in many other places as well.
|
||||||
|
///
|
||||||
|
/// Requires one of its ancestors to be a [Material] widget.
|
||||||
|
///
|
||||||
|
/// The [iconSize], [padding], [autofocus], and [alignment] arguments must not
|
||||||
|
/// be null (though they each have default values).
|
||||||
|
///
|
||||||
|
/// The [icon] argument must be specified, and is typically either an [Icon]
|
||||||
|
/// or an [ImageIcon].
|
||||||
|
const IconButton({
|
||||||
|
Key? key,
|
||||||
|
this.iconSize = 24.0,
|
||||||
|
this.visualDensity,
|
||||||
|
this.padding = const EdgeInsets.all(8.0),
|
||||||
|
this.alignment = Alignment.center,
|
||||||
|
this.splashRadius,
|
||||||
|
this.color,
|
||||||
|
this.focusColor,
|
||||||
|
this.hoverColor,
|
||||||
|
this.highlightColor,
|
||||||
|
this.splashColor,
|
||||||
|
this.disabledColor,
|
||||||
|
required this.onPressed,
|
||||||
|
this.onLongPressed,
|
||||||
|
this.mouseCursor = SystemMouseCursors.click,
|
||||||
|
this.focusNode,
|
||||||
|
this.autofocus = false,
|
||||||
|
this.tooltip,
|
||||||
|
this.enableFeedback = true,
|
||||||
|
this.constraints,
|
||||||
|
required this.icon,
|
||||||
|
}) : assert(iconSize != null),
|
||||||
|
assert(padding != null),
|
||||||
|
assert(alignment != null),
|
||||||
|
assert(splashRadius == null || splashRadius > 0),
|
||||||
|
assert(autofocus != null),
|
||||||
|
assert(icon != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
/// The size of the icon inside the button.
|
||||||
|
///
|
||||||
|
/// This property must not be null. It defaults to 24.0.
|
||||||
|
///
|
||||||
|
/// The size given here is passed down to the widget in the [icon] property
|
||||||
|
/// via an [IconTheme]. Setting the size here instead of in, for example, the
|
||||||
|
/// [Icon.size] property allows the [IconButton] to size the splash area to
|
||||||
|
/// fit the [Icon]. If you were to set the size of the [Icon] using
|
||||||
|
/// [Icon.size] instead, then the [IconButton] would default to 24.0 and then
|
||||||
|
/// the [Icon] itself would likely get clipped.
|
||||||
|
final double iconSize;
|
||||||
|
|
||||||
|
/// Defines how compact the icon button's layout will be.
|
||||||
|
///
|
||||||
|
/// {@macro flutter.material.themedata.visualDensity}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [ThemeData.visualDensity], which specifies the [visualDensity] for all
|
||||||
|
/// widgets within a [Theme].
|
||||||
|
final VisualDensity? visualDensity;
|
||||||
|
|
||||||
|
/// The padding around the button's icon. The entire padded icon will react
|
||||||
|
/// to input gestures.
|
||||||
|
///
|
||||||
|
/// This property must not be null. It defaults to 8.0 padding on all sides.
|
||||||
|
final EdgeInsetsGeometry padding;
|
||||||
|
|
||||||
|
/// Defines how the icon is positioned within the IconButton.
|
||||||
|
///
|
||||||
|
/// This property must not be null. It defaults to [Alignment.center].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Alignment], a class with convenient constants typically used to
|
||||||
|
/// specify an [AlignmentGeometry].
|
||||||
|
/// * [AlignmentDirectional], like [Alignment] for specifying alignments
|
||||||
|
/// relative to text direction.
|
||||||
|
final AlignmentGeometry alignment;
|
||||||
|
|
||||||
|
/// The splash radius.
|
||||||
|
///
|
||||||
|
/// If null, default splash radius of [Material.defaultSplashRadius] is used.
|
||||||
|
final double? splashRadius;
|
||||||
|
|
||||||
|
/// The icon to display inside the button.
|
||||||
|
///
|
||||||
|
/// The [Icon.size] and [Icon.color] of the icon is configured automatically
|
||||||
|
/// based on the [iconSize] and [color] properties of _this_ widget using an
|
||||||
|
/// [IconTheme] and therefore should not be explicitly given in the icon
|
||||||
|
/// widget.
|
||||||
|
///
|
||||||
|
/// This property must not be null.
|
||||||
|
///
|
||||||
|
/// See [Icon], [ImageIcon].
|
||||||
|
final Widget icon;
|
||||||
|
|
||||||
|
/// The color for the button's icon when it has the input focus.
|
||||||
|
///
|
||||||
|
/// Defaults to [ThemeData.focusColor] of the ambient theme.
|
||||||
|
final Color? focusColor;
|
||||||
|
|
||||||
|
/// The color for the button's icon when a pointer is hovering over it.
|
||||||
|
///
|
||||||
|
/// Defaults to [ThemeData.hoverColor] of the ambient theme.
|
||||||
|
final Color? hoverColor;
|
||||||
|
|
||||||
|
/// The color to use for the icon inside the button, if the icon is enabled.
|
||||||
|
/// Defaults to leaving this up to the [icon] widget.
|
||||||
|
///
|
||||||
|
/// The icon is enabled if [onPressed] is not null.
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// IconButton(
|
||||||
|
/// color: Colors.blue,
|
||||||
|
/// onPressed: _handleTap,
|
||||||
|
/// icon: Icons.widgets,
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
final Color? color;
|
||||||
|
|
||||||
|
/// The primary color of the button when the button is in the down (pressed) state.
|
||||||
|
/// The splash is represented as a circular overlay that appears above the
|
||||||
|
/// [highlightColor] overlay. The splash overlay has a center point that matches
|
||||||
|
/// the hit point of the user touch event. The splash overlay will expand to
|
||||||
|
/// fill the button area if the touch is held for long enough time. If the splash
|
||||||
|
/// color has transparency then the highlight and button color will show through.
|
||||||
|
///
|
||||||
|
/// Defaults to the Theme's splash color, [ThemeData.splashColor].
|
||||||
|
final Color? splashColor;
|
||||||
|
|
||||||
|
/// The secondary color of the button when the button is in the down (pressed)
|
||||||
|
/// state. The highlight color is represented as a solid color that is overlaid over the
|
||||||
|
/// button color (if any). If the highlight color has transparency, the button color
|
||||||
|
/// will show through. The highlight fades in quickly as the button is held down.
|
||||||
|
///
|
||||||
|
/// Defaults to the Theme's highlight color, [ThemeData.highlightColor].
|
||||||
|
final Color? highlightColor;
|
||||||
|
|
||||||
|
/// The color to use for the icon inside the button, if the icon is disabled.
|
||||||
|
/// Defaults to the [ThemeData.disabledColor] of the current [Theme].
|
||||||
|
///
|
||||||
|
/// The icon is disabled if [onPressed] is null.
|
||||||
|
final Color? disabledColor;
|
||||||
|
|
||||||
|
/// The callback that is called when the button is tapped or otherwise activated.
|
||||||
|
///
|
||||||
|
/// If this is set to null, the button will be disabled.
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
final VoidCallback? onLongPressed;
|
||||||
|
|
||||||
|
/// {@macro flutter.material.RawMaterialButton.mouseCursor}
|
||||||
|
///
|
||||||
|
/// Defaults to [SystemMouseCursors.click].
|
||||||
|
final MouseCursor mouseCursor;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.Focus.focusNode}
|
||||||
|
final FocusNode? focusNode;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.Focus.autofocus}
|
||||||
|
final bool autofocus;
|
||||||
|
|
||||||
|
/// Text that describes the action that will occur when the button is pressed.
|
||||||
|
///
|
||||||
|
/// This text is displayed when the user long-presses on the button and is
|
||||||
|
/// used for accessibility.
|
||||||
|
final String? tooltip;
|
||||||
|
|
||||||
|
/// Whether detected gestures should provide acoustic and/or haptic feedback.
|
||||||
|
///
|
||||||
|
/// For example, on Android a tap will produce a clicking sound and a
|
||||||
|
/// long-press will produce a short vibration, when feedback is enabled.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Feedback] for providing platform-specific feedback to certain actions.
|
||||||
|
final bool enableFeedback;
|
||||||
|
|
||||||
|
/// Optional size constraints for the button.
|
||||||
|
///
|
||||||
|
/// When unspecified, defaults to:
|
||||||
|
/// ```dart
|
||||||
|
/// const BoxConstraints(
|
||||||
|
/// minWidth: kMinInteractiveDimension,
|
||||||
|
/// minHeight: kMinInteractiveDimension,
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// where [kMinInteractiveDimension] is 48.0, and then with visual density
|
||||||
|
/// applied.
|
||||||
|
///
|
||||||
|
/// The default constraints ensure that the button is accessible.
|
||||||
|
/// Specifying this parameter enables creation of buttons smaller than
|
||||||
|
/// the minimum size, but it is not recommended.
|
||||||
|
///
|
||||||
|
/// The visual density uses the [visualDensity] parameter if specified,
|
||||||
|
/// and `Theme.of(context).visualDensity` otherwise.
|
||||||
|
final BoxConstraints? constraints;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
assert(debugCheckHasMaterial(context));
|
||||||
|
final ThemeData theme = Theme.of(context);
|
||||||
|
Color? currentColor;
|
||||||
|
if (onPressed != null)
|
||||||
|
currentColor = color;
|
||||||
|
else
|
||||||
|
currentColor = disabledColor ?? theme.disabledColor;
|
||||||
|
|
||||||
|
final VisualDensity effectiveVisualDensity =
|
||||||
|
visualDensity ?? theme.visualDensity;
|
||||||
|
|
||||||
|
final BoxConstraints unadjustedConstraints = constraints ??
|
||||||
|
const BoxConstraints(
|
||||||
|
minWidth: _kMinButtonSize,
|
||||||
|
minHeight: _kMinButtonSize,
|
||||||
|
);
|
||||||
|
final BoxConstraints adjustedConstraints =
|
||||||
|
effectiveVisualDensity.effectiveConstraints(unadjustedConstraints);
|
||||||
|
|
||||||
|
Widget result = ConstrainedBox(
|
||||||
|
constraints: adjustedConstraints,
|
||||||
|
child: Padding(
|
||||||
|
padding: padding,
|
||||||
|
child: SizedBox(
|
||||||
|
height: iconSize,
|
||||||
|
width: iconSize,
|
||||||
|
child: Align(
|
||||||
|
alignment: alignment,
|
||||||
|
child: IconTheme.merge(
|
||||||
|
data: IconThemeData(
|
||||||
|
size: iconSize,
|
||||||
|
color: currentColor,
|
||||||
|
),
|
||||||
|
child: icon,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tooltip != null) {
|
||||||
|
result = Tooltip(
|
||||||
|
message: tooltip!,
|
||||||
|
child: result,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Semantics(
|
||||||
|
button: true,
|
||||||
|
enabled: onPressed != null,
|
||||||
|
child: InkResponse(
|
||||||
|
focusNode: focusNode,
|
||||||
|
autofocus: autofocus,
|
||||||
|
canRequestFocus: onPressed != null,
|
||||||
|
onTap: onPressed,
|
||||||
|
onLongPress: onLongPressed,
|
||||||
|
mouseCursor: mouseCursor,
|
||||||
|
enableFeedback: enableFeedback,
|
||||||
|
child: result,
|
||||||
|
focusColor: focusColor ?? theme.focusColor,
|
||||||
|
hoverColor: hoverColor ?? theme.hoverColor,
|
||||||
|
highlightColor: highlightColor ?? theme.highlightColor,
|
||||||
|
splashColor: splashColor ?? theme.splashColor,
|
||||||
|
radius: splashRadius ??
|
||||||
|
math.max(
|
||||||
|
Material.defaultSplashRadius,
|
||||||
|
(iconSize + math.min(padding.horizontal, padding.vertical)) * 0.7,
|
||||||
|
// x 0.5 for diameter -> radius and + 40% overflow derived from other Material apps.
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties.add(DiagnosticsProperty<Widget>('icon', icon, showName: false));
|
||||||
|
properties.add(
|
||||||
|
StringProperty('tooltip', tooltip, defaultValue: null, quoted: false));
|
||||||
|
properties.add(ObjectFlagProperty<VoidCallback>('onPressed', onPressed,
|
||||||
|
ifNull: 'disabled'));
|
||||||
|
properties.add(ColorProperty('color', color, defaultValue: null));
|
||||||
|
properties
|
||||||
|
.add(ColorProperty('disabledColor', disabledColor, defaultValue: null));
|
||||||
|
properties.add(ColorProperty('focusColor', focusColor, defaultValue: null));
|
||||||
|
properties.add(ColorProperty('hoverColor', hoverColor, defaultValue: null));
|
||||||
|
properties.add(
|
||||||
|
ColorProperty('highlightColor', highlightColor, defaultValue: null));
|
||||||
|
properties
|
||||||
|
.add(ColorProperty('splashColor', splashColor, defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding,
|
||||||
|
defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode,
|
||||||
|
defaultValue: null));
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
|
||||||
|
import 'package:gitjournal/forks/icon_button_more_gestures.dart' as fork;
|
||||||
|
|
||||||
// FIXME:
|
// FIXME:
|
||||||
// - Pin this on top of the keyboard
|
// - Pin this on top of the keyboard
|
||||||
// - It should only be visible when the keyboard is shown
|
// - It should only be visible when the keyboard is shown
|
||||||
@ -58,15 +60,17 @@ class MarkdownToolBar extends StatelessWidget {
|
|||||||
height: 20,
|
height: 20,
|
||||||
child: const VerticalDivider(),
|
child: const VerticalDivider(),
|
||||||
),
|
),
|
||||||
IconButton(
|
fork.IconButton(
|
||||||
icon: const Icon(Icons.navigate_before),
|
icon: const Icon(Icons.navigate_before),
|
||||||
padding: const EdgeInsets.all(0.0),
|
padding: const EdgeInsets.all(0.0),
|
||||||
onPressed: _navigateToPrevWord,
|
onPressed: _navigateToPrevWord,
|
||||||
|
onLongPressed: _addBackTab,
|
||||||
),
|
),
|
||||||
IconButton(
|
fork.IconButton(
|
||||||
icon: const Icon(Icons.navigate_next),
|
icon: const Icon(Icons.navigate_next),
|
||||||
padding: const EdgeInsets.all(0.0),
|
padding: const EdgeInsets.all(0.0),
|
||||||
onPressed: _navigateToNextWord,
|
onPressed: _navigateToNextWord,
|
||||||
|
onLongPressed: _addTab,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -90,6 +94,16 @@ class MarkdownToolBar extends StatelessWidget {
|
|||||||
var offset = nextWordPos(textController.value);
|
var offset = nextWordPos(textController.value);
|
||||||
textController.selection = TextSelection.collapsed(offset: offset);
|
textController.selection = TextSelection.collapsed(offset: offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: Maybe add Tab should work on lines instead? Independent of the cursor pos
|
||||||
|
// FIXME: Make addTab work for selections as well?
|
||||||
|
void _addTab() {
|
||||||
|
textController.value = addTab(textController.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addBackTab() {
|
||||||
|
textController.value = addBackTab(textController.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final _allowedBlockTags = [
|
final _allowedBlockTags = [
|
||||||
@ -287,3 +301,47 @@ int prevWordPos(TextEditingValue textEditingValue) {
|
|||||||
|
|
||||||
return lastSpacePos + 1;
|
return lastSpacePos + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextEditingValue addTab(TextEditingValue textEditingValue) {
|
||||||
|
var cursorPos = textEditingValue.selection.baseOffset;
|
||||||
|
var text = textEditingValue.text;
|
||||||
|
|
||||||
|
var newText = "";
|
||||||
|
if (cursorPos == text.length) {
|
||||||
|
newText = text + "\t";
|
||||||
|
} else {
|
||||||
|
newText = text.substring(0, cursorPos) + "\t" + text.substring(cursorPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TextEditingValue(
|
||||||
|
text: newText,
|
||||||
|
selection: TextSelection.collapsed(offset: cursorPos + 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextEditingValue addBackTab(TextEditingValue textEditingValue) {
|
||||||
|
var cursorPos = textEditingValue.selection.baseOffset;
|
||||||
|
var text = textEditingValue.text;
|
||||||
|
|
||||||
|
if (cursorPos <= 0) {
|
||||||
|
return textEditingValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var prevChar = text[cursorPos - 1];
|
||||||
|
if (prevChar == '\t') {
|
||||||
|
var newText = "";
|
||||||
|
if (cursorPos - 1 > 1) {
|
||||||
|
newText += text.substring(0, cursorPos - 1);
|
||||||
|
}
|
||||||
|
if (cursorPos != text.length) {
|
||||||
|
newText += text.substring(cursorPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TextEditingValue(
|
||||||
|
text: newText,
|
||||||
|
selection: TextSelection.collapsed(offset: cursorPos - 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return textEditingValue;
|
||||||
|
}
|
||||||
|
@ -272,5 +272,87 @@ void main() {
|
|||||||
_testPrevWord(text, 0, 0);
|
_testPrevWord(text, 0, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test that if some text is selected then it should be modified
|
void _testTab({
|
||||||
|
required String before,
|
||||||
|
required int beforeOffset,
|
||||||
|
required String after,
|
||||||
|
required int afterOffset,
|
||||||
|
}) {
|
||||||
|
var val = TextEditingValue(
|
||||||
|
text: before,
|
||||||
|
selection: TextSelection.collapsed(offset: beforeOffset),
|
||||||
|
);
|
||||||
|
|
||||||
|
var expectedVal = TextEditingValue(
|
||||||
|
text: after,
|
||||||
|
selection: TextSelection.collapsed(offset: afterOffset),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(addTab(val), expectedVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _testBackTab({
|
||||||
|
required String before,
|
||||||
|
required int beforeOffset,
|
||||||
|
required String after,
|
||||||
|
required int afterOffset,
|
||||||
|
}) {
|
||||||
|
var val = TextEditingValue(
|
||||||
|
text: before,
|
||||||
|
selection: TextSelection.collapsed(offset: beforeOffset),
|
||||||
|
);
|
||||||
|
|
||||||
|
var expectedVal = TextEditingValue(
|
||||||
|
text: after,
|
||||||
|
selection: TextSelection.collapsed(offset: afterOffset),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(addBackTab(val), expectedVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Tab', () {
|
||||||
|
_testTab(
|
||||||
|
before: 'Hello',
|
||||||
|
beforeOffset: 0,
|
||||||
|
after: '\tHello',
|
||||||
|
afterOffset: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
_testTab(
|
||||||
|
before: 'Hello',
|
||||||
|
beforeOffset: 1,
|
||||||
|
after: 'H\tello',
|
||||||
|
afterOffset: 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
_testTab(
|
||||||
|
before: 'Hi',
|
||||||
|
beforeOffset: 2,
|
||||||
|
after: 'Hi\t',
|
||||||
|
afterOffset: 3,
|
||||||
|
);
|
||||||
|
|
||||||
|
_testBackTab(
|
||||||
|
before: '\tHello',
|
||||||
|
beforeOffset: 1,
|
||||||
|
after: 'Hello',
|
||||||
|
afterOffset: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
_testBackTab(
|
||||||
|
before: 'Hi',
|
||||||
|
beforeOffset: 0,
|
||||||
|
after: 'Hi',
|
||||||
|
afterOffset: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
_testBackTab(
|
||||||
|
before: 'Hi',
|
||||||
|
beforeOffset: 1,
|
||||||
|
after: 'Hi',
|
||||||
|
afterOffset: 1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Test that if some text is selected then it should be modified
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user