refactor(ui): improve message input controller
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// Button for showing visual component of slow mode.
|
||||
class CountdownButton extends StatelessWidget {
|
||||
/// Constructor for creating [CountdownButton].
|
||||
const CountdownButton({
|
||||
Key? key,
|
||||
required this.count,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Count of time remaining to show to the user.
|
||||
final int count;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.disabled,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: Center(
|
||||
child: Text('$count'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,551 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/media_list_view.dart';
|
||||
import 'package:stream_chat_flutter/src/video_service.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
|
||||
/// Callback for when a file has to be picked.
|
||||
typedef FilePickerCallback = void Function(
|
||||
DefaultAttachmentTypes fileType, {
|
||||
bool camera,
|
||||
});
|
||||
|
||||
/// Callback for building an icon for a custom attachment type.
|
||||
typedef CustomAttachmentIconBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
bool active,
|
||||
);
|
||||
|
||||
///
|
||||
class StreamAttachmentPicker extends StatefulWidget {
|
||||
final bool isOpen;
|
||||
final double pickerSize;
|
||||
final MessageInputController messageInputController;
|
||||
final int attachmentLimit;
|
||||
final AttachmentLimitExceedListener? onAttachmentLimitExceeded;
|
||||
final ValueChanged<bool>? onChangeInputState;
|
||||
final ValueChanged<String>? onError;
|
||||
final FilePickerCallback onFilePicked;
|
||||
|
||||
/// Video quality to use when compressing the videos
|
||||
final VideoQuality compressedVideoQuality;
|
||||
|
||||
/// Frame rate to use when compressing the videos
|
||||
final int compressedVideoFrameRate;
|
||||
|
||||
/// Max attachment size in bytes
|
||||
/// Defaults to 20 MB
|
||||
/// do not set it if you're using our default CDN
|
||||
final int maxAttachmentSize;
|
||||
|
||||
final List<DefaultAttachmentTypes> allowedAttachmentTypes;
|
||||
|
||||
final List<CustomAttachmentType> customAttachmentTypes;
|
||||
|
||||
const StreamAttachmentPicker({
|
||||
Key? key,
|
||||
required this.messageInputController,
|
||||
required this.onFilePicked,
|
||||
this.isOpen = false,
|
||||
this.pickerSize = 360.0,
|
||||
this.attachmentLimit = 10,
|
||||
this.onAttachmentLimitExceeded,
|
||||
this.maxAttachmentSize = 20971520,
|
||||
this.compressedVideoQuality = VideoQuality.DefaultQuality,
|
||||
this.compressedVideoFrameRate = 30,
|
||||
this.onChangeInputState,
|
||||
this.onError,
|
||||
this.allowedAttachmentTypes = const [
|
||||
DefaultAttachmentTypes.image,
|
||||
DefaultAttachmentTypes.file,
|
||||
DefaultAttachmentTypes.video,
|
||||
],
|
||||
this.customAttachmentTypes = const [],
|
||||
}) : super(key: key);
|
||||
|
||||
StreamAttachmentPicker copyWith({
|
||||
Key? key,
|
||||
MessageInputController? messageInputController,
|
||||
FilePickerCallback? onFilePicked,
|
||||
bool? isOpen,
|
||||
double? pickerSize,
|
||||
int? attachmentLimit,
|
||||
AttachmentLimitExceedListener? onAttachmentLimitExceeded,
|
||||
int? maxAttachmentSize,
|
||||
VideoQuality? compressedVideoQuality,
|
||||
int? compressedVideoFrameRate,
|
||||
ValueChanged<bool>? onChangeInputState,
|
||||
ValueChanged<String>? onError,
|
||||
List<DefaultAttachmentTypes>? allowedAttachmentTypes,
|
||||
List<CustomAttachmentType>? customAttachmentTypes = const [],
|
||||
}) =>
|
||||
StreamAttachmentPicker(
|
||||
key: key ?? this.key,
|
||||
messageInputController:
|
||||
messageInputController ?? this.messageInputController,
|
||||
onFilePicked: onFilePicked ?? this.onFilePicked,
|
||||
isOpen: isOpen ?? this.isOpen,
|
||||
pickerSize: pickerSize ?? this.pickerSize,
|
||||
attachmentLimit: attachmentLimit ?? this.attachmentLimit,
|
||||
onAttachmentLimitExceeded:
|
||||
onAttachmentLimitExceeded ?? this.onAttachmentLimitExceeded,
|
||||
maxAttachmentSize: maxAttachmentSize ?? this.maxAttachmentSize,
|
||||
compressedVideoQuality:
|
||||
compressedVideoQuality ?? this.compressedVideoQuality,
|
||||
compressedVideoFrameRate:
|
||||
compressedVideoFrameRate ?? this.compressedVideoFrameRate,
|
||||
onChangeInputState: onChangeInputState ?? this.onChangeInputState,
|
||||
onError: onError ?? this.onError,
|
||||
allowedAttachmentTypes:
|
||||
allowedAttachmentTypes ?? this.allowedAttachmentTypes,
|
||||
customAttachmentTypes:
|
||||
customAttachmentTypes ?? this.customAttachmentTypes,
|
||||
);
|
||||
|
||||
@override
|
||||
State<StreamAttachmentPicker> createState() => _StreamAttachmentPickerState();
|
||||
}
|
||||
|
||||
class _StreamAttachmentPickerState extends State<StreamAttachmentPicker> {
|
||||
int _filePickerIndex = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _streamChatTheme = StreamChatTheme.of(context);
|
||||
final messageInputController = widget.messageInputController;
|
||||
|
||||
final _attachmentContainsImage =
|
||||
messageInputController.attachments.any((it) => it.type == 'image');
|
||||
|
||||
final _attachmentContainsFile =
|
||||
messageInputController.attachments.any((it) => it.type == 'file');
|
||||
|
||||
final _attachmentContainsVideo =
|
||||
messageInputController.attachments.any((it) => it.type == 'video');
|
||||
|
||||
final attachmentLimitCrossed =
|
||||
messageInputController.attachments.length >= widget.attachmentLimit;
|
||||
|
||||
Color _getIconColor(int index) {
|
||||
final streamChatThemeData = _streamChatTheme;
|
||||
switch (index) {
|
||||
case 0:
|
||||
return _filePickerIndex == 0 || _attachmentContainsImage
|
||||
? streamChatThemeData.colorTheme.accentPrimary
|
||||
: (_attachmentContainsImage
|
||||
? streamChatThemeData.colorTheme.accentPrimary
|
||||
: streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(
|
||||
messageInputController.attachments.isEmpty ? 0.5 : 0.2,
|
||||
));
|
||||
case 1:
|
||||
return _attachmentContainsFile
|
||||
? streamChatThemeData.colorTheme.accentPrimary
|
||||
: (messageInputController.attachments.isEmpty
|
||||
? streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.5)
|
||||
: streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.2));
|
||||
case 2:
|
||||
return widget.messageInputController.attachments.isNotEmpty &&
|
||||
(!_attachmentContainsImage || attachmentLimitCrossed)
|
||||
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
|
||||
: _attachmentContainsFile &&
|
||||
messageInputController.attachments.isNotEmpty
|
||||
? streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.2)
|
||||
: streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.5);
|
||||
case 3:
|
||||
return widget.messageInputController.attachments.isNotEmpty &&
|
||||
(!_attachmentContainsVideo || attachmentLimitCrossed)
|
||||
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
|
||||
: _attachmentContainsFile &&
|
||||
messageInputController.attachments.isNotEmpty
|
||||
? streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.2)
|
||||
: streamChatThemeData.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.5);
|
||||
default:
|
||||
return Colors.black;
|
||||
}
|
||||
}
|
||||
|
||||
return AnimatedContainer(
|
||||
duration:
|
||||
widget.isOpen ? const Duration(milliseconds: 300) : const Duration(),
|
||||
curve: Curves.easeOut,
|
||||
height: widget.isOpen ? widget.pickerSize : 0,
|
||||
child: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
height: widget.pickerSize,
|
||||
child: Material(
|
||||
color: _streamChatTheme.colorTheme.inputBg,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (widget.allowedAttachmentTypes
|
||||
.contains(DefaultAttachmentTypes.image))
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.pictures(
|
||||
color: _getIconColor(0),
|
||||
),
|
||||
onPressed:
|
||||
messageInputController.attachments.isNotEmpty &&
|
||||
!_attachmentContainsImage
|
||||
? null
|
||||
: () {
|
||||
setState(() {
|
||||
_filePickerIndex = 0;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (widget.allowedAttachmentTypes
|
||||
.contains(DefaultAttachmentTypes.file))
|
||||
IconButton(
|
||||
iconSize: 32,
|
||||
icon: StreamSvgIcon.files(
|
||||
color: _getIconColor(1),
|
||||
),
|
||||
onPressed: messageInputController
|
||||
.attachments.isNotEmpty &&
|
||||
!_attachmentContainsFile
|
||||
? null
|
||||
: () {
|
||||
widget
|
||||
.onFilePicked(DefaultAttachmentTypes.file);
|
||||
},
|
||||
),
|
||||
if (widget.allowedAttachmentTypes
|
||||
.contains(DefaultAttachmentTypes.image))
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.camera(
|
||||
color: _getIconColor(2),
|
||||
),
|
||||
onPressed: attachmentLimitCrossed ||
|
||||
(messageInputController
|
||||
.attachments.isNotEmpty &&
|
||||
!_attachmentContainsVideo)
|
||||
? null
|
||||
: () {
|
||||
widget.onFilePicked(
|
||||
DefaultAttachmentTypes.image,
|
||||
camera: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
if (widget.allowedAttachmentTypes
|
||||
.contains(DefaultAttachmentTypes.video))
|
||||
IconButton(
|
||||
padding: const EdgeInsets.all(0),
|
||||
icon: StreamSvgIcon.record(
|
||||
color: _getIconColor(3),
|
||||
),
|
||||
onPressed: attachmentLimitCrossed ||
|
||||
(messageInputController
|
||||
.attachments.isNotEmpty &&
|
||||
!_attachmentContainsVideo)
|
||||
? null
|
||||
: () {
|
||||
widget.onFilePicked(
|
||||
DefaultAttachmentTypes.video,
|
||||
camera: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
for (int i = 0;
|
||||
i < widget.customAttachmentTypes.length;
|
||||
i++)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (messageInputController.attachments.isNotEmpty) {
|
||||
if (!messageInputController.attachments.any((e) =>
|
||||
e.type ==
|
||||
widget.customAttachmentTypes[i].type)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_filePickerIndex = i + 1;
|
||||
});
|
||||
},
|
||||
icon: widget.customAttachmentTypes[i]
|
||||
.iconBuilder(context, _filePickerIndex == i + 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.inputBg,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.isOpen &&
|
||||
(widget.allowedAttachmentTypes
|
||||
.contains(DefaultAttachmentTypes.image) ||
|
||||
(widget.allowedAttachmentTypes
|
||||
.contains(DefaultAttachmentTypes.file))))
|
||||
Expanded(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: _PickerWidget(
|
||||
filePickerIndex: _filePickerIndex,
|
||||
streamChatTheme: _streamChatTheme,
|
||||
containsFile: _attachmentContainsFile,
|
||||
selectedMedias: messageInputController.attachments
|
||||
.map((e) => e.id)
|
||||
.toList(),
|
||||
onAddMoreFilesClick: widget.onFilePicked,
|
||||
onMediaSelected: (media) {
|
||||
if (messageInputController.attachments
|
||||
.any((e) => e.id == media.id)) {
|
||||
setState(() => messageInputController.attachments
|
||||
.removeWhere((e) => e.id == media.id));
|
||||
} else {
|
||||
_addAssetAttachment(media);
|
||||
}
|
||||
},
|
||||
allowedAttachmentTypes: widget.allowedAttachmentTypes,
|
||||
customAttachmentTypes: widget.customAttachmentTypes,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _addAssetAttachment(AssetEntity medium) async {
|
||||
final mediaFile = await medium.originFile.timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => medium.originFile,
|
||||
);
|
||||
|
||||
if (mediaFile == null) return;
|
||||
|
||||
var file = AttachmentFile(
|
||||
path: mediaFile.path,
|
||||
size: await mediaFile.length(),
|
||||
bytes: mediaFile.readAsBytesSync(),
|
||||
);
|
||||
|
||||
if (file.size! > widget.maxAttachmentSize) {
|
||||
if (medium.type == AssetType.video && file.path != null) {
|
||||
final mediaInfo = await (VideoService.compressVideo(
|
||||
file.path!,
|
||||
frameRate: widget.compressedVideoFrameRate,
|
||||
quality: widget.compressedVideoQuality,
|
||||
) as FutureOr<MediaInfo>);
|
||||
|
||||
if (mediaInfo.filesize! > widget.maxAttachmentSize) {
|
||||
widget.onError?.call(
|
||||
context.translations.fileTooLargeAfterCompressionError(
|
||||
widget.maxAttachmentSize / (1024 * 1024),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
file = AttachmentFile(
|
||||
name: file.name,
|
||||
size: mediaInfo.filesize,
|
||||
bytes: await mediaInfo.file?.readAsBytes(),
|
||||
path: mediaInfo.path,
|
||||
);
|
||||
} else {
|
||||
widget.onError?.call(context.translations.fileTooLargeError(
|
||||
widget.maxAttachmentSize / (1024 * 1024),
|
||||
));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
final attachment = Attachment(
|
||||
id: medium.id,
|
||||
file: file,
|
||||
type: medium.type == AssetType.image ? 'image' : 'video',
|
||||
);
|
||||
_addAttachments([attachment]);
|
||||
});
|
||||
}
|
||||
|
||||
/// Adds an attachment to the [messageInputController.attachments] map
|
||||
void _addAttachments(Iterable<Attachment> attachments) {
|
||||
final limit = widget.attachmentLimit;
|
||||
final length =
|
||||
widget.messageInputController.attachments.length + attachments.length;
|
||||
if (length > limit) {
|
||||
final onAttachmentLimitExceed = widget.onAttachmentLimitExceeded;
|
||||
if (onAttachmentLimitExceed != null) {
|
||||
return onAttachmentLimitExceed(
|
||||
widget.attachmentLimit,
|
||||
context.translations.attachmentLimitExceedError(limit),
|
||||
);
|
||||
}
|
||||
return widget.onError?.call(
|
||||
context.translations.attachmentLimitExceedError(limit),
|
||||
);
|
||||
}
|
||||
for (final attachment in attachments) {
|
||||
widget.messageInputController.addAttachment(attachment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _PickerWidget extends StatefulWidget {
|
||||
const _PickerWidget({
|
||||
Key? key,
|
||||
required this.filePickerIndex,
|
||||
required this.containsFile,
|
||||
required this.selectedMedias,
|
||||
required this.onAddMoreFilesClick,
|
||||
required this.onMediaSelected,
|
||||
required this.streamChatTheme,
|
||||
required this.allowedAttachmentTypes,
|
||||
required this.customAttachmentTypes,
|
||||
}) : super(key: key);
|
||||
|
||||
final int filePickerIndex;
|
||||
final bool containsFile;
|
||||
final List<String> selectedMedias;
|
||||
final void Function(DefaultAttachmentTypes) onAddMoreFilesClick;
|
||||
final void Function(AssetEntity) onMediaSelected;
|
||||
final StreamChatThemeData streamChatTheme;
|
||||
final List<DefaultAttachmentTypes> allowedAttachmentTypes;
|
||||
final List<CustomAttachmentType> customAttachmentTypes;
|
||||
|
||||
@override
|
||||
_PickerWidgetState createState() => _PickerWidgetState();
|
||||
}
|
||||
|
||||
class _PickerWidgetState extends State<_PickerWidget> {
|
||||
Future<bool>? requestPermission;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
requestPermission = PhotoManager.requestPermission();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.filePickerIndex != 0) {
|
||||
return widget.customAttachmentTypes[widget.filePickerIndex - 1]
|
||||
.pickerBuilder(context);
|
||||
}
|
||||
return FutureBuilder<bool>(
|
||||
future: requestPermission,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Offstage();
|
||||
}
|
||||
|
||||
if (snapshot.data!) {
|
||||
if (widget.containsFile ||
|
||||
!widget.allowedAttachmentTypes
|
||||
.contains(DefaultAttachmentTypes.image)) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
widget.onAddMoreFilesClick(DefaultAttachmentTypes.file);
|
||||
},
|
||||
child: Container(
|
||||
constraints: const BoxConstraints.expand(),
|
||||
color: widget.streamChatTheme.colorTheme.inputBg,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
context.translations.addMoreFilesLabel,
|
||||
style: TextStyle(
|
||||
color: widget.streamChatTheme.colorTheme.accentPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return MediaListView(
|
||||
selectedIds: widget.selectedMedias,
|
||||
onSelect: widget.onMediaSelected,
|
||||
);
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
PhotoManager.openSetting();
|
||||
},
|
||||
child: Container(
|
||||
color: widget.streamChatTheme.colorTheme.inputBg,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'svgs/icon_picture_empty_state.svg',
|
||||
package: 'stream_chat_flutter',
|
||||
height: 140,
|
||||
color: widget.streamChatTheme.colorTheme.disabled,
|
||||
),
|
||||
Text(
|
||||
context.translations.enablePhotoAndVideoAccessMessage,
|
||||
style: widget.streamChatTheme.textTheme.body.copyWith(
|
||||
color: widget.streamChatTheme.colorTheme.textLowEmphasis,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Center(
|
||||
child: Text(
|
||||
context.translations.allowGalleryAccessMessage,
|
||||
style: widget.streamChatTheme.textTheme.bodyBold.copyWith(
|
||||
color: widget.streamChatTheme.colorTheme.accentPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomAttachmentType {
|
||||
String type;
|
||||
CustomAttachmentIconBuilder iconBuilder;
|
||||
WidgetBuilder pickerBuilder;
|
||||
|
||||
CustomAttachmentType({
|
||||
required this.type,
|
||||
required this.iconBuilder,
|
||||
required this.pickerBuilder,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
class StreamMessageSendButton extends StatelessWidget {
|
||||
final int timeOut;
|
||||
final bool isIdle;
|
||||
final bool isCommandEnabled;
|
||||
final bool isEditEnabled;
|
||||
final Widget? idleSendButton;
|
||||
final Widget? activeSendButton;
|
||||
final VoidCallback onSendMessage;
|
||||
|
||||
const StreamMessageSendButton({
|
||||
Key? key,
|
||||
this.timeOut = 0,
|
||||
this.isIdle = true,
|
||||
this.isCommandEnabled = false,
|
||||
this.isEditEnabled = false,
|
||||
this.idleSendButton,
|
||||
this.activeSendButton,
|
||||
required this.onSendMessage,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _streamChatTheme = StreamChatTheme.of(context);
|
||||
|
||||
late Widget sendButton;
|
||||
if (timeOut > 0) {
|
||||
sendButton = CountdownButton(count: timeOut);
|
||||
} else if (isIdle) {
|
||||
sendButton = idleSendButton ?? _buildIdleSendButton(context);
|
||||
} else {
|
||||
sendButton = activeSendButton != null
|
||||
? InkWell(
|
||||
onTap: onSendMessage,
|
||||
child: activeSendButton,
|
||||
)
|
||||
: _buildSendButton(context);
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!,
|
||||
child: sendButton,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIdleSendButton(BuildContext context) {
|
||||
final _messageInputTheme = MessageInputTheme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: StreamSvgIcon(
|
||||
assetName: _getIdleSendIcon(),
|
||||
color: _messageInputTheme.sendButtonIdleColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSendButton(BuildContext context) {
|
||||
final _messageInputTheme = MessageInputTheme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: IconButton(
|
||||
onPressed: onSendMessage,
|
||||
padding: const EdgeInsets.all(0),
|
||||
splashRadius: 24,
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
icon: StreamSvgIcon(
|
||||
assetName: _getSendIcon(),
|
||||
color: _messageInputTheme.sendButtonColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getIdleSendIcon() {
|
||||
if (isCommandEnabled) {
|
||||
return 'Icon_search.svg';
|
||||
} else {
|
||||
return 'Icon_circle_right.svg';
|
||||
}
|
||||
}
|
||||
|
||||
String _getSendIcon() {
|
||||
if (isEditEnabled) {
|
||||
return 'Icon_circle_up.svg';
|
||||
} else if (isCommandEnabled) {
|
||||
return 'Icon_search.svg';
|
||||
} else {
|
||||
return 'Icon_circle_up.svg';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,779 @@
|
||||
// ignore_for_file: prefer-trailing-comma, cascade_invocations
|
||||
|
||||
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
|
||||
|
||||
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:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
export 'package:flutter/services.dart'
|
||||
show
|
||||
TextInputType,
|
||||
TextInputAction,
|
||||
TextCapitalization,
|
||||
SmartQuotesType,
|
||||
SmartDashesType;
|
||||
|
||||
class StreamMessageTextField extends StatefulWidget {
|
||||
/// Creates a Material Design text field.
|
||||
///
|
||||
/// If [decoration] is non-null (which is the default), the text field requires
|
||||
/// one of its ancestors to be a [Material] widget.
|
||||
///
|
||||
/// To remove the decoration entirely (including the extra padding introduced
|
||||
/// by the decoration to save space for the labels), set the [decoration] to
|
||||
/// null.
|
||||
///
|
||||
/// The [maxLines] property can be set to null to remove the restriction on
|
||||
/// the number of lines. By default, it is one, meaning this is a single-line
|
||||
/// text field. [maxLines] must not be zero.
|
||||
///
|
||||
/// The [maxLength] property is set to null by default, which means the
|
||||
/// number of characters allowed in the text field is not restricted. If
|
||||
/// [maxLength] is set a character counter will be displayed below the
|
||||
/// field showing how many characters have been entered. If the value is
|
||||
/// set to a positive integer it will also display the maximum allowed
|
||||
/// number of characters to be entered. If the value is set to
|
||||
/// [TextField.noMaxLength] then only the current length is displayed.
|
||||
///
|
||||
/// After [maxLength] characters have been input, additional input
|
||||
/// is ignored, unless [maxLengthEnforcement] is set to
|
||||
/// [MaxLengthEnforcement.none].
|
||||
/// The text field enforces the length with a [LengthLimitingTextInputFormatter],
|
||||
/// which is evaluated after the supplied [inputFormatters], if any.
|
||||
/// The [maxLength] value must be either null or greater than zero.
|
||||
///
|
||||
/// If [maxLengthEnforced] is set to false, then more than [maxLength]
|
||||
/// characters may be entered, and the error counter and divider will
|
||||
/// switch to the [decoration].errorStyle when the limit is exceeded.
|
||||
///
|
||||
/// The text cursor is not shown if [showCursor] is false or if [showCursor]
|
||||
/// is null (the default) and [readOnly] is true.
|
||||
///
|
||||
/// The [selectionHeightStyle] and [selectionWidthStyle] properties allow
|
||||
/// changing the shape of the selection highlighting. These properties default
|
||||
/// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively and
|
||||
/// must not be null.
|
||||
///
|
||||
/// The [textAlign], [autofocus], [obscureText], [readOnly], [autocorrect],
|
||||
/// [maxLengthEnforced], [scrollPadding], [maxLines], [maxLength],
|
||||
/// [selectionHeightStyle], [selectionWidthStyle], [enableSuggestions], and
|
||||
/// [enableIMEPersonalizedLearning] arguments must not be null.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [maxLength], which discusses the precise meaning of "number of
|
||||
/// characters" and how it may differ from the intuitive meaning.
|
||||
const StreamMessageTextField({
|
||||
Key? key,
|
||||
this.controller,
|
||||
this.focusNode,
|
||||
this.decoration = const InputDecoration(),
|
||||
TextInputType? keyboardType,
|
||||
this.textInputAction,
|
||||
this.textCapitalization = TextCapitalization.none,
|
||||
this.style,
|
||||
this.strutStyle,
|
||||
this.textAlign = TextAlign.start,
|
||||
this.textAlignVertical,
|
||||
this.textDirection,
|
||||
this.readOnly = false,
|
||||
ToolbarOptions? toolbarOptions,
|
||||
this.showCursor,
|
||||
this.autofocus = false,
|
||||
this.obscuringCharacter = '•',
|
||||
this.obscureText = false,
|
||||
this.autocorrect = true,
|
||||
SmartDashesType? smartDashesType,
|
||||
SmartQuotesType? smartQuotesType,
|
||||
this.enableSuggestions = true,
|
||||
this.maxLines = 1,
|
||||
this.minLines,
|
||||
this.expands = false,
|
||||
this.maxLength,
|
||||
@Deprecated(
|
||||
'Use maxLengthEnforcement parameter which provides more specific '
|
||||
'behavior related to the maxLength limit. '
|
||||
'This feature was deprecated after v1.25.0-5.0.pre.',
|
||||
)
|
||||
this.maxLengthEnforced = true,
|
||||
this.maxLengthEnforcement,
|
||||
this.onChanged,
|
||||
this.onEditingComplete,
|
||||
this.onSubmitted,
|
||||
this.onAppPrivateCommand,
|
||||
this.inputFormatters,
|
||||
this.enabled,
|
||||
this.cursorWidth = 2.0,
|
||||
this.cursorHeight,
|
||||
this.cursorRadius,
|
||||
this.cursorColor,
|
||||
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
|
||||
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
|
||||
this.keyboardAppearance,
|
||||
this.scrollPadding = const EdgeInsets.all(20.0),
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.enableInteractiveSelection = true,
|
||||
this.selectionControls,
|
||||
this.onTap,
|
||||
this.mouseCursor,
|
||||
this.buildCounter,
|
||||
this.scrollController,
|
||||
this.scrollPhysics,
|
||||
this.autofillHints,
|
||||
this.restorationId,
|
||||
this.enableIMEPersonalizedLearning = true,
|
||||
}) : assert(textAlign != null),
|
||||
assert(readOnly != null),
|
||||
assert(autofocus != null),
|
||||
assert(obscuringCharacter != null && obscuringCharacter.length == 1),
|
||||
assert(obscureText != null),
|
||||
assert(autocorrect != null),
|
||||
smartDashesType = smartDashesType ??
|
||||
(obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
|
||||
smartQuotesType = smartQuotesType ??
|
||||
(obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
||||
assert(enableSuggestions != null),
|
||||
assert(enableInteractiveSelection != null),
|
||||
assert(maxLengthEnforced != null),
|
||||
assert(
|
||||
maxLengthEnforced || maxLengthEnforcement == null,
|
||||
'maxLengthEnforced is deprecated, use only maxLengthEnforcement',
|
||||
),
|
||||
assert(scrollPadding != null),
|
||||
assert(dragStartBehavior != null),
|
||||
assert(selectionHeightStyle != null),
|
||||
assert(selectionWidthStyle != 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(expands != null),
|
||||
assert(
|
||||
!expands || (maxLines == null && minLines == null),
|
||||
'minLines and maxLines must be null when expands is true.',
|
||||
),
|
||||
assert(!obscureText || maxLines == 1,
|
||||
'Obscured fields cannot be multiline.'),
|
||||
assert(maxLength == null ||
|
||||
maxLength == TextField.noMaxLength ||
|
||||
maxLength > 0),
|
||||
// Assert the following instead of setting it directly to avoid surprising the user by silently changing the value they set.
|
||||
assert(
|
||||
!identical(textInputAction, TextInputAction.newline) ||
|
||||
maxLines == 1 ||
|
||||
!identical(keyboardType, TextInputType.text),
|
||||
'Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.',
|
||||
),
|
||||
assert(enableIMEPersonalizedLearning != null),
|
||||
keyboardType = keyboardType ??
|
||||
(maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
||||
toolbarOptions = toolbarOptions ??
|
||||
(obscureText
|
||||
? const ToolbarOptions(
|
||||
selectAll: true,
|
||||
paste: true,
|
||||
)
|
||||
: const ToolbarOptions(
|
||||
copy: true,
|
||||
cut: true,
|
||||
selectAll: true,
|
||||
paste: true,
|
||||
)),
|
||||
super(key: key);
|
||||
|
||||
/// Controls the message being edited.
|
||||
///
|
||||
/// If null, this widget will create its own [MessageInputController].
|
||||
final MessageInputController? controller;
|
||||
|
||||
/// Defines the keyboard focus for this widget.
|
||||
///
|
||||
/// The [focusNode] is a long-lived object that's typically managed by a
|
||||
/// [StatefulWidget] parent. See [FocusNode] for more information.
|
||||
///
|
||||
/// To give the keyboard 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].
|
||||
///
|
||||
/// ## Keyboard
|
||||
///
|
||||
/// Requesting the focus will typically cause the keyboard to be shown
|
||||
/// if it's not showing already.
|
||||
///
|
||||
/// On Android, the user can hide the keyboard - without changing the focus -
|
||||
/// with the system back button. They can restore the keyboard's visibility
|
||||
/// by tapping on a text field. The user might hide the keyboard and
|
||||
/// switch to a physical keyboard, or they might just need to get it
|
||||
/// out of the way for a moment, to expose something it's
|
||||
/// obscuring. In this case requesting the focus again will not
|
||||
/// cause the focus to change, and will not make the keyboard visible.
|
||||
///
|
||||
/// This widget builds an [EditableText] and will ensure that the keyboard is
|
||||
/// showing when it is tapped by calling [EditableTextState.requestKeyboard()].
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// The decoration to show around the text field.
|
||||
///
|
||||
/// By default, draws a horizontal line under the text field but can be
|
||||
/// configured to show an icon, label, hint text, and error text.
|
||||
///
|
||||
/// Specify null to remove the decoration entirely (including the
|
||||
/// extra padding introduced by the decoration to save space for the labels).
|
||||
final InputDecoration? decoration;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.keyboardType}
|
||||
final TextInputType keyboardType;
|
||||
|
||||
/// The type of action button to use for the keyboard.
|
||||
///
|
||||
/// Defaults to [TextInputAction.newline] if [keyboardType] is
|
||||
/// [TextInputType.multiline] and [TextInputAction.done] otherwise.
|
||||
final TextInputAction? textInputAction;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.textCapitalization}
|
||||
final TextCapitalization textCapitalization;
|
||||
|
||||
/// The style to use for the text being edited.
|
||||
///
|
||||
/// This text style is also used as the base style for the [decoration].
|
||||
///
|
||||
/// If null, defaults to the `subtitle1` text style from the current [Theme].
|
||||
final TextStyle? style;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.strutStyle}
|
||||
final StrutStyle? strutStyle;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.textAlign}
|
||||
final TextAlign textAlign;
|
||||
|
||||
/// {@macro flutter.material.InputDecorator.textAlignVertical}
|
||||
final TextAlignVertical? textAlignVertical;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.textDirection}
|
||||
final TextDirection? textDirection;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.obscuringCharacter}
|
||||
final String obscuringCharacter;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.obscureText}
|
||||
final bool obscureText;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.autocorrect}
|
||||
final bool autocorrect;
|
||||
|
||||
/// {@macro flutter.services.TextInputConfiguration.smartDashesType}
|
||||
final SmartDashesType smartDashesType;
|
||||
|
||||
/// {@macro flutter.services.TextInputConfiguration.smartQuotesType}
|
||||
final SmartQuotesType smartQuotesType;
|
||||
|
||||
/// {@macro flutter.services.TextInputConfiguration.enableSuggestions}
|
||||
final bool enableSuggestions;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.maxLines}
|
||||
/// * [expands], which determines whether the field should fill the height of
|
||||
/// its parent.
|
||||
final int? maxLines;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.minLines}
|
||||
/// * [expands], which determines whether the field should fill the height of
|
||||
/// its parent.
|
||||
final int? minLines;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.expands}
|
||||
final bool expands;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.readOnly}
|
||||
final bool readOnly;
|
||||
|
||||
/// Configuration of toolbar options.
|
||||
///
|
||||
/// If not set, select all and paste will default to be enabled. Copy and cut
|
||||
/// will be disabled if [obscureText] is true. If [readOnly] is true,
|
||||
/// paste and cut will be disabled regardless.
|
||||
final ToolbarOptions toolbarOptions;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.showCursor}
|
||||
final bool? showCursor;
|
||||
|
||||
/// If [maxLength] is set to this value, only the "current input length"
|
||||
/// part of the character counter is shown.
|
||||
static const int noMaxLength = -1;
|
||||
|
||||
/// The maximum number of characters (Unicode scalar values) to allow in the
|
||||
/// text field.
|
||||
///
|
||||
/// If set, a character counter will be displayed below the
|
||||
/// field showing how many characters have been entered. If set to a number
|
||||
/// greater than 0, it will also display the maximum number allowed. If set
|
||||
/// to [TextField.noMaxLength] then only the current character count is displayed.
|
||||
///
|
||||
/// After [maxLength] characters have been input, additional input
|
||||
/// is ignored, unless [maxLengthEnforcement] is set to
|
||||
/// [MaxLengthEnforcement.none].
|
||||
///
|
||||
/// The text field enforces the length with a [LengthLimitingTextInputFormatter],
|
||||
/// which is evaluated after the supplied [inputFormatters], if any.
|
||||
///
|
||||
/// This value must be either null, [TextField.noMaxLength], or greater than 0.
|
||||
/// If null (the default) then there is no limit to the number of characters
|
||||
/// that can be entered. If set to [TextField.noMaxLength], then no limit will
|
||||
/// be enforced, but the number of characters entered will still be displayed.
|
||||
///
|
||||
/// Whitespace characters (e.g. newline, space, tab) are included in the
|
||||
/// character count.
|
||||
///
|
||||
/// If [maxLengthEnforced] is set to false or [maxLengthEnforcement] is
|
||||
/// [MaxLengthEnforcement.none], then more than [maxLength]
|
||||
/// characters may be entered, but the error counter and divider will switch
|
||||
/// to the [decoration]'s [InputDecoration.errorStyle] when the limit is
|
||||
/// exceeded.
|
||||
///
|
||||
/// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength}
|
||||
final int? maxLength;
|
||||
|
||||
/// If [maxLength] is set, [maxLengthEnforced] indicates whether or not to
|
||||
/// enforce the limit, or merely provide a character counter and warning when
|
||||
/// [maxLength] is exceeded.
|
||||
///
|
||||
/// If true, prevents the field from allowing more than [maxLength]
|
||||
/// characters.
|
||||
@Deprecated(
|
||||
'Use maxLengthEnforcement parameter which provides more specific '
|
||||
'behavior related to the maxLength limit. '
|
||||
'This feature was deprecated after v1.25.0-5.0.pre.',
|
||||
)
|
||||
final bool maxLengthEnforced;
|
||||
|
||||
/// Determines how the [maxLength] limit should be enforced.
|
||||
///
|
||||
/// {@macro flutter.services.textFormatter.effectiveMaxLengthEnforcement}
|
||||
///
|
||||
/// {@macro flutter.services.textFormatter.maxLengthEnforcement}
|
||||
final MaxLengthEnforcement? maxLengthEnforcement;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.onChanged}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [inputFormatters], which are called before [onChanged]
|
||||
/// runs and can validate and change ("format") the input value.
|
||||
/// * [onEditingComplete], [onSubmitted]:
|
||||
/// which are more specialized input change notifications.
|
||||
final ValueChanged<String>? onChanged;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.onEditingComplete}
|
||||
final VoidCallback? onEditingComplete;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.onSubmitted}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextInputAction.next] and [TextInputAction.previous], which
|
||||
/// automatically shift the focus to the next/previous focusable item when
|
||||
/// the user is done editing.
|
||||
final ValueChanged<String>? onSubmitted;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.onAppPrivateCommand}
|
||||
final AppPrivateCommandCallback? onAppPrivateCommand;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.inputFormatters}
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
|
||||
/// If false the text field is "disabled": it ignores taps and its
|
||||
/// [decoration] is rendered in grey.
|
||||
///
|
||||
/// If non-null this property overrides the [decoration]'s
|
||||
/// [InputDecoration.enabled] property.
|
||||
final bool? enabled;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.cursorWidth}
|
||||
final double cursorWidth;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.cursorHeight}
|
||||
final double? cursorHeight;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.cursorRadius}
|
||||
final Radius? cursorRadius;
|
||||
|
||||
/// The color of the cursor.
|
||||
///
|
||||
/// The cursor indicates the current location of text insertion point in
|
||||
/// the field.
|
||||
///
|
||||
/// If this is null it will default to the ambient
|
||||
/// [TextSelectionThemeData.cursorColor]. If that is null, and the
|
||||
/// [ThemeData.platform] is [TargetPlatform.iOS] or [TargetPlatform.macOS]
|
||||
/// it will use [CupertinoThemeData.primaryColor]. Otherwise it will use
|
||||
/// the value of [ColorScheme.primary] of [ThemeData.colorScheme].
|
||||
final Color? cursorColor;
|
||||
|
||||
/// Controls how tall the selection highlight boxes are computed to be.
|
||||
///
|
||||
/// See [ui.BoxHeightStyle] for details on available styles.
|
||||
final ui.BoxHeightStyle selectionHeightStyle;
|
||||
|
||||
/// Controls how wide the selection highlight boxes are computed to be.
|
||||
///
|
||||
/// See [ui.BoxWidthStyle] for details on available styles.
|
||||
final ui.BoxWidthStyle selectionWidthStyle;
|
||||
|
||||
/// The appearance of the keyboard.
|
||||
///
|
||||
/// This setting is only honored on iOS devices.
|
||||
///
|
||||
/// If unset, defaults to the brightness of [ThemeData.primaryColorBrightness].
|
||||
final Brightness? keyboardAppearance;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.scrollPadding}
|
||||
final EdgeInsets scrollPadding;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.enableInteractiveSelection}
|
||||
final bool enableInteractiveSelection;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.selectionControls}
|
||||
final TextSelectionControls? selectionControls;
|
||||
|
||||
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
||||
final DragStartBehavior dragStartBehavior;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.selectionEnabled}
|
||||
bool get selectionEnabled => enableInteractiveSelection;
|
||||
|
||||
/// {@template flutter.material.textfield.onTap}
|
||||
/// Called for each distinct tap except for every second tap of a double tap.
|
||||
///
|
||||
/// The text field 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 text field with a competing
|
||||
/// GestureDetector is problematic.
|
||||
///
|
||||
/// To unconditionally handle taps, without interfering with the text field's
|
||||
/// internal gesture detector, provide this callback.
|
||||
///
|
||||
/// If the text field is created with [enabled] false, taps will not be
|
||||
/// recognized.
|
||||
///
|
||||
/// 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
|
||||
/// text field's internal gesture detector, use a [Listener].
|
||||
/// {@endtemplate}
|
||||
final GestureTapCallback? onTap;
|
||||
|
||||
/// The cursor for a mouse pointer when it enters or is hovering over the
|
||||
/// widget.
|
||||
///
|
||||
/// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
|
||||
/// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
|
||||
///
|
||||
/// * [MaterialState.error].
|
||||
/// * [MaterialState.hovered].
|
||||
/// * [MaterialState.focused].
|
||||
/// * [MaterialState.disabled].
|
||||
///
|
||||
/// If this property is null, [MaterialStateMouseCursor.textable] will be used.
|
||||
///
|
||||
/// The [mouseCursor] is the only property of [TextField] that controls the
|
||||
/// appearance of the mouse pointer. All other properties related to "cursor"
|
||||
/// stand for the text cursor, which is usually a blinking vertical line at
|
||||
/// the editing position.
|
||||
final MouseCursor? mouseCursor;
|
||||
|
||||
/// Callback that generates a custom [InputDecoration.counter] widget.
|
||||
///
|
||||
/// See [InputCounterWidgetBuilder] for an explanation of the passed in
|
||||
/// arguments. The returned widget will be placed below the line in place of
|
||||
/// the default widget built when [InputDecoration.counterText] is specified.
|
||||
///
|
||||
/// The returned widget will be wrapped in a [Semantics] widget for
|
||||
/// accessibility, but it also needs to be accessible itself. For example,
|
||||
/// if returning a Text widget, set the [Text.semanticsLabel] property.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
/// ```dart
|
||||
/// Widget counter(
|
||||
/// BuildContext context,
|
||||
/// {
|
||||
/// required int currentLength,
|
||||
/// required int? maxLength,
|
||||
/// required bool isFocused,
|
||||
/// }
|
||||
/// ) {
|
||||
/// return Text(
|
||||
/// '$currentLength of $maxLength characters',
|
||||
/// semanticsLabel: 'character count',
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// If buildCounter returns null, then no counter and no Semantics widget will
|
||||
/// be created at all.
|
||||
final InputCounterWidgetBuilder? buildCounter;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.scrollPhysics}
|
||||
final ScrollPhysics? scrollPhysics;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.scrollController}
|
||||
final ScrollController? scrollController;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.autofillHints}
|
||||
/// {@macro flutter.services.AutofillConfiguration.autofillHints}
|
||||
final Iterable<String>? autofillHints;
|
||||
|
||||
/// {@template flutter.material.textfield.restorationId}
|
||||
/// Restoration ID to save and restore the state of the text field.
|
||||
///
|
||||
/// If non-null, the text field will persist and restore its current scroll
|
||||
/// offset and - if no [controller] has been provided - the content of the
|
||||
/// text field. If a [controller] has been provided, it is the responsibility
|
||||
/// of the owner of that controller to persist and restore it, e.g. by using
|
||||
/// a [RestorableTextEditingController].
|
||||
///
|
||||
/// The state of this widget is persisted in a [RestorationBucket] claimed
|
||||
/// from the surrounding [RestorationScope] using the provided restoration ID.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [RestorationManager], which explains how state restoration works in
|
||||
/// Flutter.
|
||||
/// {@endtemplate}
|
||||
final String? restorationId;
|
||||
|
||||
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
|
||||
final bool enableIMEPersonalizedLearning;
|
||||
|
||||
@override
|
||||
_StreamMessageTextFieldState createState() => _StreamMessageTextFieldState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode,
|
||||
defaultValue: null));
|
||||
properties
|
||||
.add(DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<InputDecoration>(
|
||||
'decoration', decoration,
|
||||
defaultValue: const InputDecoration()));
|
||||
properties.add(DiagnosticsProperty<TextInputType>(
|
||||
'keyboardType', keyboardType,
|
||||
defaultValue: TextInputType.text));
|
||||
properties.add(
|
||||
DiagnosticsProperty<TextStyle>('style', style, defaultValue: null));
|
||||
properties.add(
|
||||
DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
|
||||
properties.add(DiagnosticsProperty<String>(
|
||||
'obscuringCharacter', obscuringCharacter,
|
||||
defaultValue: '•'));
|
||||
properties.add(DiagnosticsProperty<bool>('obscureText', obscureText,
|
||||
defaultValue: false));
|
||||
properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect,
|
||||
defaultValue: true));
|
||||
properties.add(EnumProperty<SmartDashesType>(
|
||||
'smartDashesType', smartDashesType,
|
||||
defaultValue:
|
||||
obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
|
||||
properties.add(EnumProperty<SmartQuotesType>(
|
||||
'smartQuotesType', smartQuotesType,
|
||||
defaultValue:
|
||||
obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled));
|
||||
properties.add(DiagnosticsProperty<bool>(
|
||||
'enableSuggestions', enableSuggestions,
|
||||
defaultValue: true));
|
||||
properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
|
||||
properties.add(IntProperty('minLines', minLines, defaultValue: null));
|
||||
properties.add(
|
||||
DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
|
||||
properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
|
||||
properties.add(FlagProperty('maxLengthEnforced',
|
||||
value: maxLengthEnforced,
|
||||
defaultValue: true,
|
||||
ifFalse: 'maxLength not enforced'));
|
||||
properties.add(EnumProperty<MaxLengthEnforcement>(
|
||||
'maxLengthEnforcement', maxLengthEnforcement,
|
||||
defaultValue: null));
|
||||
properties.add(EnumProperty<TextInputAction>(
|
||||
'textInputAction', textInputAction,
|
||||
defaultValue: null));
|
||||
properties.add(EnumProperty<TextCapitalization>(
|
||||
'textCapitalization', textCapitalization,
|
||||
defaultValue: TextCapitalization.none));
|
||||
properties.add(EnumProperty<TextAlign>('textAlign', textAlign,
|
||||
defaultValue: TextAlign.start));
|
||||
properties.add(DiagnosticsProperty<TextAlignVertical>(
|
||||
'textAlignVertical', textAlignVertical,
|
||||
defaultValue: null));
|
||||
properties.add(EnumProperty<TextDirection>('textDirection', textDirection,
|
||||
defaultValue: null));
|
||||
properties
|
||||
.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
|
||||
properties
|
||||
.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius,
|
||||
defaultValue: null));
|
||||
properties
|
||||
.add(ColorProperty('cursorColor', cursorColor, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<Brightness>(
|
||||
'keyboardAppearance', keyboardAppearance,
|
||||
defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>(
|
||||
'scrollPadding', scrollPadding,
|
||||
defaultValue: const EdgeInsets.all(20.0)));
|
||||
properties.add(FlagProperty('selectionEnabled',
|
||||
value: selectionEnabled,
|
||||
defaultValue: true,
|
||||
ifFalse: 'selection disabled'));
|
||||
properties.add(DiagnosticsProperty<TextSelectionControls>(
|
||||
'selectionControls', selectionControls,
|
||||
defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<ScrollController>(
|
||||
'scrollController', scrollController,
|
||||
defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<ScrollPhysics>(
|
||||
'scrollPhysics', scrollPhysics,
|
||||
defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<bool>(
|
||||
'enableIMEPersonalizedLearning', enableIMEPersonalizedLearning,
|
||||
defaultValue: true));
|
||||
}
|
||||
}
|
||||
|
||||
class _StreamMessageTextFieldState extends State<StreamMessageTextField>
|
||||
with RestorationMixin<StreamMessageTextField> {
|
||||
RestorableMessageInputController? _controller;
|
||||
|
||||
MessageInputController get _effectiveController =>
|
||||
widget.controller ?? _controller!.value;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.controller == null) {
|
||||
_createLocalController();
|
||||
}
|
||||
}
|
||||
|
||||
void _createLocalController([Message? message]) {
|
||||
assert(_controller == null, '');
|
||||
_controller = RestorableMessageInputController(message: message);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant StreamMessageTextField oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.controller == null && oldWidget.controller != null) {
|
||||
_createLocalController(oldWidget.controller!.value);
|
||||
} else if (widget.controller != null && oldWidget.controller == null) {
|
||||
unregisterFromRestoration(_controller!);
|
||||
_controller!.dispose();
|
||||
_controller = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
|
||||
if (_controller != null) {
|
||||
_registerController();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String? get restorationId => widget.restorationId;
|
||||
|
||||
void _registerController() {
|
||||
assert(_controller != null, '');
|
||||
registerForRestoration(_controller!, 'controller');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => TextField(
|
||||
key: widget.key,
|
||||
controller: _effectiveController.textEditingController,
|
||||
onChanged: (newText) {
|
||||
_effectiveController.text = newText;
|
||||
},
|
||||
focusNode: widget.focusNode,
|
||||
decoration: widget.decoration,
|
||||
keyboardType: widget.keyboardType,
|
||||
textInputAction: widget.textInputAction,
|
||||
textCapitalization: widget.textCapitalization,
|
||||
style: widget.style,
|
||||
strutStyle: widget.strutStyle,
|
||||
textAlign: widget.textAlign,
|
||||
textAlignVertical: widget.textAlignVertical,
|
||||
textDirection: widget.textDirection,
|
||||
readOnly: widget.readOnly,
|
||||
toolbarOptions: widget.toolbarOptions,
|
||||
showCursor: widget.showCursor,
|
||||
autofocus: widget.autofocus,
|
||||
obscuringCharacter: widget.obscuringCharacter,
|
||||
obscureText: widget.obscureText,
|
||||
autocorrect: widget.autocorrect,
|
||||
smartDashesType: widget.smartDashesType,
|
||||
smartQuotesType: widget.smartQuotesType,
|
||||
enableSuggestions: widget.enableSuggestions,
|
||||
maxLines: widget.maxLines,
|
||||
minLines: widget.minLines,
|
||||
expands: widget.expands,
|
||||
maxLength: widget.maxLength,
|
||||
maxLengthEnforced: widget.maxLengthEnforced,
|
||||
maxLengthEnforcement: widget.maxLengthEnforcement,
|
||||
onEditingComplete: widget.onEditingComplete,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
onAppPrivateCommand: widget.onAppPrivateCommand,
|
||||
inputFormatters: widget.inputFormatters,
|
||||
enabled: widget.enabled,
|
||||
cursorWidth: widget.cursorWidth,
|
||||
cursorHeight: widget.cursorHeight,
|
||||
cursorRadius: widget.cursorRadius,
|
||||
cursorColor: widget.cursorColor,
|
||||
selectionHeightStyle: widget.selectionHeightStyle,
|
||||
selectionWidthStyle: widget.selectionWidthStyle,
|
||||
keyboardAppearance: widget.keyboardAppearance,
|
||||
scrollPadding: widget.scrollPadding,
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
enableInteractiveSelection: widget.enableInteractiveSelection,
|
||||
selectionControls: widget.selectionControls,
|
||||
onTap: widget.onTap,
|
||||
mouseCursor: widget.mouseCursor,
|
||||
buildCounter: widget.buildCounter,
|
||||
scrollController: widget.scrollController,
|
||||
scrollPhysics: widget.scrollPhysics,
|
||||
autofillHints: widget.autofillHints,
|
||||
restorationId: widget.restorationId,
|
||||
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user