SelectableText field now supports keyboard actions natively (#56)
This commit is contained in:
parent
3e0da2a8f0
commit
2831c84b57
|
@ -21,6 +21,6 @@
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>MinimumOSVersion</key>
|
<key>MinimumOSVersion</key>
|
||||||
<string>8.0</string>
|
<string>9.0</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -42,7 +42,7 @@ PODS:
|
||||||
- MTBBarcodeScanner (5.0.11)
|
- MTBBarcodeScanner (5.0.11)
|
||||||
- package_info (0.0.1):
|
- package_info (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- path_provider (0.0.1):
|
- path_provider_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SDWebImage (5.8.0):
|
- SDWebImage (5.8.0):
|
||||||
- SDWebImage/Core (= 5.8.0)
|
- SDWebImage/Core (= 5.8.0)
|
||||||
|
@ -52,7 +52,7 @@ PODS:
|
||||||
- SDWebImage/Core (~> 5.6)
|
- SDWebImage/Core (~> 5.6)
|
||||||
- SwiftProtobuf (1.9.0)
|
- SwiftProtobuf (1.9.0)
|
||||||
- SwiftyJSON (5.0.1)
|
- SwiftyJSON (5.0.1)
|
||||||
- url_launcher (0.0.1):
|
- url_launcher_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
|
@ -60,9 +60,9 @@ DEPENDENCIES:
|
||||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- package_info (from `.symlinks/plugins/package_info/ios`)
|
- package_info (from `.symlinks/plugins/package_info/ios`)
|
||||||
- path_provider (from `.symlinks/plugins/path_provider/ios`)
|
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||||
- SwiftyJSON (~> 5.0)
|
- SwiftyJSON (~> 5.0)
|
||||||
- url_launcher (from `.symlinks/plugins/url_launcher/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
|
@ -84,10 +84,10 @@ EXTERNAL SOURCES:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
package_info:
|
package_info:
|
||||||
:path: ".symlinks/plugins/package_info/ios"
|
:path: ".symlinks/plugins/package_info/ios"
|
||||||
path_provider:
|
path_provider_ios:
|
||||||
:path: ".symlinks/plugins/path_provider/ios"
|
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||||
url_launcher:
|
url_launcher_ios:
|
||||||
:path: ".symlinks/plugins/url_launcher/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
barcode_scan: a5c27959edfafaa0c771905bad0b29d6d39e4479
|
barcode_scan: a5c27959edfafaa0c771905bad0b29d6d39e4479
|
||||||
|
@ -95,15 +95,15 @@ SPEC CHECKSUMS:
|
||||||
DKPhotoGallery: e880aef16c108333240e1e7327896f2ea380f4f0
|
DKPhotoGallery: e880aef16c108333240e1e7327896f2ea380f4f0
|
||||||
file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1
|
file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1
|
||||||
FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31
|
FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31
|
||||||
Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c
|
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
||||||
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
||||||
package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62
|
package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62
|
||||||
path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c
|
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
|
||||||
SDWebImage: 84000f962cbfa70c07f19d2234cbfcf5d779b5dc
|
SDWebImage: 84000f962cbfa70c07f19d2234cbfcf5d779b5dc
|
||||||
SDWebImageFLPlugin: 6c2295fb1242d44467c6c87dc5db6b0a13228fd8
|
SDWebImageFLPlugin: 6c2295fb1242d44467c6c87dc5db6b0a13228fd8
|
||||||
SwiftProtobuf: ecbec1be9036d15655f6b3443a1c4ea693c97932
|
SwiftProtobuf: ecbec1be9036d15655f6b3443a1c4ea693c97932
|
||||||
SwiftyJSON: 2f33a42c6fbc52764d96f13368585094bfd8aa5e
|
SwiftyJSON: 2f33a42c6fbc52764d96f13368585094bfd8aa5e
|
||||||
url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef
|
url_launcher_ios: 02f1989d4e14e998335b02b67a7590fa34f971af
|
||||||
|
|
||||||
PODFILE CHECKSUM: 92e176614f91c6517d4254a0edec8b66f076c77e
|
PODFILE CHECKSUM: 92e176614f91c6517d4254a0edec8b66f076c77e
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 46;
|
objectVersion = 50;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
@ -275,7 +275,7 @@
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastSwiftUpdateCheck = 1140;
|
LastSwiftUpdateCheck = 1140;
|
||||||
LastUpgradeCheck = 1020;
|
LastUpgradeCheck = 1300;
|
||||||
ORGANIZATIONNAME = "The Chromium Authors";
|
ORGANIZATIONNAME = "The Chromium Authors";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
43AA89532444DA6500EDC39C = {
|
43AA89532444DA6500EDC39C = {
|
||||||
|
@ -349,8 +349,8 @@
|
||||||
"${BUILT_PRODUCTS_DIR}/barcode_scan/barcode_scan.framework",
|
"${BUILT_PRODUCTS_DIR}/barcode_scan/barcode_scan.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework",
|
"${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/package_info/package_info.framework",
|
"${BUILT_PRODUCTS_DIR}/package_info/package_info.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework",
|
"${BUILT_PRODUCTS_DIR}/path_provider_ios/path_provider_ios.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/url_launcher/url_launcher.framework",
|
"${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework",
|
||||||
);
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
|
@ -365,8 +365,8 @@
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/barcode_scan.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/barcode_scan.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_ios.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework",
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
|
@ -587,7 +587,10 @@
|
||||||
);
|
);
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
LIBRARY_SEARCH_PATHS = (
|
LIBRARY_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/Flutter",
|
"$(PROJECT_DIR)/Flutter",
|
||||||
|
@ -622,7 +625,11 @@
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
INFOPLIST_FILE = NebulaNetworkExtension/Info.plist;
|
INFOPLIST_FILE = NebulaNetworkExtension/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
MARKETING_VERSION = 0.0.43;
|
MARKETING_VERSION = 0.0.43;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
|
@ -659,7 +666,11 @@
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
INFOPLIST_FILE = NebulaNetworkExtension/Info.plist;
|
INFOPLIST_FILE = NebulaNetworkExtension/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
MARKETING_VERSION = 0.0.43;
|
MARKETING_VERSION = 0.0.43;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
OTHER_LDFLAGS = "";
|
OTHER_LDFLAGS = "";
|
||||||
|
@ -693,7 +704,11 @@
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
INFOPLIST_FILE = NebulaNetworkExtension/Info.plist;
|
INFOPLIST_FILE = NebulaNetworkExtension/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
MARKETING_VERSION = 0.0.43;
|
MARKETING_VERSION = 0.0.43;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
OTHER_LDFLAGS = "";
|
OTHER_LDFLAGS = "";
|
||||||
|
@ -830,7 +845,10 @@
|
||||||
);
|
);
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
LIBRARY_SEARCH_PATHS = (
|
LIBRARY_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/Flutter",
|
"$(PROJECT_DIR)/Flutter",
|
||||||
|
@ -863,7 +881,10 @@
|
||||||
);
|
);
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
LIBRARY_SEARCH_PATHS = (
|
LIBRARY_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/Flutter",
|
"$(PROJECT_DIR)/Flutter",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1020"
|
LastUpgradeVersion = "1300"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|
|
@ -1,677 +0,0 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
// @dart = 2.8
|
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/rendering.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
//TODO: please let us delete this file
|
|
||||||
|
|
||||||
/// An eyeballed value that moves the cursor slightly left of where it is
|
|
||||||
/// rendered for text on Android so its positioning more accurately matches the
|
|
||||||
/// native iOS text cursor positioning.
|
|
||||||
///
|
|
||||||
/// This value is in device pixels, not logical pixels as is typically used
|
|
||||||
/// throughout the codebase.
|
|
||||||
const int iOSHorizontalOffset = -2;
|
|
||||||
|
|
||||||
class _TextSpanEditingController extends TextEditingController {
|
|
||||||
_TextSpanEditingController({@required TextSpan textSpan})
|
|
||||||
: assert(textSpan != null),
|
|
||||||
_textSpan = textSpan,
|
|
||||||
super(text: textSpan.toPlainText());
|
|
||||||
|
|
||||||
final TextSpan _textSpan;
|
|
||||||
|
|
||||||
@override
|
|
||||||
TextSpan buildTextSpan({BuildContext context, TextStyle style, bool withComposing}) {
|
|
||||||
// This does not care about composing.
|
|
||||||
return TextSpan(
|
|
||||||
style: style,
|
|
||||||
children: <TextSpan>[_textSpan],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
set text(String newText) {
|
|
||||||
// This should never be reached.
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SpecialSelectableTextSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder {
|
|
||||||
_SpecialSelectableTextSelectionGestureDetectorBuilder({
|
|
||||||
@required _SpecialSelectableTextState state,
|
|
||||||
}) : _state = state,
|
|
||||||
super(delegate: state);
|
|
||||||
|
|
||||||
final _SpecialSelectableTextState _state;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onForcePressStart(ForcePressDetails details) {
|
|
||||||
super.onForcePressStart(details);
|
|
||||||
if (delegate.selectionEnabled && shouldShowSelectionToolbar) {
|
|
||||||
editableText.showToolbar();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onForcePressEnd(ForcePressDetails details) {
|
|
||||||
// Not required.
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
|
|
||||||
if (delegate.selectionEnabled) {
|
|
||||||
switch (Theme.of(_state.context).platform) {
|
|
||||||
case TargetPlatform.iOS:
|
|
||||||
case TargetPlatform.macOS:
|
|
||||||
renderEditable.selectPositionAt(
|
|
||||||
from: details.globalPosition,
|
|
||||||
cause: SelectionChangedCause.longPress,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case TargetPlatform.android:
|
|
||||||
case TargetPlatform.fuchsia:
|
|
||||||
case TargetPlatform.linux:
|
|
||||||
case TargetPlatform.windows:
|
|
||||||
renderEditable.selectWordsInRange(
|
|
||||||
from: details.globalPosition - details.offsetFromOrigin,
|
|
||||||
to: details.globalPosition,
|
|
||||||
cause: SelectionChangedCause.longPress,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onSingleTapUp(TapUpDetails details) {
|
|
||||||
editableText.hideToolbar();
|
|
||||||
if (delegate.selectionEnabled) {
|
|
||||||
switch (Theme.of(_state.context).platform) {
|
|
||||||
case TargetPlatform.iOS:
|
|
||||||
case TargetPlatform.macOS:
|
|
||||||
renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
|
|
||||||
break;
|
|
||||||
case TargetPlatform.android:
|
|
||||||
case TargetPlatform.fuchsia:
|
|
||||||
case TargetPlatform.linux:
|
|
||||||
case TargetPlatform.windows:
|
|
||||||
renderEditable.selectPosition(cause: SelectionChangedCause.tap);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (_state.widget.onTap != null) _state.widget.onTap();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onSingleLongTapStart(LongPressStartDetails details) {
|
|
||||||
if (delegate.selectionEnabled) {
|
|
||||||
switch (Theme.of(_state.context).platform) {
|
|
||||||
case TargetPlatform.iOS:
|
|
||||||
case TargetPlatform.macOS:
|
|
||||||
renderEditable.selectPositionAt(
|
|
||||||
from: details.globalPosition,
|
|
||||||
cause: SelectionChangedCause.longPress,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case TargetPlatform.android:
|
|
||||||
case TargetPlatform.fuchsia:
|
|
||||||
case TargetPlatform.linux:
|
|
||||||
case TargetPlatform.windows:
|
|
||||||
renderEditable.selectWord(cause: SelectionChangedCause.longPress);
|
|
||||||
Feedback.forLongPress(_state.context);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A run of selectable text with a single style.
|
|
||||||
///
|
|
||||||
/// The [SpecialSelectableText] widget displays a string of text with a single style.
|
|
||||||
/// The string might break across multiple lines or might all be displayed on
|
|
||||||
/// the same line depending on the layout constraints.
|
|
||||||
///
|
|
||||||
/// {@youtube 560 315 https://www.youtube.com/watch?v=ZSU3ZXOs6hc}
|
|
||||||
///
|
|
||||||
/// The [style] argument is optional. When omitted, the text will use the style
|
|
||||||
/// from the closest enclosing [DefaultTextStyle]. If the given style's
|
|
||||||
/// [TextStyle.inherit] property is true (the default), the given style will
|
|
||||||
/// be merged with the closest enclosing [DefaultTextStyle]. This merging
|
|
||||||
/// behavior is useful, for example, to make the text bold while using the
|
|
||||||
/// default font family and size.
|
|
||||||
///
|
|
||||||
/// {@tool snippet}
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// SpecialSelectableText(
|
|
||||||
/// 'Hello! How are you?',
|
|
||||||
/// textAlign: TextAlign.center,
|
|
||||||
/// style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
/// )
|
|
||||||
/// ```
|
|
||||||
/// {@end-tool}
|
|
||||||
///
|
|
||||||
/// Using the [SpecialSelectableText.rich] constructor, the [SpecialSelectableText] widget can
|
|
||||||
/// display a paragraph with differently styled [TextSpan]s. The sample
|
|
||||||
/// that follows displays "Hello beautiful world" with different styles
|
|
||||||
/// for each word.
|
|
||||||
///
|
|
||||||
/// {@tool snippet}
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// const SpecialSelectableText.rich(
|
|
||||||
/// TextSpan(
|
|
||||||
/// text: 'Hello', // default text style
|
|
||||||
/// children: <TextSpan>[
|
|
||||||
/// TextSpan(text: ' beautiful ', style: TextStyle(fontStyle: FontStyle.italic)),
|
|
||||||
/// TextSpan(text: 'world', style: TextStyle(fontWeight: FontWeight.bold)),
|
|
||||||
/// ],
|
|
||||||
/// ),
|
|
||||||
/// )
|
|
||||||
/// ```
|
|
||||||
/// {@end-tool}
|
|
||||||
///
|
|
||||||
/// ## Interactivity
|
|
||||||
///
|
|
||||||
/// To make [SpecialSelectableText] react to touch events, use callback [onTap] to achieve
|
|
||||||
/// the desired behavior.
|
|
||||||
///
|
|
||||||
/// See also:
|
|
||||||
///
|
|
||||||
/// * [Text], which is the non selectable version of this widget.
|
|
||||||
/// * [TextField], which is the editable version of this widget.
|
|
||||||
class SpecialSelectableText extends StatefulWidget {
|
|
||||||
/// Creates a selectable text widget.
|
|
||||||
///
|
|
||||||
/// If the [style] argument is null, the text will use the style from the
|
|
||||||
/// closest enclosing [DefaultTextStyle].
|
|
||||||
///
|
|
||||||
|
|
||||||
/// The [showCursor], [autofocus], [dragStartBehavior], and [data] parameters
|
|
||||||
/// must not be null. If specified, the [maxLines] argument must be greater
|
|
||||||
/// than zero.
|
|
||||||
const SpecialSelectableText(
|
|
||||||
this.data, {
|
|
||||||
Key key,
|
|
||||||
this.focusNode,
|
|
||||||
this.style,
|
|
||||||
this.strutStyle,
|
|
||||||
this.textAlign,
|
|
||||||
this.textDirection,
|
|
||||||
this.textScaleFactor,
|
|
||||||
this.showCursor = false,
|
|
||||||
this.autofocus = false,
|
|
||||||
ToolbarOptions toolbarOptions,
|
|
||||||
this.minLines,
|
|
||||||
this.maxLines,
|
|
||||||
this.cursorWidth = 2.0,
|
|
||||||
this.cursorRadius,
|
|
||||||
this.cursorColor,
|
|
||||||
this.dragStartBehavior = DragStartBehavior.start,
|
|
||||||
this.enableInteractiveSelection = true,
|
|
||||||
this.onTap,
|
|
||||||
this.scrollPhysics,
|
|
||||||
this.textHeightBehavior,
|
|
||||||
this.textWidthBasis,
|
|
||||||
}) : assert(showCursor != null),
|
|
||||||
assert(autofocus != null),
|
|
||||||
assert(dragStartBehavior != null),
|
|
||||||
assert(maxLines == null || maxLines > 0),
|
|
||||||
assert(minLines == null || minLines > 0),
|
|
||||||
assert(
|
|
||||||
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
|
|
||||||
'minLines can\'t be greater than maxLines',
|
|
||||||
),
|
|
||||||
assert(
|
|
||||||
data != null,
|
|
||||||
'A non-null String must be provided to a SpecialSelectableText widget.',
|
|
||||||
),
|
|
||||||
textSpan = null,
|
|
||||||
toolbarOptions = toolbarOptions ??
|
|
||||||
const ToolbarOptions(
|
|
||||||
selectAll: true,
|
|
||||||
copy: true,
|
|
||||||
),
|
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
/// Creates a selectable text widget with a [TextSpan].
|
|
||||||
///
|
|
||||||
/// The [textSpan] parameter must not be null and only contain [TextSpan] in
|
|
||||||
/// [textSpan.children]. Other type of [InlineSpan] is not allowed.
|
|
||||||
///
|
|
||||||
/// The [autofocus] and [dragStartBehavior] arguments must not be null.
|
|
||||||
const SpecialSelectableText.rich(
|
|
||||||
this.textSpan, {
|
|
||||||
Key key,
|
|
||||||
this.focusNode,
|
|
||||||
this.style,
|
|
||||||
this.strutStyle,
|
|
||||||
this.textAlign,
|
|
||||||
this.textDirection,
|
|
||||||
this.textScaleFactor,
|
|
||||||
this.showCursor = false,
|
|
||||||
this.autofocus = false,
|
|
||||||
ToolbarOptions toolbarOptions,
|
|
||||||
this.minLines,
|
|
||||||
this.maxLines,
|
|
||||||
this.cursorWidth = 2.0,
|
|
||||||
this.cursorRadius,
|
|
||||||
this.cursorColor,
|
|
||||||
this.dragStartBehavior = DragStartBehavior.start,
|
|
||||||
this.enableInteractiveSelection = true,
|
|
||||||
this.onTap,
|
|
||||||
this.scrollPhysics,
|
|
||||||
this.textHeightBehavior,
|
|
||||||
this.textWidthBasis,
|
|
||||||
}) : assert(showCursor != null),
|
|
||||||
assert(autofocus != null),
|
|
||||||
assert(dragStartBehavior != null),
|
|
||||||
assert(maxLines == null || maxLines > 0),
|
|
||||||
assert(minLines == null || minLines > 0),
|
|
||||||
assert(
|
|
||||||
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
|
|
||||||
'minLines can\'t be greater than maxLines',
|
|
||||||
),
|
|
||||||
assert(
|
|
||||||
textSpan != null,
|
|
||||||
'A non-null TextSpan must be provided to a SpecialSelectableText.rich widget.',
|
|
||||||
),
|
|
||||||
data = null,
|
|
||||||
toolbarOptions = toolbarOptions ??
|
|
||||||
const ToolbarOptions(
|
|
||||||
selectAll: true,
|
|
||||||
copy: true,
|
|
||||||
),
|
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
/// The text to display.
|
|
||||||
///
|
|
||||||
/// This will be null if a [textSpan] is provided instead.
|
|
||||||
final String data;
|
|
||||||
|
|
||||||
/// The text to display as a [TextSpan].
|
|
||||||
///
|
|
||||||
/// This will be null if [data] is provided instead.
|
|
||||||
final TextSpan textSpan;
|
|
||||||
|
|
||||||
/// Defines the focus for this widget.
|
|
||||||
///
|
|
||||||
/// Text is only selectable when widget is focused.
|
|
||||||
///
|
|
||||||
/// The [focusNode] is a long-lived object that's typically managed by a
|
|
||||||
/// [StatefulWidget] parent. See [FocusNode] for more information.
|
|
||||||
///
|
|
||||||
/// To give the focus to this widget, provide a [focusNode] and then
|
|
||||||
/// use the current [FocusScope] to request the focus:
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// FocusScope.of(context).requestFocus(myFocusNode);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// This happens automatically when the widget is tapped.
|
|
||||||
///
|
|
||||||
/// To be notified when the widget gains or loses the focus, add a listener
|
|
||||||
/// to the [focusNode]:
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// focusNode.addListener(() { print(myFocusNode.hasFocus); });
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// If null, this widget will create its own [FocusNode].
|
|
||||||
final FocusNode focusNode;
|
|
||||||
|
|
||||||
/// The style to use for the text.
|
|
||||||
///
|
|
||||||
/// If null, defaults [DefaultTextStyle] of context.
|
|
||||||
final TextStyle style;
|
|
||||||
|
|
||||||
/// {@macro flutter.widgets.editableText.strutStyle}
|
|
||||||
final StrutStyle strutStyle;
|
|
||||||
|
|
||||||
/// {@macro flutter.widgets.editableText.textAlign}
|
|
||||||
final TextAlign textAlign;
|
|
||||||
|
|
||||||
/// {@macro flutter.widgets.editableText.textDirection}
|
|
||||||
final TextDirection textDirection;
|
|
||||||
|
|
||||||
/// {@macro flutter.widgets.editableText.textScaleFactor}
|
|
||||||
final double textScaleFactor;
|
|
||||||
|
|
||||||
/// {@macro flutter.widgets.editableText.autofocus}
|
|
||||||
final bool autofocus;
|
|
||||||
|
|
||||||
/// {@macro flutter.widgets.editableText.minLines}
|
|
||||||
final int minLines;
|
|
||||||
|
|
||||||
/// {@macro flutter.widgets.editableText.maxLines}
|
|
||||||
final int maxLines;
|
|
||||||
|
|
||||||
/// {@macro flutter.widgets.editableText.showCursor}
|
|
||||||
final bool showCursor;
|
|
||||||
|
|
||||||
/// {@macro flutter.widgets.editableText.cursorWidth}
|
|
||||||
final double cursorWidth;
|
|
||||||
|
|
||||||
/// {@macro flutter.widgets.editableText.cursorRadius}
|
|
||||||
final Radius cursorRadius;
|
|
||||||
|
|
||||||
/// The color to use when painting the cursor.
|
|
||||||
///
|
|
||||||
/// Defaults to the theme's `cursorColor` when null.
|
|
||||||
final Color cursorColor;
|
|
||||||
|
|
||||||
/// {@macro flutter.widgets.editableText.enableInteractiveSelection}
|
|
||||||
final bool enableInteractiveSelection;
|
|
||||||
|
|
||||||
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
|
||||||
final DragStartBehavior dragStartBehavior;
|
|
||||||
|
|
||||||
/// Configuration of toolbar options.
|
|
||||||
///
|
|
||||||
/// Paste and cut will be disabled regardless.
|
|
||||||
///
|
|
||||||
/// If not set, select all and copy will be enabled by default.
|
|
||||||
final ToolbarOptions toolbarOptions;
|
|
||||||
|
|
||||||
/// {@macro flutter.rendering.editable.selectionEnabled}
|
|
||||||
bool get selectionEnabled {
|
|
||||||
return enableInteractiveSelection;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Called when the user taps on this selectable text.
|
|
||||||
///
|
|
||||||
/// The selectable text builds a [GestureDetector] to handle input events like tap,
|
|
||||||
/// to trigger focus requests, to move the caret, adjust the selection, etc.
|
|
||||||
/// Handling some of those events by wrapping the selectable text with a competing
|
|
||||||
/// GestureDetector is problematic.
|
|
||||||
///
|
|
||||||
/// To unconditionally handle taps, without interfering with the selectable text's
|
|
||||||
/// internal gesture detector, provide this callback.
|
|
||||||
///
|
|
||||||
/// To be notified when the text field gains or loses the focus, provide a
|
|
||||||
/// [focusNode] and add a listener to that.
|
|
||||||
///
|
|
||||||
/// To listen to arbitrary pointer events without competing with the
|
|
||||||
/// selectable text's internal gesture detector, use a [Listener].
|
|
||||||
final GestureTapCallback onTap;
|
|
||||||
|
|
||||||
/// {@macro flutter.widgets.editableText.scrollPhysics}
|
|
||||||
final ScrollPhysics scrollPhysics;
|
|
||||||
|
|
||||||
/// {@macro flutter.dart:ui.textHeightBehavior}
|
|
||||||
final TextHeightBehavior textHeightBehavior;
|
|
||||||
|
|
||||||
/// {@macro flutter.painting.textPainter.textWidthBasis}
|
|
||||||
final TextWidthBasis textWidthBasis;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_SpecialSelectableTextState createState() => _SpecialSelectableTextState();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
||||||
super.debugFillProperties(properties);
|
|
||||||
properties.add(DiagnosticsProperty<String>('data', data, defaultValue: null));
|
|
||||||
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
|
|
||||||
properties.add(DiagnosticsProperty<TextStyle>('style', style, defaultValue: null));
|
|
||||||
properties.add(DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
|
|
||||||
properties.add(DiagnosticsProperty<bool>('showCursor', showCursor, defaultValue: false));
|
|
||||||
properties.add(IntProperty('minLines', minLines, defaultValue: null));
|
|
||||||
properties.add(IntProperty('maxLines', maxLines, defaultValue: null));
|
|
||||||
properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null));
|
|
||||||
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
|
|
||||||
properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null));
|
|
||||||
properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
|
|
||||||
properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null));
|
|
||||||
properties.add(DiagnosticsProperty<Color>('cursorColor', cursorColor, defaultValue: null));
|
|
||||||
properties.add(
|
|
||||||
FlagProperty('selectionEnabled', value: selectionEnabled, defaultValue: true, ifFalse: 'selection disabled'));
|
|
||||||
properties.add(DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null));
|
|
||||||
properties
|
|
||||||
.add(DiagnosticsProperty<TextHeightBehavior>('textHeightBehavior', textHeightBehavior, defaultValue: null));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SpecialSelectableTextState extends State<SpecialSelectableText>
|
|
||||||
with AutomaticKeepAliveClientMixin
|
|
||||||
implements TextSelectionGestureDetectorBuilderDelegate {
|
|
||||||
EditableTextState get _editableText => editableTextKey.currentState;
|
|
||||||
|
|
||||||
_TextSpanEditingController _controller;
|
|
||||||
|
|
||||||
FocusNode _keyFocusNode = FocusNode();
|
|
||||||
FocusNode _focusNode;
|
|
||||||
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
|
|
||||||
|
|
||||||
bool _showSelectionHandles = false;
|
|
||||||
|
|
||||||
_SpecialSelectableTextSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder;
|
|
||||||
|
|
||||||
// API for TextSelectionGestureDetectorBuilderDelegate.
|
|
||||||
@override
|
|
||||||
bool forcePressEnabled;
|
|
||||||
|
|
||||||
@override
|
|
||||||
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get selectionEnabled => widget.selectionEnabled;
|
|
||||||
// End of API for TextSelectionGestureDetectorBuilderDelegate.
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_selectionGestureDetectorBuilder = _SpecialSelectableTextSelectionGestureDetectorBuilder(state: this);
|
|
||||||
_controller = _TextSpanEditingController(textSpan: widget.textSpan ?? TextSpan(text: widget.data));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(SpecialSelectableText oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (widget.data != oldWidget.data || widget.textSpan != oldWidget.textSpan) {
|
|
||||||
_controller = _TextSpanEditingController(textSpan: widget.textSpan ?? TextSpan(text: widget.data));
|
|
||||||
}
|
|
||||||
if (_effectiveFocusNode.hasFocus && _controller.selection.isCollapsed) {
|
|
||||||
_showSelectionHandles = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_focusNode?.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) {
|
|
||||||
final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
|
|
||||||
if (willShowSelectionHandles != _showSelectionHandles) {
|
|
||||||
setState(() {
|
|
||||||
_showSelectionHandles = willShowSelectionHandles;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (Theme.of(context).platform) {
|
|
||||||
case TargetPlatform.iOS:
|
|
||||||
case TargetPlatform.macOS:
|
|
||||||
if (cause == SelectionChangedCause.longPress) {
|
|
||||||
_editableText?.bringIntoView(selection.base);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
case TargetPlatform.android:
|
|
||||||
case TargetPlatform.fuchsia:
|
|
||||||
case TargetPlatform.linux:
|
|
||||||
case TargetPlatform.windows:
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Toggle the toolbar when a selection handle is tapped.
|
|
||||||
void _handleSelectionHandleTapped() {
|
|
||||||
if (_controller.selection.isCollapsed) {
|
|
||||||
_editableText.toggleToolbar();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _shouldShowSelectionHandles(SelectionChangedCause cause) {
|
|
||||||
// When the text field is activated by something that doesn't trigger the
|
|
||||||
// selection overlay, we shouldn't show the handles either.
|
|
||||||
if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) return false;
|
|
||||||
|
|
||||||
if (_controller.selection.isCollapsed) return false;
|
|
||||||
|
|
||||||
if (cause == SelectionChangedCause.keyboard) return false;
|
|
||||||
|
|
||||||
if (cause == SelectionChangedCause.longPress) return true;
|
|
||||||
|
|
||||||
if (_controller.text.isNotEmpty) return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get wantKeepAlive => true;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
super.build(context); // See AutomaticKeepAliveClientMixin.
|
|
||||||
assert(() {
|
|
||||||
return _controller._textSpan.visitChildren((InlineSpan span) => span.runtimeType == TextSpan);
|
|
||||||
}(), 'SpecialSelectableText only supports TextSpan; Other type of InlineSpan is not allowed');
|
|
||||||
assert(debugCheckHasMediaQuery(context));
|
|
||||||
assert(debugCheckHasDirectionality(context));
|
|
||||||
assert(
|
|
||||||
!(widget.style != null &&
|
|
||||||
widget.style.inherit == false &&
|
|
||||||
(widget.style.fontSize == null || widget.style.textBaseline == null)),
|
|
||||||
'inherit false style must supply fontSize and textBaseline',
|
|
||||||
);
|
|
||||||
|
|
||||||
final ThemeData themeData = Theme.of(context);
|
|
||||||
final FocusNode focusNode = _effectiveFocusNode;
|
|
||||||
|
|
||||||
TextSelectionControls textSelectionControls;
|
|
||||||
bool paintCursorAboveText;
|
|
||||||
bool cursorOpacityAnimates;
|
|
||||||
Offset cursorOffset;
|
|
||||||
Color cursorColor = widget.cursorColor;
|
|
||||||
Radius cursorRadius = widget.cursorRadius;
|
|
||||||
|
|
||||||
switch (themeData.platform) {
|
|
||||||
case TargetPlatform.iOS:
|
|
||||||
case TargetPlatform.macOS:
|
|
||||||
forcePressEnabled = true;
|
|
||||||
textSelectionControls = cupertinoTextSelectionControls;
|
|
||||||
paintCursorAboveText = true;
|
|
||||||
cursorOpacityAnimates = true;
|
|
||||||
cursorColor ??= CupertinoTheme.of(context).primaryColor;
|
|
||||||
cursorRadius ??= const Radius.circular(2.0);
|
|
||||||
cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case TargetPlatform.android:
|
|
||||||
case TargetPlatform.fuchsia:
|
|
||||||
case TargetPlatform.linux:
|
|
||||||
case TargetPlatform.windows:
|
|
||||||
forcePressEnabled = false;
|
|
||||||
textSelectionControls = materialTextSelectionControls;
|
|
||||||
paintCursorAboveText = false;
|
|
||||||
cursorOpacityAnimates = false;
|
|
||||||
cursorColor ??= themeData.cursorColor;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
|
|
||||||
TextStyle effectiveTextStyle = widget.style;
|
|
||||||
if (widget.style == null || widget.style.inherit) effectiveTextStyle = defaultTextStyle.style.merge(widget.style);
|
|
||||||
if (MediaQuery.boldTextOverride(context))
|
|
||||||
effectiveTextStyle = effectiveTextStyle.merge(const TextStyle(fontWeight: FontWeight.bold));
|
|
||||||
final Widget child = RepaintBoundary(
|
|
||||||
child: EditableText(
|
|
||||||
key: editableTextKey,
|
|
||||||
style: effectiveTextStyle,
|
|
||||||
readOnly: true,
|
|
||||||
textWidthBasis: widget.textWidthBasis ?? defaultTextStyle.textWidthBasis,
|
|
||||||
textHeightBehavior: widget.textHeightBehavior ?? defaultTextStyle.textHeightBehavior,
|
|
||||||
showSelectionHandles: _showSelectionHandles,
|
|
||||||
showCursor: widget.showCursor,
|
|
||||||
controller: _controller,
|
|
||||||
focusNode: focusNode,
|
|
||||||
strutStyle: widget.strutStyle ?? const StrutStyle(),
|
|
||||||
textAlign: widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
|
|
||||||
textDirection: widget.textDirection,
|
|
||||||
textScaleFactor: widget.textScaleFactor,
|
|
||||||
autofocus: widget.autofocus,
|
|
||||||
forceLine: false,
|
|
||||||
toolbarOptions: widget.toolbarOptions,
|
|
||||||
minLines: widget.minLines,
|
|
||||||
maxLines: widget.maxLines ?? defaultTextStyle.maxLines,
|
|
||||||
selectionColor: themeData.textSelectionColor,
|
|
||||||
selectionControls: widget.selectionEnabled ? textSelectionControls : null,
|
|
||||||
onSelectionChanged: _handleSelectionChanged,
|
|
||||||
onSelectionHandleTapped: _handleSelectionHandleTapped,
|
|
||||||
rendererIgnoresPointer: true,
|
|
||||||
cursorWidth: widget.cursorWidth,
|
|
||||||
cursorRadius: cursorRadius,
|
|
||||||
cursorColor: cursorColor,
|
|
||||||
cursorOpacityAnimates: cursorOpacityAnimates,
|
|
||||||
cursorOffset: cursorOffset,
|
|
||||||
paintCursorAboveText: paintCursorAboveText,
|
|
||||||
backgroundCursorColor: CupertinoColors.inactiveGray,
|
|
||||||
enableInteractiveSelection: widget.enableInteractiveSelection,
|
|
||||||
dragStartBehavior: widget.dragStartBehavior,
|
|
||||||
scrollPhysics: widget.scrollPhysics,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return Semantics(
|
|
||||||
onTap: () {
|
|
||||||
if (!_controller.selection.isValid)
|
|
||||||
_controller.selection = TextSelection.collapsed(offset: _controller.text.length);
|
|
||||||
_effectiveFocusNode.requestFocus();
|
|
||||||
},
|
|
||||||
onLongPress: () {
|
|
||||||
_effectiveFocusNode.requestFocus();
|
|
||||||
},
|
|
||||||
child: _selectionGestureDetectorBuilder.buildGestureDetector(
|
|
||||||
behavior: HitTestBehavior.translucent,
|
|
||||||
child: RawKeyboardListener(
|
|
||||||
focusNode: _keyFocusNode,
|
|
||||||
onKey: _onKey,
|
|
||||||
child: child,
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onKey(RawKeyEvent event) {
|
|
||||||
// We don't care about key up events
|
|
||||||
if (event is RawKeyUpEvent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: tab to next focus node
|
|
||||||
|
|
||||||
// Handle special keyboard events with control key
|
|
||||||
if (event.data.isControlPressed) {
|
|
||||||
// Handle select all
|
|
||||||
if (event.logicalKey == LogicalKeyboardKey.keyA) {
|
|
||||||
_controller.selection = TextSelection(baseOffset: 0, extentOffset: _controller.text.length);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle copy
|
|
||||||
if (event.logicalKey == LogicalKeyboardKey.keyC) {
|
|
||||||
Clipboard.setData(ClipboardData(text: _controller.selection.textInside(_controller.text)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
||||||
import 'package:mobile_nebula/components/SimplePage.dart';
|
import 'package:mobile_nebula/components/SimplePage.dart';
|
||||||
import 'package:mobile_nebula/components/SpecialSelectableText.dart';
|
|
||||||
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
||||||
import 'package:mobile_nebula/components/config/ConfigPageItem.dart';
|
import 'package:mobile_nebula/components/config/ConfigPageItem.dart';
|
||||||
import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
||||||
|
@ -79,6 +78,6 @@ class _AboutScreenState extends State<AboutScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildText(String str) {
|
_buildText(String str) {
|
||||||
return Align(alignment: AlignmentDirectional.centerEnd, child: SpecialSelectableText(str));
|
return Align(alignment: AlignmentDirectional.centerEnd, child: SelectableText(str));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
||||||
import 'package:mobile_nebula/components/SimplePage.dart';
|
import 'package:mobile_nebula/components/SimplePage.dart';
|
||||||
import 'package:mobile_nebula/components/SpecialSelectableText.dart';
|
|
||||||
import 'package:mobile_nebula/components/config/ConfigCheckboxItem.dart';
|
import 'package:mobile_nebula/components/config/ConfigCheckboxItem.dart';
|
||||||
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
||||||
import 'package:mobile_nebula/components/config/ConfigPageItem.dart';
|
import 'package:mobile_nebula/components/config/ConfigPageItem.dart';
|
||||||
|
@ -61,7 +60,7 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
|
||||||
|
|
||||||
Widget _buildMain() {
|
Widget _buildMain() {
|
||||||
return ConfigSection(children: [
|
return ConfigSection(children: [
|
||||||
ConfigItem(label: Text('VPN IP'), labelWidth: 150, content: SpecialSelectableText(hostInfo.vpnIp)),
|
ConfigItem(label: Text('VPN IP'), labelWidth: 150, content: SelectableText(hostInfo.vpnIp)),
|
||||||
hostInfo.cert != null
|
hostInfo.cert != null
|
||||||
? ConfigPageItem(
|
? ConfigPageItem(
|
||||||
label: Text('Certificate'),
|
label: Text('Certificate'),
|
||||||
|
@ -78,16 +77,16 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
label: Text('Lighthouse'),
|
label: Text('Lighthouse'),
|
||||||
labelWidth: 150,
|
labelWidth: 150,
|
||||||
content: SpecialSelectableText(widget.isLighthouse ? 'Yes' : 'No')),
|
content: SelectableText(widget.isLighthouse ? 'Yes' : 'No')),
|
||||||
ConfigItem(label: Text('Local Index'), labelWidth: 150, content: SpecialSelectableText('${hostInfo.localIndex}')),
|
ConfigItem(label: Text('Local Index'), labelWidth: 150, content: SelectableText('${hostInfo.localIndex}')),
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
label: Text('Remote Index'), labelWidth: 150, content: SpecialSelectableText('${hostInfo.remoteIndex}')),
|
label: Text('Remote Index'), labelWidth: 150, content: SelectableText('${hostInfo.remoteIndex}')),
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
label: Text('Message Counter'),
|
label: Text('Message Counter'),
|
||||||
labelWidth: 150,
|
labelWidth: 150,
|
||||||
content: SpecialSelectableText('${hostInfo.messageCounter}')),
|
content: SelectableText('${hostInfo.messageCounter}')),
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
label: Text('Cached Packets'), labelWidth: 150, content: SpecialSelectableText('${hostInfo.cachedPackets}')),
|
label: Text('Cached Packets'), labelWidth: 150, content: SelectableText('${hostInfo.cachedPackets}')),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
||||||
import 'package:mobile_nebula/components/SimplePage.dart';
|
import 'package:mobile_nebula/components/SimplePage.dart';
|
||||||
import 'package:mobile_nebula/components/SpecialSelectableText.dart';
|
|
||||||
import 'package:mobile_nebula/components/config/ConfigPageItem.dart';
|
import 'package:mobile_nebula/components/config/ConfigPageItem.dart';
|
||||||
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
||||||
import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
||||||
|
@ -111,7 +110,7 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
|
||||||
site.errors.forEach((error) {
|
site.errors.forEach((error) {
|
||||||
items.add(ConfigItem(
|
items.add(ConfigItem(
|
||||||
labelWidth: 0,
|
labelWidth: 0,
|
||||||
content: Padding(padding: EdgeInsets.symmetric(vertical: 10), child: SpecialSelectableText(error))));
|
content: Padding(padding: EdgeInsets.symmetric(vertical: 10), child: SelectableText(error))));
|
||||||
});
|
});
|
||||||
|
|
||||||
return ConfigSection(
|
return ConfigSection(
|
||||||
|
|
|
@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
||||||
import 'package:mobile_nebula/components/SimplePage.dart';
|
import 'package:mobile_nebula/components/SimplePage.dart';
|
||||||
import 'package:mobile_nebula/components/SpecialSelectableText.dart';
|
|
||||||
import 'package:mobile_nebula/models/Site.dart';
|
import 'package:mobile_nebula/models/Site.dart';
|
||||||
import 'package:mobile_nebula/services/settings.dart';
|
import 'package:mobile_nebula/services/settings.dart';
|
||||||
import 'package:mobile_nebula/services/share.dart';
|
import 'package:mobile_nebula/services/share.dart';
|
||||||
|
@ -57,7 +56,7 @@ class _SiteLogsScreenState extends State<SiteLogsScreen> {
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.all(5),
|
padding: EdgeInsets.all(5),
|
||||||
constraints: logBoxConstraints(context),
|
constraints: logBoxConstraints(context),
|
||||||
child: SpecialSelectableText(logs.trim(), style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14))),
|
child: SelectableText(logs.trim(), style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14))),
|
||||||
bottomBar: _buildBottomBar(),
|
bottomBar: _buildBottomBar(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
||||||
import 'package:mobile_nebula/components/SimplePage.dart';
|
import 'package:mobile_nebula/components/SimplePage.dart';
|
||||||
import 'package:mobile_nebula/components/SpecialSelectableText.dart';
|
|
||||||
import 'package:mobile_nebula/components/config/ConfigButtonItem.dart';
|
import 'package:mobile_nebula/components/config/ConfigButtonItem.dart';
|
||||||
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
||||||
import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
||||||
|
@ -96,7 +95,7 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
|
||||||
children: [
|
children: [
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
labelWidth: 0,
|
labelWidth: 0,
|
||||||
content: SpecialSelectableText(pubKey, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
content: SelectableText(pubKey, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
||||||
),
|
),
|
||||||
ConfigButtonItem(
|
ConfigButtonItem(
|
||||||
content: Text('Share Public Key'),
|
content: Text('Share Public Key'),
|
||||||
|
|
|
@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
||||||
import 'package:mobile_nebula/components/FormPage.dart';
|
import 'package:mobile_nebula/components/FormPage.dart';
|
||||||
import 'package:mobile_nebula/components/SpecialSelectableText.dart';
|
|
||||||
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
||||||
import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
||||||
import 'package:mobile_nebula/models/Certificate.dart';
|
import 'package:mobile_nebula/models/Certificate.dart';
|
||||||
|
@ -77,7 +76,7 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
|
||||||
|
|
||||||
Widget _buildID() {
|
Widget _buildID() {
|
||||||
return ConfigSection(children: <Widget>[
|
return ConfigSection(children: <Widget>[
|
||||||
ConfigItem(label: Text('Name'), content: SpecialSelectableText(certInfo.cert.details.name)),
|
ConfigItem(label: Text('Name'), content: SelectableText(certInfo.cert.details.name)),
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
label: Text('Type'), content: Text(certInfo.cert.details.isCa ? 'CA certificate' : 'Client certificate')),
|
label: Text('Type'), content: Text(certInfo.cert.details.isCa ? 'CA certificate' : 'Client certificate')),
|
||||||
]);
|
]);
|
||||||
|
@ -95,10 +94,10 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
|
||||||
ConfigItem(label: Text('Valid?'), content: valid),
|
ConfigItem(label: Text('Valid?'), content: valid),
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
label: Text('Created'),
|
label: Text('Created'),
|
||||||
content: SpecialSelectableText(certInfo.cert.details.notBefore.toLocal().toString())),
|
content: SelectableText(certInfo.cert.details.notBefore.toLocal().toString())),
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
label: Text('Expires'),
|
label: Text('Expires'),
|
||||||
content: SpecialSelectableText(certInfo.cert.details.notAfter.toLocal().toString())),
|
content: SelectableText(certInfo.cert.details.notAfter.toLocal().toString())),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -107,16 +106,16 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
|
||||||
List<Widget> items = [];
|
List<Widget> items = [];
|
||||||
if (certInfo.cert.details.groups.length > 0) {
|
if (certInfo.cert.details.groups.length > 0) {
|
||||||
items.add(
|
items.add(
|
||||||
ConfigItem(label: Text('Groups'), content: SpecialSelectableText(certInfo.cert.details.groups.join(', '))));
|
ConfigItem(label: Text('Groups'), content: SelectableText(certInfo.cert.details.groups.join(', '))));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (certInfo.cert.details.ips.length > 0) {
|
if (certInfo.cert.details.ips.length > 0) {
|
||||||
items.add(ConfigItem(label: Text('IPs'), content: SpecialSelectableText(certInfo.cert.details.ips.join(', '))));
|
items.add(ConfigItem(label: Text('IPs'), content: SelectableText(certInfo.cert.details.ips.join(', '))));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (certInfo.cert.details.subnets.length > 0) {
|
if (certInfo.cert.details.subnets.length > 0) {
|
||||||
items.add(
|
items.add(
|
||||||
ConfigItem(label: Text('Subnets'), content: SpecialSelectableText(certInfo.cert.details.subnets.join(', '))));
|
ConfigItem(label: Text('Subnets'), content: SelectableText(certInfo.cert.details.subnets.join(', '))));
|
||||||
}
|
}
|
||||||
|
|
||||||
return items.length > 0
|
return items.length > 0
|
||||||
|
@ -129,19 +128,19 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
label: Text('Fingerprint'),
|
label: Text('Fingerprint'),
|
||||||
content: SpecialSelectableText(certInfo.cert.fingerprint,
|
content: SelectableText(certInfo.cert.fingerprint,
|
||||||
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start),
|
crossAxisAlignment: CrossAxisAlignment.start),
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
label: Text('Public Key'),
|
label: Text('Public Key'),
|
||||||
content: SpecialSelectableText(certInfo.cert.details.publicKey,
|
content: SelectableText(certInfo.cert.details.publicKey,
|
||||||
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start),
|
crossAxisAlignment: CrossAxisAlignment.start),
|
||||||
certInfo.rawCert != null
|
certInfo.rawCert != null
|
||||||
? ConfigItem(
|
? ConfigItem(
|
||||||
label: Text('PEM Format'),
|
label: Text('PEM Format'),
|
||||||
content:
|
content:
|
||||||
SpecialSelectableText(certInfo.rawCert, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
SelectableText(certInfo.rawCert, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start)
|
crossAxisAlignment: CrossAxisAlignment.start)
|
||||||
: Container(),
|
: Container(),
|
||||||
],
|
],
|
||||||
|
|
|
@ -2,7 +2,6 @@ import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
||||||
import 'package:mobile_nebula/components/SimplePage.dart';
|
import 'package:mobile_nebula/components/SimplePage.dart';
|
||||||
import 'package:mobile_nebula/components/SpecialSelectableText.dart';
|
|
||||||
import 'package:mobile_nebula/services/share.dart';
|
import 'package:mobile_nebula/services/share.dart';
|
||||||
|
|
||||||
class RenderedConfigScreen extends StatelessWidget {
|
class RenderedConfigScreen extends StatelessWidget {
|
||||||
|
@ -26,7 +25,7 @@ class RenderedConfigScreen extends StatelessWidget {
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.all(5),
|
padding: EdgeInsets.all(5),
|
||||||
constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width),
|
constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width),
|
||||||
child: SpecialSelectableText(config, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14))),
|
child: SelectableText(config, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:mobile_nebula/components/FormPage.dart';
|
import 'package:mobile_nebula/components/FormPage.dart';
|
||||||
import 'package:mobile_nebula/components/PlatformTextFormField.dart';
|
import 'package:mobile_nebula/components/PlatformTextFormField.dart';
|
||||||
import 'package:mobile_nebula/components/SpecialSelectableText.dart';
|
|
||||||
import 'package:mobile_nebula/components/config/ConfigPageItem.dart';
|
import 'package:mobile_nebula/components/config/ConfigPageItem.dart';
|
||||||
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
||||||
import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
||||||
|
@ -92,7 +91,7 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
||||||
data = err.toString();
|
data = err.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ConfigSection(label: 'DEBUG', children: [ConfigItem(labelWidth: 0, content: SpecialSelectableText(data))]);
|
return ConfigSection(label: 'DEBUG', children: [ConfigItem(labelWidth: 0, content: SelectableText(data))]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _main() {
|
Widget _main() {
|
||||||
|
|
Loading…
Reference in New Issue