html_editor_enhanced/lib/utils/utils.dart

1202 lines
34 KiB
Dart
Raw Normal View History

2022-10-30 15:25:33 +07:00
import 'dart:convert';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:html_editor_enhanced/html_editor.dart';
import 'package:html_editor_enhanced/utils/shims/dart_ui.dart';
/// small function to always check if mounted before running setState()
void setState(
bool mounted, void Function(Function()) setState, void Function() fn) {
if (mounted) {
setState.call(fn);
}
}
/// courtesy of @modulovalue (https://github.com/modulovalue/dart_intersperse/blob/master/lib/src/intersperse.dart)
/// intersperses elements in between list items - used to insert dividers between
/// toolbar buttons when using [ToolbarType.nativeScrollable]
Iterable<T> intersperse<T>(T element, Iterable<T> iterable) sync* {
final iterator = iterable.iterator;
if (iterator.moveNext()) {
yield iterator.current;
while (iterator.moveNext()) {
yield element;
yield iterator.current;
}
}
}
/// Generates a random string to be used as the [VisibilityDetector] key.
/// Technically this limits the number of editors to a finite number, but
/// nobody will be embedding enough editors to reach the theoretical limit
/// (yes, this is a challenge ;-) )
String getRandString(int len) {
var random = Random.secure();
var values = List<int>.generate(len, (i) => random.nextInt(255));
return base64UrlEncode(values);
}
/// Class that helps pass editor settings to the [onSettingsChange] callback
class EditorSettings {
String parentElement;
String fontName;
double fontSize;
bool isBold;
bool isItalic;
bool isUnderline;
bool isStrikethrough;
bool isSuperscript;
bool isSubscript;
Color foregroundColor;
Color backgroundColor;
bool isUl;
bool isOl;
bool isAlignLeft;
bool isAlignCenter;
bool isAlignRight;
bool isAlignJustify;
double lineHeight;
TextDirection textDirection;
EditorSettings({
required this.parentElement,
required this.fontName,
required this.fontSize,
required this.isBold,
required this.isItalic,
required this.isUnderline,
required this.isStrikethrough,
required this.isSuperscript,
required this.isSubscript,
required this.foregroundColor,
required this.backgroundColor,
required this.isUl,
required this.isOl,
required this.isAlignLeft,
required this.isAlignCenter,
required this.isAlignRight,
required this.isAlignJustify,
required this.lineHeight,
required this.textDirection,
});
}
/// Class to create a script that can be run on Flutter Web.
///
/// [name] provides a unique identifier for the script. Note: It must be unique!
/// Otherwise your script may not be called when using [controller.evaluateJavascriptWeb].
/// [script] provides the script itself. If you'd like to return a value back to
/// Dart, you can do that via a postMessage call (see the README for an example).
class WebScript {
String name;
String script;
WebScript({
required this.name,
required this.script,
}) : assert(name.isNotEmpty && script.isNotEmpty);
}
/// Delegate for the icon that controls the expansion status of the toolbar
class ExpandIconDelegate extends SliverPersistentHeaderDelegate {
final double? _size;
final bool _isExpanded;
final void Function() _setState;
ExpandIconDelegate(this._size, this._isExpanded, this._setState);
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
height: _size,
width: _size,
color: Theme.of(context).scaffoldBackgroundColor,
child: IconButton(
constraints: BoxConstraints(
maxHeight: _size!,
maxWidth: _size!,
),
iconSize: _size! * 3 / 5,
icon: Icon(
_isExpanded ? Icons.expand_less : Icons.expand_more,
color: Colors.grey,
),
onPressed: () async {
_setState.call();
},
),
);
}
@override
double get maxExtent => _size!;
@override
double get minExtent => _size!;
@override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}
/// The following code contains all the code necessary for custom dropdowns.
/// It is really long because dropdowns utilize a bunch of private classes that
/// must be copy pasted.
/// The main change is marked with a comment in the code (CTRL-F "main change")
const Duration _kDropdownMenuDuration = Duration(milliseconds: 300);
const double _kMenuItemHeight = kMinInteractiveDimension;
const double _kDenseButtonHeight = 24.0;
const EdgeInsets _kMenuItemPadding = EdgeInsets.symmetric(horizontal: 16.0);
const EdgeInsetsGeometry _kAlignedButtonPadding =
EdgeInsetsDirectional.only(start: 16.0, end: 4.0);
const EdgeInsets _kUnalignedButtonPadding = EdgeInsets.zero;
const EdgeInsets _kAlignedMenuMargin = EdgeInsets.zero;
const EdgeInsetsGeometry _kUnalignedMenuMargin =
EdgeInsetsDirectional.only(start: 16.0, end: 24.0);
typedef DropdownButtonBuilder = List<Widget> Function(BuildContext context);
class _DropdownMenuPainter extends CustomPainter {
_DropdownMenuPainter({
this.color,
this.elevation,
this.selectedIndex,
required this.resize,
required this.getSelectedItemOffset,
}) : _painter = BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(2.0),
boxShadow: kElevationToShadow[elevation],
).createBoxPainter(),
super(repaint: resize);
final Color? color;
final int? elevation;
final int? selectedIndex;
final Animation<double> resize;
final ValueGetter<double> getSelectedItemOffset;
final BoxPainter _painter;
@override
void paint(Canvas canvas, Size size) {
final selectedItemOffset = getSelectedItemOffset();
final top = Tween<double>(
begin: selectedItemOffset.clamp(
0.0, max(size.height - _kMenuItemHeight, 0.0)),
end: 0.0,
);
final bottom = Tween<double>(
begin: (top.begin! + _kMenuItemHeight)
.clamp(min(_kMenuItemHeight, size.height), size.height),
end: size.height,
);
final rect = Rect.fromLTRB(
0.0, top.evaluate(resize), size.width, bottom.evaluate(resize));
_painter.paint(canvas, rect.topLeft, ImageConfiguration(size: rect.size));
}
@override
bool shouldRepaint(_DropdownMenuPainter oldPainter) {
return oldPainter.color != color ||
oldPainter.elevation != elevation ||
oldPainter.selectedIndex != selectedIndex ||
oldPainter.resize != resize;
}
}
class _DropdownMenuItemButton<T> extends StatefulWidget {
const _DropdownMenuItemButton({
Key? key,
this.padding,
required this.route,
required this.buttonRect,
required this.constraints,
required this.itemIndex,
}) : super(key: key);
final _DropdownRoute<T> route;
final EdgeInsets? padding;
final Rect buttonRect;
final BoxConstraints constraints;
final int itemIndex;
@override
_DropdownMenuItemButtonState<T> createState() =>
_DropdownMenuItemButtonState<T>();
}
class _DropdownMenuItemButtonState<T>
extends State<_DropdownMenuItemButton<T>> {
void _handleFocusChange(bool focused) {
final bool inTraditionalMode;
switch (FocusManager.instance.highlightMode) {
case FocusHighlightMode.touch:
inTraditionalMode = false;
break;
case FocusHighlightMode.traditional:
inTraditionalMode = true;
break;
}
if (focused && inTraditionalMode) {
final menuLimits = widget.route.getMenuLimits(
widget.buttonRect,
widget.constraints.maxHeight,
widget.itemIndex,
);
widget.route.scrollController!.animateTo(
menuLimits.scrollOffset,
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 100),
);
}
}
void _handleOnTap() {
final dropdownMenuItem = widget.route.items[widget.itemIndex].item!;
dropdownMenuItem.onTap?.call();
Navigator.pop(
context,
_DropdownRouteResult<T>(dropdownMenuItem.value),
);
}
static final Map<LogicalKeySet, Intent> _webShortcuts =
<LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.arrowDown):
const DirectionalFocusIntent(TraversalDirection.down),
LogicalKeySet(LogicalKeyboardKey.arrowUp):
const DirectionalFocusIntent(TraversalDirection.up),
};
@override
Widget build(BuildContext context) {
final CurvedAnimation opacity;
final unit = 0.5 / (widget.route.items.length + 1.5);
if (widget.itemIndex == widget.route.selectedIndex) {
opacity = CurvedAnimation(
parent: widget.route.animation!, curve: const Threshold(0.0));
} else {
final start = (0.5 + (widget.itemIndex + 1) * unit).clamp(0.0, 1.0);
final end = (start + 1.5 * unit).clamp(0.0, 1.0);
opacity = CurvedAnimation(
parent: widget.route.animation!, curve: Interval(start, end));
}
Widget child = FadeTransition(
opacity: opacity,
child: InkWell(
autofocus: widget.itemIndex == widget.route.selectedIndex,
onTap: _handleOnTap,
onFocusChange: _handleFocusChange,
child: Container(
padding: widget.padding,
child: widget.route.items[widget.itemIndex],
),
),
);
if (kIsWeb) {
child = Shortcuts(
shortcuts: _webShortcuts,
child: child,
);
}
return child;
}
}
class _DropdownMenu<T> extends StatefulWidget {
const _DropdownMenu({
Key? key,
this.padding,
required this.route,
required this.buttonRect,
required this.constraints,
this.dropdownColor,
}) : super(key: key);
final _DropdownRoute<T> route;
final EdgeInsets? padding;
final Rect buttonRect;
final BoxConstraints constraints;
final Color? dropdownColor;
@override
_DropdownMenuState<T> createState() => _DropdownMenuState<T>();
}
class _DropdownMenuState<T> extends State<_DropdownMenu<T>> {
late CurvedAnimation _fadeOpacity;
late CurvedAnimation _resize;
@override
void initState() {
super.initState();
_fadeOpacity = CurvedAnimation(
parent: widget.route.animation!,
curve: const Interval(0.0, 0.25),
reverseCurve: const Interval(0.75, 1.0),
);
_resize = CurvedAnimation(
parent: widget.route.animation!,
curve: const Interval(0.25, 0.5),
reverseCurve: const Threshold(0.0),
);
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
final localizations = MaterialLocalizations.of(context);
final route = widget.route;
final children = <Widget>[
for (int itemIndex = 0; itemIndex < route.items.length; ++itemIndex)
_DropdownMenuItemButton<T>(
route: widget.route,
padding: widget.padding,
buttonRect: widget.buttonRect,
constraints: widget.constraints,
itemIndex: itemIndex,
),
];
return FadeTransition(
opacity: _fadeOpacity,
child: CustomPaint(
painter: _DropdownMenuPainter(
color: widget.dropdownColor ?? Theme.of(context).canvasColor,
elevation: route.elevation,
selectedIndex: route.selectedIndex,
resize: _resize,
getSelectedItemOffset: () => route.getItemOffset(route.selectedIndex),
),
child: Semantics(
scopesRoute: true,
namesRoute: true,
explicitChildNodes: true,
label: localizations.popupMenuLabel,
child: Material(
type: MaterialType.transparency,
textStyle: route.style,
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
overscroll: false,
physics: const ClampingScrollPhysics(),
platform: Theme.of(context).platform,
),
child: PrimaryScrollController(
controller: widget.route.scrollController!,
child: Scrollbar(
thumbVisibility: true,
child: ListView(
padding: kMaterialListPadding,
shrinkWrap: true,
children: children,
),
),
),
),
),
),
),
);
}
}
class _DropdownMenuRouteLayout<T> extends SingleChildLayoutDelegate {
_DropdownMenuRouteLayout({
required this.buttonRect,
required this.route,
required this.textDirection,
});
final Rect buttonRect;
final _DropdownRoute<T> route;
final TextDirection? textDirection;
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
var maxHeight = max(0.0, constraints.maxHeight - 2 * _kMenuItemHeight);
if (route.menuMaxHeight != null && route.menuMaxHeight! <= maxHeight) {
maxHeight = route.menuMaxHeight!;
}
final width = min(constraints.maxWidth, buttonRect.width);
return BoxConstraints(
minWidth: width,
maxWidth: width,
minHeight: 0.0,
maxHeight: maxHeight,
);
}
@override
Offset getPositionForChild(Size size, Size childSize) {
final menuLimits =
route.getMenuLimits(buttonRect, size.height, route.selectedIndex);
assert(() {
final container = Offset.zero & size;
if (container.intersect(buttonRect) == buttonRect) {
assert(menuLimits.top >= 0.0);
assert(menuLimits.top + menuLimits.height <= size.height);
}
return true;
}());
assert(textDirection != null);
final double left;
switch (textDirection!) {
case TextDirection.rtl:
left = buttonRect.right.clamp(0.0, size.width) - childSize.width;
break;
case TextDirection.ltr:
left = buttonRect.left.clamp(0.0, size.width - childSize.width);
break;
}
return Offset(left, menuLimits.top);
}
@override
bool shouldRelayout(_DropdownMenuRouteLayout<T> oldDelegate) {
return buttonRect != oldDelegate.buttonRect ||
textDirection != oldDelegate.textDirection;
}
}
@immutable
class _DropdownRouteResult<T> {
const _DropdownRouteResult(this.result);
final T? result;
@override
bool operator ==(Object other) {
return other is _DropdownRouteResult<T> && other.result == result;
}
@override
int get hashCode => result.hashCode;
}
class _MenuLimits {
const _MenuLimits(this.top, this.bottom, this.height, this.scrollOffset);
final double top;
final double bottom;
final double height;
final double scrollOffset;
}
class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
_DropdownRoute({
required this.items,
required this.padding,
required this.buttonRect,
required this.selectedIndex,
this.elevation = 8,
required this.capturedThemes,
required this.style,
this.barrierLabel,
this.itemHeight,
this.dropdownColor,
this.menuMaxHeight,
required this.menuDirection,
}) : itemHeights = List<double>.filled(
items.length, itemHeight ?? kMinInteractiveDimension);
final List<_MenuItem<T>> items;
final EdgeInsetsGeometry padding;
final Rect buttonRect;
final int selectedIndex;
final int elevation;
final CapturedThemes capturedThemes;
final TextStyle style;
final double? itemHeight;
final Color? dropdownColor;
final double? menuMaxHeight;
final DropdownMenuDirection menuDirection;
final List<double> itemHeights;
ScrollController? scrollController;
@override
Duration get transitionDuration => _kDropdownMenuDuration;
@override
bool get barrierDismissible => true;
@override
Color? get barrierColor => null;
@override
final String? barrierLabel;
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return _DropdownRoutePage<T>(
route: this,
constraints: constraints,
items: items,
padding: padding,
buttonRect: buttonRect,
selectedIndex: selectedIndex,
elevation: elevation,
capturedThemes: capturedThemes,
style: style,
dropdownColor: dropdownColor,
);
},
);
}
void _dismiss() {
if (isActive) {
navigator?.removeRoute(this);
}
}
double getItemOffset(int index) {
var offset = kMaterialListPadding.top;
if (items.isNotEmpty && index > 0) {
assert(items.length == itemHeights.length);
offset += itemHeights
.sublist(0, index)
.reduce((double total, double height) => total + height);
}
return offset;
}
_MenuLimits getMenuLimits(
Rect buttonRect, double availableHeight, int index) {
final maxMenuHeight = availableHeight - 2.0 * _kMenuItemHeight;
final buttonTop = buttonRect.top;
final buttonBottom = min(buttonRect.bottom, availableHeight);
final selectedItemOffset = getItemOffset(index);
final topLimit = min(_kMenuItemHeight, buttonTop);
final bottomLimit = max(availableHeight - _kMenuItemHeight, buttonBottom);
var preferredMenuHeight = kMaterialListPadding.vertical;
if (items.isNotEmpty) {
preferredMenuHeight +=
itemHeights.reduce((double total, double height) => total + height);
}
final menuHeight = min(maxMenuHeight, preferredMenuHeight);
//the next two lines are the main change for reversed dropdown opening
var menuTop = menuDirection == DropdownMenuDirection.up
? buttonTop - min((menuMaxHeight ?? menuHeight), menuHeight)
: (buttonTop - selectedItemOffset) -
(itemHeights[selectedIndex] - buttonRect.height) / 2.0;
var menuBottom = menuTop + min((menuMaxHeight ?? menuHeight), menuHeight);
if (menuTop < topLimit) {
menuTop = min(buttonTop, topLimit);
}
if (menuBottom > bottomLimit) {
menuBottom = max(buttonBottom, bottomLimit);
menuTop = menuBottom - menuHeight;
}
var scrollOffset = 0.0;
if (preferredMenuHeight > maxMenuHeight) {
scrollOffset = max(0.0, selectedItemOffset - (buttonTop - menuTop));
scrollOffset = min(scrollOffset, preferredMenuHeight - menuHeight);
}
return _MenuLimits(menuTop, menuBottom, menuHeight, scrollOffset);
}
}
class _DropdownRoutePage<T> extends StatelessWidget {
const _DropdownRoutePage({
Key? key,
required this.route,
required this.constraints,
this.items,
required this.padding,
required this.buttonRect,
required this.selectedIndex,
this.elevation = 8,
required this.capturedThemes,
this.style,
required this.dropdownColor,
}) : super(key: key);
final _DropdownRoute<T> route;
final BoxConstraints constraints;
final List<_MenuItem<T>>? items;
final EdgeInsetsGeometry padding;
final Rect buttonRect;
final int selectedIndex;
final int elevation;
final CapturedThemes capturedThemes;
final TextStyle? style;
final Color? dropdownColor;
@override
Widget build(BuildContext context) {
assert(debugCheckHasDirectionality(context));
if (route.scrollController == null) {
final menuLimits =
route.getMenuLimits(buttonRect, constraints.maxHeight, selectedIndex);
route.scrollController =
ScrollController(initialScrollOffset: menuLimits.scrollOffset);
}
final textDirection = Directionality.maybeOf(context);
final Widget menu = _DropdownMenu<T>(
route: route,
padding: padding.resolve(textDirection),
buttonRect: buttonRect,
constraints: constraints,
dropdownColor: dropdownColor,
);
return MediaQuery.removePadding(
context: context,
removeTop: true,
removeBottom: true,
removeLeft: true,
removeRight: true,
child: Builder(
builder: (BuildContext context) {
return CustomSingleChildLayout(
delegate: _DropdownMenuRouteLayout<T>(
buttonRect: buttonRect,
route: route,
textDirection: textDirection,
),
child: capturedThemes.wrap(menu),
);
},
),
);
}
}
class _MenuItem<T> extends SingleChildRenderObjectWidget {
const _MenuItem({
Key? key,
required this.onLayout,
required this.item,
}) : super(key: key, child: item);
final ValueChanged<Size> onLayout;
final CustomDropdownMenuItem<T>? item;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderMenuItem(onLayout);
}
@override
void updateRenderObject(
BuildContext context, covariant _RenderMenuItem renderObject) {
renderObject.onLayout = onLayout;
}
}
class _RenderMenuItem extends RenderProxyBox {
_RenderMenuItem(this.onLayout, [RenderBox? child]) : super(child);
ValueChanged<Size> onLayout;
@override
void performLayout() {
super.performLayout();
onLayout(size);
}
}
class _DropdownMenuItemContainer extends StatelessWidget {
const _DropdownMenuItemContainer({
Key? key,
required this.child,
}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
return Container(
constraints: const BoxConstraints(minHeight: _kMenuItemHeight),
alignment: AlignmentDirectional.centerStart,
child: child,
);
}
}
class CustomDropdownMenuItem<T> extends _DropdownMenuItemContainer {
const CustomDropdownMenuItem({
Key? key,
this.onTap,
this.value,
required Widget child,
}) : super(key: key, child: child);
final VoidCallback? onTap;
final T? value;
}
class CustomDropdownButtonHideUnderline extends InheritedWidget {
const CustomDropdownButtonHideUnderline({
Key? key,
required Widget child,
}) : super(key: key, child: child);
static bool at(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<
CustomDropdownButtonHideUnderline>() !=
null;
}
@override
bool updateShouldNotify(CustomDropdownButtonHideUnderline oldWidget) => false;
}
class CustomDropdownButton<T> extends StatefulWidget {
CustomDropdownButton({
Key? key,
required this.items,
this.selectedItemBuilder,
this.value,
this.hint,
this.disabledHint,
this.onChanged,
this.onTap,
this.elevation = 8,
this.style,
this.underline,
this.icon,
this.iconDisabledColor,
this.iconEnabledColor,
this.iconSize = 24.0,
this.isDense = false,
this.isExpanded = false,
this.itemHeight = kMinInteractiveDimension,
this.focusColor,
this.focusNode,
this.autofocus = false,
this.dropdownColor,
this.menuMaxHeight,
required this.menuDirection,
}) : assert(
items == null ||
items.isEmpty ||
value == null ||
items.where((CustomDropdownMenuItem<T> item) {
return item.value == value;
}).length ==
1,
"There should be exactly one item with [DropdownButton]'s value: "
'$value. \n'
'Either zero or 2 or more [DropdownMenuItem]s were detected '
'with the same value',
),
assert(itemHeight == null || itemHeight >= kMinInteractiveDimension),
super(key: key);
final DropdownMenuDirection menuDirection;
final List<CustomDropdownMenuItem<T>>? items;
final T? value;
final Widget? hint;
final Widget? disabledHint;
final ValueChanged<T?>? onChanged;
final VoidCallback? onTap;
final DropdownButtonBuilder? selectedItemBuilder;
final int elevation;
final TextStyle? style;
final Widget? underline;
final Widget? icon;
final Color? iconDisabledColor;
final Color? iconEnabledColor;
final double iconSize;
final bool isDense;
final bool isExpanded;
final double? itemHeight;
final Color? focusColor;
final FocusNode? focusNode;
final bool autofocus;
final Color? dropdownColor;
final double? menuMaxHeight;
@override
_DropdownButtonState<T> createState() => _DropdownButtonState<T>();
}
class _DropdownButtonState<T> extends State<CustomDropdownButton<T>>
with WidgetsBindingObserver {
int? _selectedIndex;
_DropdownRoute<T>? _dropdownRoute;
Orientation? _lastOrientation;
FocusNode? _internalNode;
FocusNode? get focusNode => widget.focusNode ?? _internalNode;
bool _hasPrimaryFocus = false;
late Map<Type, Action<Intent>> _actionMap;
late FocusHighlightMode _focusHighlightMode;
FocusNode _createFocusNode() {
return FocusNode(debugLabel: '${widget.runtimeType}');
}
@override
void initState() {
super.initState();
_updateSelectedIndex();
if (widget.focusNode == null) {
_internalNode ??= _createFocusNode();
}
_actionMap = <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(
onInvoke: (ActivateIntent intent) => _handleTap(),
),
ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(
onInvoke: (ButtonActivateIntent intent) => _handleTap(),
),
};
focusNode!.addListener(_handleFocusChanged);
final focusManager = WidgetsBinding.instance.focusManager;
_focusHighlightMode = focusManager.highlightMode;
focusManager.addHighlightModeListener(_handleFocusHighlightModeChange);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_removeDropdownRoute();
WidgetsBinding.instance.focusManager
.removeHighlightModeListener(_handleFocusHighlightModeChange);
focusNode!.removeListener(_handleFocusChanged);
_internalNode?.dispose();
super.dispose();
}
void _removeDropdownRoute() {
_dropdownRoute?._dismiss();
_dropdownRoute = null;
_lastOrientation = null;
}
void _handleFocusChanged() {
if (_hasPrimaryFocus != focusNode!.hasPrimaryFocus) {
this.setState(() {
_hasPrimaryFocus = focusNode!.hasPrimaryFocus;
});
}
}
void _handleFocusHighlightModeChange(FocusHighlightMode mode) {
if (!mounted) {
return;
}
this.setState(() {
_focusHighlightMode = mode;
});
}
@override
void didUpdateWidget(CustomDropdownButton<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.focusNode != oldWidget.focusNode) {
oldWidget.focusNode?.removeListener(_handleFocusChanged);
if (widget.focusNode == null) {
_internalNode ??= _createFocusNode();
}
_hasPrimaryFocus = focusNode!.hasPrimaryFocus;
focusNode!.addListener(_handleFocusChanged);
}
_updateSelectedIndex();
}
void _updateSelectedIndex() {
if (widget.items == null ||
widget.items!.isEmpty ||
(widget.value == null &&
widget.items!
.where((CustomDropdownMenuItem<T> item) =>
item.value == widget.value)
.isEmpty)) {
_selectedIndex = null;
return;
}
assert(widget.items!
.where(
(CustomDropdownMenuItem<T> item) => item.value == widget.value)
.length ==
1);
for (var itemIndex = 0; itemIndex < widget.items!.length; itemIndex++) {
if (widget.items![itemIndex].value == widget.value) {
_selectedIndex = itemIndex;
return;
}
}
}
TextStyle? get _textStyle =>
widget.style ?? Theme.of(context).textTheme.subtitle1;
void _handleTap() {
final textDirection = Directionality.maybeOf(context);
final menuMargin = ButtonTheme.of(context).alignedDropdown
? _kAlignedMenuMargin
: _kUnalignedMenuMargin;
final menuItems = <_MenuItem<T>>[
for (int index = 0; index < widget.items!.length; index += 1)
_MenuItem<T>(
item: widget.items![index],
onLayout: (Size size) {
if (_dropdownRoute == null) {
return;
}
_dropdownRoute!.itemHeights[index] = size.height;
},
),
];
final navigator = Navigator.of(context);
assert(_dropdownRoute == null);
final itemBox = context.findRenderObject()! as RenderBox;
final itemRect = itemBox.localToGlobal(Offset.zero,
ancestor: navigator.context.findRenderObject()) &
itemBox.size;
_dropdownRoute = _DropdownRoute<T>(
items: menuItems,
buttonRect: menuMargin.resolve(textDirection).inflateRect(itemRect),
padding: _kMenuItemPadding.resolve(textDirection),
selectedIndex: _selectedIndex ?? 0,
elevation: widget.elevation,
capturedThemes:
InheritedTheme.capture(from: context, to: navigator.context),
style: _textStyle!,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
itemHeight: widget.itemHeight,
dropdownColor: widget.dropdownColor,
menuMaxHeight: widget.menuMaxHeight,
menuDirection: widget.menuDirection,
);
navigator
.push(_dropdownRoute!)
.then<void>((_DropdownRouteResult<T>? newValue) {
_removeDropdownRoute();
if (!mounted || newValue == null) {
return;
}
widget.onChanged?.call(newValue.result);
});
widget.onTap?.call();
}
double get _denseButtonHeight {
final fontSize = _textStyle!.fontSize ??
Theme.of(context).textTheme.subtitle1!.fontSize!;
return max(fontSize, max(widget.iconSize, _kDenseButtonHeight));
}
Color get _iconColor {
if (_enabled) {
if (widget.iconEnabledColor != null) {
return widget.iconEnabledColor!;
}
switch (Theme.of(context).brightness) {
case Brightness.light:
return Colors.grey.shade700;
case Brightness.dark:
return Colors.white70;
}
} else {
if (widget.iconDisabledColor != null) {
return widget.iconDisabledColor!;
}
switch (Theme.of(context).brightness) {
case Brightness.light:
return Colors.grey.shade400;
case Brightness.dark:
return Colors.white10;
}
}
}
bool get _enabled =>
widget.items != null &&
widget.items!.isNotEmpty &&
widget.onChanged != null;
Orientation _getOrientation(BuildContext context) {
var result = MediaQuery.maybeOf(context)?.orientation;
if (result == null) {
final Size size = window.physicalSize;
result = size.width > size.height
? Orientation.landscape
: Orientation.portrait;
}
return result;
}
bool get _showHighlight {
switch (_focusHighlightMode) {
case FocusHighlightMode.touch:
return false;
case FocusHighlightMode.traditional:
return _hasPrimaryFocus;
}
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
assert(debugCheckHasMaterialLocalizations(context));
final newOrientation = _getOrientation(context);
_lastOrientation ??= newOrientation;
if (newOrientation != _lastOrientation) {
_removeDropdownRoute();
_lastOrientation = newOrientation;
}
final items = widget.selectedItemBuilder == null
? (widget.items != null ? List<Widget>.from(widget.items!) : <Widget>[])
: List<Widget>.from(widget.selectedItemBuilder!(context));
int? hintIndex;
if (widget.hint != null || (!_enabled && widget.disabledHint != null)) {
var displayedHint =
_enabled ? widget.hint! : widget.disabledHint ?? widget.hint!;
if (widget.selectedItemBuilder == null) {
displayedHint = _DropdownMenuItemContainer(child: displayedHint);
}
hintIndex = items.length;
items.add(DefaultTextStyle(
style: _textStyle!.copyWith(color: Theme.of(context).hintColor),
child: IgnorePointer(
ignoringSemantics: false,
child: displayedHint,
),
));
}
final padding = ButtonTheme.of(context).alignedDropdown
? _kAlignedButtonPadding
: _kUnalignedButtonPadding;
final Widget innerItemsWidget;
if (items.isEmpty) {
innerItemsWidget = Container();
} else {
innerItemsWidget = IndexedStack(
index: _selectedIndex ?? hintIndex,
alignment: AlignmentDirectional.centerStart,
children: widget.isDense
? items
: items.map((Widget item) {
return widget.itemHeight != null
? SizedBox(height: widget.itemHeight, child: item)
: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[item]);
}).toList(),
);
}
const defaultIcon = Icon(Icons.arrow_drop_down);
Widget result = DefaultTextStyle(
style: _enabled
? _textStyle!
: _textStyle!.copyWith(color: Theme.of(context).disabledColor),
child: Container(
decoration: _showHighlight
? BoxDecoration(
color: widget.focusColor ?? Theme.of(context).focusColor,
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
)
: null,
padding: padding.resolve(Directionality.of(context)),
height: widget.isDense ? _denseButtonHeight : null,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (widget.isExpanded)
Expanded(child: innerItemsWidget)
else
innerItemsWidget,
IconTheme(
data: IconThemeData(
color: _iconColor,
size: widget.iconSize,
),
child: widget.icon ?? defaultIcon,
),
],
),
),
);
if (!CustomDropdownButtonHideUnderline.at(context)) {
final bottom = (widget.isDense || widget.itemHeight == null) ? 0.0 : 8.0;
result = Stack(
children: <Widget>[
result,
Positioned(
left: 0.0,
right: 0.0,
bottom: bottom,
child: widget.underline ??
Container(
height: 1.0,
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: Color(0xFFBDBDBD),
width: 0.0,
),
),
),
),
),
],
);
}
return Semantics(
button: true,
child: Actions(
actions: _actionMap,
child: Focus(
canRequestFocus: _enabled,
focusNode: focusNode,
autofocus: widget.autofocus,
child: GestureDetector(
onTap: _enabled ? _handleTap : null,
behavior: HitTestBehavior.opaque,
child: result,
),
),
),
);
}
}