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 intersperse(T element, Iterable 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.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 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 resize; final ValueGetter getSelectedItemOffset; final BoxPainter _painter; @override void paint(Canvas canvas, Size size) { final selectedItemOffset = getSelectedItemOffset(); final top = Tween( begin: selectedItemOffset.clamp( 0.0, max(size.height - _kMenuItemHeight, 0.0)), end: 0.0, ); final bottom = Tween( 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 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 route; final EdgeInsets? padding; final Rect buttonRect; final BoxConstraints constraints; final int itemIndex; @override _DropdownMenuItemButtonState createState() => _DropdownMenuItemButtonState(); } class _DropdownMenuItemButtonState extends State<_DropdownMenuItemButton> { 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(dropdownMenuItem.value), ); } static final Map _webShortcuts = { 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 extends StatefulWidget { const _DropdownMenu({ Key? key, this.padding, required this.route, required this.buttonRect, required this.constraints, this.dropdownColor, }) : super(key: key); final _DropdownRoute route; final EdgeInsets? padding; final Rect buttonRect; final BoxConstraints constraints; final Color? dropdownColor; @override _DropdownMenuState createState() => _DropdownMenuState(); } class _DropdownMenuState extends State<_DropdownMenu> { 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 = [ for (int itemIndex = 0; itemIndex < route.items.length; ++itemIndex) _DropdownMenuItemButton( 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 extends SingleChildLayoutDelegate { _DropdownMenuRouteLayout({ required this.buttonRect, required this.route, required this.textDirection, }); final Rect buttonRect; final _DropdownRoute 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 oldDelegate) { return buttonRect != oldDelegate.buttonRect || textDirection != oldDelegate.textDirection; } } @immutable class _DropdownRouteResult { const _DropdownRouteResult(this.result); final T? result; @override bool operator ==(Object other) { return other is _DropdownRouteResult && 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 extends PopupRoute<_DropdownRouteResult> { _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.filled( items.length, itemHeight ?? kMinInteractiveDimension); final List<_MenuItem> 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 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 animation, Animation secondaryAnimation) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return _DropdownRoutePage( 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 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 route; final BoxConstraints constraints; final List<_MenuItem>? 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( 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( buttonRect: buttonRect, route: route, textDirection: textDirection, ), child: capturedThemes.wrap(menu), ); }, ), ); } } class _MenuItem extends SingleChildRenderObjectWidget { const _MenuItem({ Key? key, required this.onLayout, required this.item, }) : super(key: key, child: item); final ValueChanged onLayout; final CustomDropdownMenuItem? 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 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 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 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 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>? items; final T? value; final Widget? hint; final Widget? disabledHint; final ValueChanged? 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 createState() => _DropdownButtonState(); } class _DropdownButtonState extends State> with WidgetsBindingObserver { int? _selectedIndex; _DropdownRoute? _dropdownRoute; Orientation? _lastOrientation; FocusNode? _internalNode; FocusNode? get focusNode => widget.focusNode ?? _internalNode; bool _hasPrimaryFocus = false; late Map> _actionMap; late FocusHighlightMode _focusHighlightMode; FocusNode _createFocusNode() { return FocusNode(debugLabel: '${widget.runtimeType}'); } @override void initState() { super.initState(); _updateSelectedIndex(); if (widget.focusNode == null) { _internalNode ??= _createFocusNode(); } _actionMap = >{ ActivateIntent: CallbackAction( onInvoke: (ActivateIntent intent) => _handleTap(), ), ButtonActivateIntent: CallbackAction( 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 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 item) => item.value == widget.value) .isEmpty)) { _selectedIndex = null; return; } assert(widget.items! .where( (CustomDropdownMenuItem 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>[ for (int index = 0; index < widget.items!.length; index += 1) _MenuItem( 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( 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((_DropdownRouteResult? 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.from(widget.items!) : []) : List.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: [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: [ 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: [ 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, ), ), ), ); } }