Line data Source code
1 : // Copyright 2014 The Flutter Authors. All rights reserved.
2 : // Use of this source code is governed by a BSD-style license that can be
3 : // found in the LICENSE file.
4 :
5 : /// Copy of material dropdown.dart file
6 : /// Changes are removing horizontal item Padding (kMenuItemPadding) and Min Height (kMenuItemHeight)
7 :
8 : // coverage: ignore-file
9 :
10 : import 'dart:math' as math;
11 : import 'dart:ui' show window;
12 :
13 : import 'package:flutter/foundation.dart';
14 : import 'package:flutter/material.dart';
15 : import 'package:flutter/rendering.dart';
16 : import 'package:flutter/services.dart';
17 : import 'package:flutter/widgets.dart';
18 :
19 : const Duration _kGridRowDropdownMenuDuration = Duration(milliseconds: 300);
20 : const double _kMenuItemHeight = 0;
21 : const double _kDenseButtonHeight = 24.0;
22 : const EdgeInsets _kMenuItemPadding = EdgeInsets.zero;
23 : const EdgeInsetsGeometry _kAlignedButtonPadding = EdgeInsetsDirectional.only(start: 16.0, end: 4.0);
24 : const EdgeInsets _kUnalignedButtonPadding = EdgeInsets.zero;
25 : const EdgeInsets _kAlignedMenuMargin = EdgeInsets.zero;
26 : const EdgeInsetsGeometry _kUnalignedMenuMargin = EdgeInsetsDirectional.only(start: 16.0, end: 24.0);
27 :
28 : /// A builder to customize dropdown buttons.
29 : ///
30 : /// Used by [GridRowDropdownButton.selectedItemBuilder].
31 : typedef GridRowDropdownButtonBuilder = List<Widget> Function(BuildContext context);
32 :
33 : class _GridRowDropdownMenuPainter extends CustomPainter {
34 0 : _GridRowDropdownMenuPainter({
35 : this.color,
36 : this.elevation,
37 : this.selectedIndex,
38 : required this.resize,
39 : required this.getSelectedItemOffset,
40 0 : }) : _painter = BoxDecoration(
41 : // If you add an image here, you must provide a real
42 : // configuration in the paint() function and you must provide some sort
43 : // of onChanged callback here.
44 : color: color,
45 0 : borderRadius: BorderRadius.circular(2.0),
46 0 : boxShadow: kElevationToShadow[elevation],
47 0 : ).createBoxPainter(),
48 0 : super(repaint: resize);
49 :
50 : final Color? color;
51 : final int? elevation;
52 : final int? selectedIndex;
53 : final Animation<double> resize;
54 : final ValueGetter<double> getSelectedItemOffset;
55 : final BoxPainter _painter;
56 :
57 0 : @override
58 : void paint(Canvas canvas, Size size) {
59 0 : final double selectedItemOffset = getSelectedItemOffset();
60 0 : final Tween<double> top = Tween<double>(
61 0 : begin: selectedItemOffset.clamp(0.0, math.max(size.height - _kMenuItemHeight, 0.0)),
62 : end: 0.0,
63 : );
64 :
65 0 : final Tween<double> bottom = Tween<double>(
66 0 : begin: (top.begin! + _kMenuItemHeight).clamp(math.min(_kMenuItemHeight, size.height), size.height),
67 0 : end: size.height,
68 : );
69 :
70 0 : final Rect rect = Rect.fromLTRB(0.0, top.evaluate(resize), size.width, bottom.evaluate(resize));
71 :
72 0 : _painter.paint(canvas, rect.topLeft, ImageConfiguration(size: rect.size));
73 : }
74 :
75 0 : @override
76 : bool shouldRepaint(_GridRowDropdownMenuPainter oldPainter) {
77 0 : return oldPainter.color != color
78 0 : || oldPainter.elevation != elevation
79 0 : || oldPainter.selectedIndex != selectedIndex
80 0 : || oldPainter.resize != resize;
81 : }
82 : }
83 :
84 : // The widget that is the button wrapping the menu items.
85 : class _GridRowDropdownMenuItemButton<T> extends StatefulWidget {
86 0 : const _GridRowDropdownMenuItemButton({
87 : Key? key,
88 : this.padding,
89 : required this.route,
90 : required this.buttonRect,
91 : required this.constraints,
92 : required this.itemIndex,
93 : required this.enableFeedback,
94 0 : }) : super(key: key);
95 :
96 : final _GridRowDropdownRoute<T> route;
97 : final EdgeInsets? padding;
98 : final Rect buttonRect;
99 : final BoxConstraints constraints;
100 : final int itemIndex;
101 : final bool enableFeedback;
102 :
103 0 : @override
104 0 : _GridRowDropdownMenuItemButtonState<T> createState() => _GridRowDropdownMenuItemButtonState<T>();
105 : }
106 :
107 : class _GridRowDropdownMenuItemButtonState<T> extends State<_GridRowDropdownMenuItemButton<T>> {
108 0 : void _handleFocusChange(bool focused) {
109 : final bool inTraditionalMode;
110 0 : switch (FocusManager.instance.highlightMode) {
111 0 : case FocusHighlightMode.touch:
112 : inTraditionalMode = false;
113 : break;
114 0 : case FocusHighlightMode.traditional:
115 : inTraditionalMode = true;
116 : break;
117 : }
118 :
119 : if (focused && inTraditionalMode) {
120 0 : final _MenuLimits menuLimits = widget.route.getMenuLimits(
121 0 : widget.buttonRect,
122 0 : widget.constraints.maxHeight,
123 0 : widget.itemIndex,
124 : );
125 0 : widget.route.scrollController!.animateTo(
126 0 : menuLimits.scrollOffset,
127 : curve: Curves.easeInOut,
128 : duration: const Duration(milliseconds: 100),
129 : );
130 : }
131 : }
132 :
133 0 : void _handleOnTap() {
134 0 : final GridRowDropdownMenuItem<T> dropdownMenuItem = widget.route.items[widget.itemIndex].item!;
135 :
136 0 : dropdownMenuItem.onTap?.call();
137 :
138 0 : Navigator.pop(
139 0 : context,
140 0 : _GridRowDropdownRouteResult<T>(dropdownMenuItem.value),
141 : );
142 : }
143 :
144 : static const Map<ShortcutActivator, Intent> _webShortcuts = <ShortcutActivator, Intent>{
145 : // On the web, up/down don't change focus, *except* in a <select>
146 : // element, which is what a dropdown emulates.
147 : SingleActivator(LogicalKeyboardKey.arrowDown): DirectionalFocusIntent(TraversalDirection.down),
148 : SingleActivator(LogicalKeyboardKey.arrowUp): DirectionalFocusIntent(TraversalDirection.up),
149 : };
150 :
151 0 : @override
152 : Widget build(BuildContext context) {
153 0 : final GridRowDropdownMenuItem<T> dropdownMenuItem = widget.route.items[widget.itemIndex].item!;
154 : final CurvedAnimation opacity;
155 0 : final double unit = 0.5 / (widget.route.items.length + 1.5);
156 0 : if (widget.itemIndex == widget.route.selectedIndex) {
157 0 : opacity = CurvedAnimation(parent: widget.route.animation!, curve: const Threshold(0.0));
158 : } else {
159 0 : final double start = (0.5 + (widget.itemIndex + 1) * unit).clamp(0.0, 1.0);
160 0 : final double end = (start + 1.5 * unit).clamp(0.0, 1.0);
161 0 : opacity = CurvedAnimation(parent: widget.route.animation!, curve: Interval(start, end));
162 : }
163 0 : Widget child = Container(
164 0 : padding: widget.padding,
165 0 : child: widget.route.items[widget.itemIndex],
166 : );
167 : // An [InkWell] is added to the item only if it is enabled
168 0 : if (dropdownMenuItem.enabled) {
169 0 : child = InkWell(
170 0 : autofocus: widget.itemIndex == widget.route.selectedIndex,
171 0 : enableFeedback: widget.enableFeedback,
172 0 : onTap: _handleOnTap,
173 0 : onFocusChange: _handleFocusChange,
174 : child: child,
175 : );
176 : }
177 0 : child = FadeTransition(opacity: opacity, child: child);
178 0 : if (kIsWeb && dropdownMenuItem.enabled) {
179 0 : child = Shortcuts(
180 : shortcuts: _webShortcuts,
181 : child: child,
182 : );
183 : }
184 : return child;
185 : }
186 : }
187 :
188 : class _GridRowDropdownMenu<T> extends StatefulWidget {
189 0 : const _GridRowDropdownMenu({
190 : Key? key,
191 : this.padding,
192 : required this.route,
193 : required this.buttonRect,
194 : required this.constraints,
195 : this.dropdownColor,
196 : required this.enableFeedback,
197 0 : }) : super(key: key);
198 :
199 : final _GridRowDropdownRoute<T> route;
200 : final EdgeInsets? padding;
201 : final Rect buttonRect;
202 : final BoxConstraints constraints;
203 : final Color? dropdownColor;
204 : final bool enableFeedback;
205 :
206 0 : @override
207 0 : _GridRowDropdownMenuState<T> createState() => _GridRowDropdownMenuState<T>();
208 : }
209 :
210 : class _GridRowDropdownMenuState<T> extends State<_GridRowDropdownMenu<T>> {
211 : late CurvedAnimation _fadeOpacity;
212 : late CurvedAnimation _resize;
213 :
214 0 : @override
215 : void initState() {
216 0 : super.initState();
217 : // We need to hold these animations as state because of their curve
218 : // direction. When the route's animation reverses, if we were to recreate
219 : // the CurvedAnimation objects in build, we'd lose
220 : // CurvedAnimation._curveDirection.
221 0 : _fadeOpacity = CurvedAnimation(
222 0 : parent: widget.route.animation!,
223 : curve: const Interval(0.0, 0.25),
224 : reverseCurve: const Interval(0.75, 1.0),
225 : );
226 0 : _resize = CurvedAnimation(
227 0 : parent: widget.route.animation!,
228 : curve: const Interval(0.25, 0.5),
229 : reverseCurve: const Threshold(0.0),
230 : );
231 : }
232 :
233 0 : @override
234 : Widget build(BuildContext context) {
235 : // The menu is shown in three stages (unit timing in brackets):
236 : // [0s - 0.25s] - Fade in a rect-sized menu container with the selected item.
237 : // [0.25s - 0.5s] - Grow the otherwise empty menu container from the center
238 : // until it's big enough for as many items as we're going to show.
239 : // [0.5s - 1.0s] Fade in the remaining visible items from top to bottom.
240 : //
241 : // When the menu is dismissed we just fade the entire thing out
242 : // in the first 0.25s.
243 0 : assert(debugCheckHasMaterialLocalizations(context));
244 0 : final MaterialLocalizations localizations = MaterialLocalizations.of(context);
245 0 : final _GridRowDropdownRoute<T> route = widget.route;
246 0 : final List<Widget> children = <Widget>[
247 0 : for (int itemIndex = 0; itemIndex < route.items.length; ++itemIndex)
248 0 : _GridRowDropdownMenuItemButton<T>(
249 0 : route: widget.route,
250 0 : padding: widget.padding,
251 0 : buttonRect: widget.buttonRect,
252 0 : constraints: widget.constraints,
253 : itemIndex: itemIndex,
254 0 : enableFeedback: widget.enableFeedback,
255 : ),
256 : ];
257 :
258 0 : return FadeTransition(
259 0 : opacity: _fadeOpacity,
260 0 : child: CustomPaint(
261 0 : painter: _GridRowDropdownMenuPainter(
262 0 : color: widget.dropdownColor ?? Theme.of(context).canvasColor,
263 0 : elevation: route.elevation,
264 0 : selectedIndex: route.selectedIndex,
265 0 : resize: _resize,
266 : // This offset is passed as a callback, not a value, because it must
267 : // be retrieved at paint time (after layout), not at build time.
268 0 : getSelectedItemOffset: () => route.getItemOffset(route.selectedIndex),
269 : ),
270 0 : child: Semantics(
271 : scopesRoute: true,
272 : namesRoute: true,
273 : explicitChildNodes: true,
274 0 : label: localizations.popupMenuLabel,
275 0 : child: Material(
276 : type: MaterialType.transparency,
277 0 : textStyle: route.style,
278 0 : child: ScrollConfiguration(
279 : // GridRowDropdown menus should never overscroll or display an overscroll indicator.
280 : // Scrollbars are built-in below.
281 : // Platform must use Theme and ScrollPhysics must be Clamping.
282 0 : behavior: ScrollConfiguration.of(context).copyWith(
283 : scrollbars: false,
284 : overscroll: false,
285 : physics: const ClampingScrollPhysics(),
286 0 : platform: Theme.of(context).platform,
287 : ),
288 0 : child: PrimaryScrollController(
289 0 : controller: widget.route.scrollController!,
290 0 : child: Scrollbar(
291 : isAlwaysShown: true,
292 0 : child: ListView(
293 : padding: kMaterialListPadding,
294 : shrinkWrap: true,
295 : children: children,
296 : ),
297 : ),
298 : ),
299 : ),
300 : ),
301 : ),
302 : ),
303 : );
304 : }
305 : }
306 :
307 : class _GridRowDropdownMenuRouteLayout<T> extends SingleChildLayoutDelegate {
308 0 : _GridRowDropdownMenuRouteLayout({
309 : required this.buttonRect,
310 : required this.route,
311 : required this.textDirection,
312 : });
313 :
314 : final Rect buttonRect;
315 : final _GridRowDropdownRoute<T> route;
316 : final TextDirection? textDirection;
317 :
318 0 : @override
319 : BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
320 : // The maximum height of a simple menu should be one or more rows less than
321 : // the view height. This ensures a tappable area outside of the simple menu
322 : // with which to dismiss the menu.
323 : // -- https://material.io/design/components/menus.html#usage
324 0 : double maxHeight = math.max(0.0, constraints.maxHeight - 2 * _kMenuItemHeight);
325 0 : if (route.menuMaxHeight != null && route.menuMaxHeight! <= maxHeight) {
326 0 : maxHeight = route.menuMaxHeight!;
327 : }
328 : // The width of a menu should be at most the view width. This ensures that
329 : // the menu does not extend past the left and right edges of the screen.
330 0 : final double width = math.min(constraints.maxWidth, buttonRect.width);
331 0 : return BoxConstraints(
332 : minWidth: width,
333 : maxWidth: width,
334 : minHeight: 0.0,
335 : maxHeight: maxHeight,
336 : );
337 : }
338 :
339 0 : @override
340 : Offset getPositionForChild(Size size, Size childSize) {
341 0 : final _MenuLimits menuLimits = route.getMenuLimits(buttonRect, size.height, route.selectedIndex);
342 :
343 0 : assert(() {
344 0 : final Rect container = Offset.zero & size;
345 0 : if (container.intersect(buttonRect) == buttonRect) {
346 : // If the button was entirely on-screen, then verify
347 : // that the menu is also on-screen.
348 : // If the button was a bit off-screen, then, oh well.
349 0 : assert(menuLimits.top >= 0.0);
350 0 : assert(menuLimits.top + menuLimits.height <= size.height);
351 : }
352 : return true;
353 0 : }());
354 0 : assert(textDirection != null);
355 : final double left;
356 0 : switch (textDirection!) {
357 0 : case TextDirection.rtl:
358 0 : left = buttonRect.right.clamp(0.0, size.width) - childSize.width;
359 : break;
360 0 : case TextDirection.ltr:
361 0 : left = buttonRect.left.clamp(0.0, size.width - childSize.width);
362 : break;
363 : }
364 :
365 0 : return Offset(left, menuLimits.top);
366 : }
367 :
368 0 : @override
369 : bool shouldRelayout(_GridRowDropdownMenuRouteLayout<T> oldDelegate) {
370 0 : return buttonRect != oldDelegate.buttonRect || textDirection != oldDelegate.textDirection;
371 : }
372 : }
373 :
374 : // We box the return value so that the return value can be null. Otherwise,
375 : // canceling the route (which returns null) would get confused with actually
376 : // returning a real null value.
377 : @immutable
378 : class _GridRowDropdownRouteResult<T> {
379 0 : const _GridRowDropdownRouteResult(this.result);
380 :
381 : final T? result;
382 :
383 0 : @override
384 : bool operator ==(Object other) {
385 0 : return other is _GridRowDropdownRouteResult<T>
386 0 : && other.result == result;
387 : }
388 :
389 0 : @override
390 0 : int get hashCode => result.hashCode;
391 : }
392 :
393 : class _MenuLimits {
394 0 : const _MenuLimits(this.top, this.bottom, this.height, this.scrollOffset);
395 : final double top;
396 : final double bottom;
397 : final double height;
398 : final double scrollOffset;
399 : }
400 :
401 : class _GridRowDropdownRoute<T> extends PopupRoute<_GridRowDropdownRouteResult<T>> {
402 0 : _GridRowDropdownRoute({
403 : required this.items,
404 : required this.padding,
405 : required this.buttonRect,
406 : required this.selectedIndex,
407 : this.elevation = 8,
408 : required this.capturedThemes,
409 : required this.style,
410 : this.barrierLabel,
411 : this.itemHeight,
412 : this.dropdownColor,
413 : this.menuMaxHeight,
414 : required this.enableFeedback,
415 0 : }) : assert(style != null),
416 0 : itemHeights = List<double>.filled(items.length, itemHeight ?? kMinInteractiveDimension);
417 :
418 : final List<_MenuItem<T>> items;
419 : final EdgeInsetsGeometry padding;
420 : final Rect buttonRect;
421 : final int selectedIndex;
422 : final int elevation;
423 : final CapturedThemes capturedThemes;
424 : final TextStyle style;
425 : final double? itemHeight;
426 : final Color? dropdownColor;
427 : final double? menuMaxHeight;
428 : final bool enableFeedback;
429 :
430 : final List<double> itemHeights;
431 : ScrollController? scrollController;
432 :
433 0 : @override
434 : Duration get transitionDuration => _kGridRowDropdownMenuDuration;
435 :
436 0 : @override
437 : bool get barrierDismissible => true;
438 :
439 0 : @override
440 : Color? get barrierColor => null;
441 :
442 : @override
443 : final String? barrierLabel;
444 :
445 0 : @override
446 : Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
447 0 : return LayoutBuilder(
448 0 : builder: (BuildContext context, BoxConstraints constraints) {
449 0 : return _GridRowDropdownRoutePage<T>(
450 : route: this,
451 : constraints: constraints,
452 0 : items: items,
453 0 : padding: padding,
454 0 : buttonRect: buttonRect,
455 0 : selectedIndex: selectedIndex,
456 0 : elevation: elevation,
457 0 : capturedThemes: capturedThemes,
458 0 : style: style,
459 0 : dropdownColor: dropdownColor,
460 0 : enableFeedback: enableFeedback,
461 : );
462 : },
463 : );
464 : }
465 :
466 0 : void _dismiss() {
467 0 : if (isActive) {
468 0 : navigator?.removeRoute(this);
469 : }
470 : }
471 :
472 0 : double getItemOffset(int index) {
473 0 : double offset = kMaterialListPadding.top;
474 0 : if (items.isNotEmpty && index > 0) {
475 0 : assert(items.length == itemHeights.length);
476 0 : offset += itemHeights
477 0 : .sublist(0, index)
478 0 : .reduce((double total, double height) => total + height);
479 : }
480 : return offset;
481 : }
482 :
483 : // Returns the vertical extent of the menu and the initial scrollOffset
484 : // for the ListView that contains the menu items. The vertical center of the
485 : // selected item is aligned with the button's vertical center, as far as
486 : // that's possible given availableHeight.
487 0 : _MenuLimits getMenuLimits(Rect buttonRect, double availableHeight, int index) {
488 0 : final double maxMenuHeight = availableHeight - 2.0 * _kMenuItemHeight;
489 0 : final double buttonTop = buttonRect.top;
490 0 : final double buttonBottom = math.min(buttonRect.bottom, availableHeight);
491 0 : final double selectedItemOffset = getItemOffset(index);
492 :
493 : // If the button is placed on the bottom or top of the screen, its top or
494 : // bottom may be less than [_kMenuItemHeight] from the edge of the screen.
495 : // In this case, we want to change the menu limits to align with the top
496 : // or bottom edge of the button.
497 0 : final double topLimit = math.min(_kMenuItemHeight, buttonTop);
498 0 : final double bottomLimit = math.max(availableHeight - _kMenuItemHeight, buttonBottom);
499 :
500 0 : double menuTop = (buttonTop - selectedItemOffset) - (itemHeights[selectedIndex] - buttonRect.height) / 2.0;
501 0 : double preferredMenuHeight = kMaterialListPadding.vertical;
502 0 : if (items.isNotEmpty)
503 0 : preferredMenuHeight += itemHeights.reduce((double total, double height) => total + height);
504 :
505 : // If there are too many elements in the menu, we need to shrink it down
506 : // so it is at most the maxMenuHeight.
507 0 : final double menuHeight = math.min(maxMenuHeight, preferredMenuHeight);
508 0 : double menuBottom = menuTop + menuHeight;
509 :
510 : // If the computed top or bottom of the menu are outside of the range
511 : // specified, we need to bring them into range. If the item height is larger
512 : // than the button height and the button is at the very bottom or top of the
513 : // screen, the menu will be aligned with the bottom or top of the button
514 : // respectively.
515 0 : if (menuTop < topLimit)
516 0 : menuTop = math.min(buttonTop, topLimit);
517 :
518 0 : if (menuBottom > bottomLimit) {
519 0 : menuBottom = math.max(buttonBottom, bottomLimit);
520 0 : menuTop = menuBottom - menuHeight;
521 : }
522 :
523 : double scrollOffset = 0;
524 : // If all of the menu items will not fit within availableHeight then
525 : // compute the scroll offset that will line the selected menu item up
526 : // with the select item. This is only done when the menu is first
527 : // shown - subsequently we leave the scroll offset where the user left
528 : // it. This scroll offset is only accurate for fixed height menu items
529 : // (the default).
530 0 : if (preferredMenuHeight > maxMenuHeight) {
531 : // The offset should be zero if the selected item is in view at the beginning
532 : // of the menu. Otherwise, the scroll offset should center the item if possible.
533 0 : scrollOffset = math.max(0.0, selectedItemOffset - (buttonTop - menuTop));
534 : // If the selected item's scroll offset is greater than the maximum scroll offset,
535 : // set it instead to the maximum allowed scroll offset.
536 0 : scrollOffset = math.min(scrollOffset, preferredMenuHeight - menuHeight);
537 : }
538 :
539 0 : return _MenuLimits(menuTop, menuBottom, menuHeight, scrollOffset);
540 : }
541 : }
542 :
543 : class _GridRowDropdownRoutePage<T> extends StatelessWidget {
544 0 : const _GridRowDropdownRoutePage({
545 : Key? key,
546 : required this.route,
547 : required this.constraints,
548 : this.items,
549 : required this.padding,
550 : required this.buttonRect,
551 : required this.selectedIndex,
552 : this.elevation = 8,
553 : required this.capturedThemes,
554 : this.style,
555 : required this.dropdownColor,
556 : required this.enableFeedback,
557 0 : }) : super(key: key);
558 :
559 : final _GridRowDropdownRoute<T> route;
560 : final BoxConstraints constraints;
561 : final List<_MenuItem<T>>? items;
562 : final EdgeInsetsGeometry padding;
563 : final Rect buttonRect;
564 : final int selectedIndex;
565 : final int elevation;
566 : final CapturedThemes capturedThemes;
567 : final TextStyle? style;
568 : final Color? dropdownColor;
569 : final bool enableFeedback;
570 :
571 0 : @override
572 : Widget build(BuildContext context) {
573 0 : assert(debugCheckHasDirectionality(context));
574 :
575 : // Computing the initialScrollOffset now, before the items have been laid
576 : // out. This only works if the item heights are effectively fixed, i.e. either
577 : // GridRowDropdownButton.itemHeight is specified or GridRowDropdownButton.itemHeight is null
578 : // and all of the items' intrinsic heights are less than kMinInteractiveDimension.
579 : // Otherwise the initialScrollOffset is just a rough approximation based on
580 : // treating the items as if their heights were all equal to kMinInteractiveDimension.
581 0 : if (route.scrollController == null) {
582 0 : final _MenuLimits menuLimits = route.getMenuLimits(buttonRect, constraints.maxHeight, selectedIndex);
583 0 : route.scrollController = ScrollController(initialScrollOffset: menuLimits.scrollOffset);
584 : }
585 :
586 0 : final TextDirection? textDirection = Directionality.maybeOf(context);
587 0 : final Widget menu = _GridRowDropdownMenu<T>(
588 0 : route: route,
589 0 : padding: padding.resolve(textDirection),
590 0 : buttonRect: buttonRect,
591 0 : constraints: constraints,
592 0 : dropdownColor: dropdownColor,
593 0 : enableFeedback: enableFeedback,
594 : );
595 :
596 0 : return MediaQuery.removePadding(
597 : context: context,
598 : removeTop: true,
599 : removeBottom: true,
600 : removeLeft: true,
601 : removeRight: true,
602 0 : child: Builder(
603 0 : builder: (BuildContext context) {
604 0 : return CustomSingleChildLayout(
605 0 : delegate: _GridRowDropdownMenuRouteLayout<T>(
606 0 : buttonRect: buttonRect,
607 0 : route: route,
608 : textDirection: textDirection,
609 : ),
610 0 : child: capturedThemes.wrap(menu),
611 : );
612 : },
613 : ),
614 : );
615 : }
616 : }
617 :
618 : // This widget enables _GridRowDropdownRoute to look up the sizes of
619 : // each menu item. These sizes are used to compute the offset of the selected
620 : // item so that _GridRowDropdownRoutePage can align the vertical center of the
621 : // selected item lines up with the vertical center of the dropdown button,
622 : // as closely as possible.
623 : class _MenuItem<T> extends SingleChildRenderObjectWidget {
624 0 : const _MenuItem({
625 : Key? key,
626 : required this.onLayout,
627 : required this.item,
628 0 : }) : assert(onLayout != null), super(key: key, child: item);
629 :
630 : final ValueChanged<Size> onLayout;
631 : final GridRowDropdownMenuItem<T>? item;
632 :
633 0 : @override
634 : RenderObject createRenderObject(BuildContext context) {
635 0 : return _RenderMenuItem(onLayout);
636 : }
637 :
638 0 : @override
639 : void updateRenderObject(BuildContext context, covariant _RenderMenuItem renderObject) {
640 0 : renderObject.onLayout = onLayout;
641 : }
642 : }
643 :
644 : class _RenderMenuItem extends RenderProxyBox {
645 0 : _RenderMenuItem(this.onLayout, [RenderBox? child]) : assert(onLayout != null), super(child);
646 :
647 : ValueChanged<Size> onLayout;
648 :
649 0 : @override
650 : void performLayout() {
651 0 : super.performLayout();
652 0 : onLayout(size);
653 : }
654 : }
655 :
656 : // The container widget for a menu item created by a [GridRowDropdownButton]. It
657 : // provides the default configuration for [GridRowDropdownMenuItem]s, as well as a
658 : // [GridRowDropdownButton]'s hint and disabledHint widgets.
659 : class _GridRowDropdownMenuItemContainer extends StatelessWidget {
660 : /// Creates an item for a dropdown menu.
661 : ///
662 : /// The [child] argument is required.
663 0 : const _GridRowDropdownMenuItemContainer({
664 : Key? key,
665 : this.alignment = AlignmentDirectional.centerStart,
666 : required this.child,
667 0 : }) : assert(child != null),
668 0 : super(key: key);
669 :
670 : /// The widget below this widget in the tree.
671 : ///
672 : /// Typically a [Text] widget.
673 : final Widget child;
674 :
675 : /// Defines how the item is positioned within the container.
676 : ///
677 : /// This property must not be null. It defaults to [AlignmentDirectional.centerStart].
678 : ///
679 : /// See also:
680 : ///
681 : /// * [Alignment], a class with convenient constants typically used to
682 : /// specify an [AlignmentGeometry].
683 : /// * [AlignmentDirectional], like [Alignment] for specifying alignments
684 : /// relative to text direction.
685 : final AlignmentGeometry alignment;
686 :
687 0 : @override
688 : Widget build(BuildContext context) {
689 0 : return Container(
690 : constraints: const BoxConstraints(minHeight: _kMenuItemHeight),
691 0 : alignment: alignment,
692 0 : child: child,
693 : );
694 : }
695 : }
696 :
697 : /// An item in a menu created by a [GridRowDropdownButton].
698 : ///
699 : /// The type `T` is the type of the value the entry represents. All the entries
700 : /// in a given menu must represent values with consistent types.
701 : class GridRowDropdownMenuItem<T> extends _GridRowDropdownMenuItemContainer {
702 : /// Creates an item for a dropdown menu.
703 : ///
704 : /// The [child] argument is required.
705 0 : const GridRowDropdownMenuItem({
706 : Key? key,
707 : this.onTap,
708 : this.value,
709 : this.enabled = true,
710 : AlignmentGeometry alignment = AlignmentDirectional.centerStart,
711 : required Widget child,
712 0 : }) : assert(child != null),
713 0 : super(key: key, alignment:alignment, child: child);
714 :
715 : /// Called when the dropdown menu item is tapped.
716 : final VoidCallback? onTap;
717 :
718 : /// The value to return if the user selects this menu item.
719 : ///
720 : /// Eventually returned in a call to [GridRowDropdownButton.onChanged].
721 : final T? value;
722 :
723 : /// Whether or not a user can select this menu item.
724 : ///
725 : /// Defaults to `true`.
726 : final bool enabled;
727 : }
728 :
729 : /// An inherited widget that causes any descendant [GridRowDropdownButton]
730 : /// widgets to not include their regular underline.
731 : ///
732 : /// This is used by [DataTable] to remove the underline from any
733 : /// [GridRowDropdownButton] widgets placed within material data tables, as
734 : /// required by the material design specification.
735 : class GridRowDropdownButtonHideUnderline extends InheritedWidget {
736 : /// Creates a [GridRowDropdownButtonHideUnderline]. A non-null [child] must
737 : /// be given.
738 0 : const GridRowDropdownButtonHideUnderline({
739 : Key? key,
740 : required Widget child,
741 0 : }) : assert(child != null),
742 0 : super(key: key, child: child);
743 :
744 : /// Returns whether the underline of [GridRowDropdownButton] widgets should
745 : /// be hidden.
746 0 : static bool at(BuildContext context) {
747 0 : return context.dependOnInheritedWidgetOfExactType<GridRowDropdownButtonHideUnderline>() != null;
748 : }
749 :
750 0 : @override
751 : bool updateShouldNotify(GridRowDropdownButtonHideUnderline oldWidget) => false;
752 : }
753 :
754 : /// A material design button for selecting from a list of items.
755 : ///
756 : /// A dropdown button lets the user select from a number of items. The button
757 : /// shows the currently selected item as well as an arrow that opens a menu for
758 : /// selecting another item.
759 : ///
760 : /// The type `T` is the type of the [value] that each dropdown item represents.
761 : /// All the entries in a given menu must represent values with consistent types.
762 : /// Typically, an enum is used. Each [GridRowDropdownMenuItem] in [items] must be
763 : /// specialized with that same type argument.
764 : ///
765 : /// The [onChanged] callback should update a state variable that defines the
766 : /// dropdown's value. It should also call [State.setState] to rebuild the
767 : /// dropdown with the new value.
768 : ///
769 : /// {@tool dartpad --template=stateful_widget_scaffold_center}
770 : ///
771 : /// This sample shows a `GridRowDropdownButton` with a large arrow icon,
772 : /// purple text style, and bold purple underline, whose value is one of "One",
773 : /// "Two", "Free", or "Four".
774 : ///
775 : /// 
776 : ///
777 : /// ```dart
778 : /// String dropdownValue = 'One';
779 : ///
780 : /// @override
781 : /// Widget build(BuildContext context) {
782 : /// return GridRowDropdownButton<String>(
783 : /// value: dropdownValue,
784 : /// icon: const Icon(Icons.arrow_downward),
785 : /// iconSize: 24,
786 : /// elevation: 16,
787 : /// style: const TextStyle(
788 : /// color: Colors.deepPurple
789 : /// ),
790 : /// underline: Container(
791 : /// height: 2,
792 : /// color: Colors.deepPurpleAccent,
793 : /// ),
794 : /// onChanged: (String? newValue) {
795 : /// setState(() {
796 : /// dropdownValue = newValue!;
797 : /// });
798 : /// },
799 : /// items: <String>['One', 'Two', 'Free', 'Four']
800 : /// .map<GridRowDropdownMenuItem<String>>((String value) {
801 : /// return GridRowDropdownMenuItem<String>(
802 : /// value: value,
803 : /// child: Text(value),
804 : /// );
805 : /// })
806 : /// .toList(),
807 : /// );
808 : /// }
809 : /// ```
810 : /// {@end-tool}
811 : ///
812 : /// If the [onChanged] callback is null or the list of [items] is null
813 : /// then the dropdown button will be disabled, i.e. its arrow will be
814 : /// displayed in grey and it will not respond to input. A disabled button
815 : /// will display the [disabledHint] widget if it is non-null. However, if
816 : /// [disabledHint] is null and [hint] is non-null, the [hint] widget will
817 : /// instead be displayed.
818 : ///
819 : /// Requires one of its ancestors to be a [Material] widget.
820 : ///
821 : /// See also:
822 : ///
823 : /// * [GridRowDropdownMenuItem], the class used to represent the [items].
824 : /// * [GridRowDropdownButtonHideUnderline], which prevents its descendant dropdown buttons
825 : /// from displaying their underlines.
826 : /// * [ElevatedButton], [TextButton], ordinary buttons that trigger a single action.
827 : /// * <https://material.io/design/components/menus.html#dropdown-menu>
828 : class GridRowDropdownButton<T> extends StatefulWidget {
829 : /// Creates a dropdown button.
830 : ///
831 : /// The [items] must have distinct values. If [value] isn't null then it
832 : /// must be equal to one of the [GridRowDropdownMenuItem] values. If [items] or
833 : /// [onChanged] is null, the button will be disabled, the down arrow
834 : /// will be greyed out.
835 : ///
836 : /// If [value] is null and the button is enabled, [hint] will be displayed
837 : /// if it is non-null.
838 : ///
839 : /// If [value] is null and the button is disabled, [disabledHint] will be displayed
840 : /// if it is non-null. If [disabledHint] is null, then [hint] will be displayed
841 : /// if it is non-null.
842 : ///
843 : /// The [elevation] and [iconSize] arguments must not be null (they both have
844 : /// defaults, so do not need to be specified). The boolean [isDense] and
845 : /// [isExpanded] arguments must not be null.
846 : ///
847 : /// The [autofocus] argument must not be null.
848 : ///
849 : /// The [dropdownColor] argument specifies the background color of the
850 : /// dropdown when it is open. If it is null, the current theme's
851 : /// [ThemeData.canvasColor] will be used instead.
852 0 : GridRowDropdownButton({
853 : Key? key,
854 : required this.items,
855 : this.selectedItemBuilder,
856 : this.value,
857 : this.hint,
858 : this.disabledHint,
859 : this.onChanged,
860 : this.onTap,
861 : this.elevation = 8,
862 : this.style,
863 : this.underline,
864 : this.icon,
865 : this.iconDisabledColor,
866 : this.iconEnabledColor,
867 : this.iconSize = 24.0,
868 : this.isDense = false,
869 : this.isExpanded = false,
870 : this.itemHeight = kMinInteractiveDimension,
871 : this.focusColor,
872 : this.focusNode,
873 : this.autofocus = false,
874 : this.dropdownColor,
875 : this.menuMaxHeight,
876 : this.enableFeedback,
877 : this.alignment = AlignmentDirectional.centerStart,
878 : // When adding new arguments, consider adding similar arguments to
879 : // GridRowDropdownButtonFormField.
880 0 : }) : assert(items == null || items.isEmpty || value == null ||
881 0 : items.where((GridRowDropdownMenuItem<T> item) {
882 0 : return item.value == value;
883 0 : }).length == 1,
884 : "There should be exactly one item with [GridRowDropdownButton]'s value: "
885 : '$value. \n'
886 : 'Either zero or 2 or more [GridRowDropdownMenuItem]s were detected '
887 : 'with the same value',
888 : ),
889 0 : assert(elevation != null),
890 0 : assert(iconSize != null),
891 0 : assert(isDense != null),
892 0 : assert(isExpanded != null),
893 0 : assert(autofocus != null),
894 0 : assert(itemHeight == null || itemHeight >= kMinInteractiveDimension),
895 0 : super(key: key);
896 :
897 : /// The list of items the user can select.
898 : ///
899 : /// If the [onChanged] callback is null or the list of items is null
900 : /// then the dropdown button will be disabled, i.e. its arrow will be
901 : /// displayed in grey and it will not respond to input.
902 : final List<GridRowDropdownMenuItem<T>>? items;
903 :
904 : /// The value of the currently selected [GridRowDropdownMenuItem].
905 : ///
906 : /// If [value] is null and the button is enabled, [hint] will be displayed
907 : /// if it is non-null.
908 : ///
909 : /// If [value] is null and the button is disabled, [disabledHint] will be displayed
910 : /// if it is non-null. If [disabledHint] is null, then [hint] will be displayed
911 : /// if it is non-null.
912 : final T? value;
913 :
914 : /// A placeholder widget that is displayed by the dropdown button.
915 : ///
916 : /// If [value] is null and the dropdown is enabled ([items] and [onChanged] are non-null),
917 : /// this widget is displayed as a placeholder for the dropdown button's value.
918 : ///
919 : /// If [value] is null and the dropdown is disabled and [disabledHint] is null,
920 : /// this widget is used as the placeholder.
921 : final Widget? hint;
922 :
923 : /// A preferred placeholder widget that is displayed when the dropdown is disabled.
924 : ///
925 : /// If [value] is null, the dropdown is disabled ([items] or [onChanged] is null),
926 : /// this widget is displayed as a placeholder for the dropdown button's value.
927 : final Widget? disabledHint;
928 :
929 : /// {@template flutter.material.dropdownButton.onChanged}
930 : /// Called when the user selects an item.
931 : ///
932 : /// If the [onChanged] callback is null or the list of [GridRowDropdownButton.items]
933 : /// is null then the dropdown button will be disabled, i.e. its arrow will be
934 : /// displayed in grey and it will not respond to input. A disabled button
935 : /// will display the [GridRowDropdownButton.disabledHint] widget if it is non-null.
936 : /// If [GridRowDropdownButton.disabledHint] is also null but [GridRowDropdownButton.hint] is
937 : /// non-null, [GridRowDropdownButton.hint] will instead be displayed.
938 : /// {@endtemplate}
939 : final ValueChanged<T?>? onChanged;
940 :
941 : /// Called when the dropdown button is tapped.
942 : ///
943 : /// This is distinct from [onChanged], which is called when the user
944 : /// selects an item from the dropdown.
945 : ///
946 : /// The callback will not be invoked if the dropdown button is disabled.
947 : final VoidCallback? onTap;
948 :
949 : /// A builder to customize the dropdown buttons corresponding to the
950 : /// [GridRowDropdownMenuItem]s in [items].
951 : ///
952 : /// When a [GridRowDropdownMenuItem] is selected, the widget that will be displayed
953 : /// from the list corresponds to the [GridRowDropdownMenuItem] of the same index
954 : /// in [items].
955 : ///
956 : /// {@tool dartpad --template=stateful_widget_scaffold}
957 : ///
958 : /// This sample shows a `GridRowDropdownButton` with a button with [Text] that
959 : /// corresponds to but is unique from [GridRowDropdownMenuItem].
960 : ///
961 : /// ```dart
962 : /// final List<String> items = <String>['1','2','3'];
963 : /// String selectedItem = '1';
964 : ///
965 : /// @override
966 : /// Widget build(BuildContext context) {
967 : /// return Padding(
968 : /// padding: const EdgeInsets.symmetric(horizontal: 12.0),
969 : /// child: GridRowDropdownButton<String>(
970 : /// value: selectedItem,
971 : /// onChanged: (String? string) => setState(() => selectedItem = string!),
972 : /// selectedItemBuilder: (BuildContext context) {
973 : /// return items.map<Widget>((String item) {
974 : /// return Text(item);
975 : /// }).toList();
976 : /// },
977 : /// items: items.map((String item) {
978 : /// return GridRowDropdownMenuItem<String>(
979 : /// child: Text('Log $item'),
980 : /// value: item,
981 : /// );
982 : /// }).toList(),
983 : /// ),
984 : /// );
985 : /// }
986 : /// ```
987 : /// {@end-tool}
988 : ///
989 : /// If this callback is null, the [GridRowDropdownMenuItem] from [items]
990 : /// that matches [value] will be displayed.
991 : final GridRowDropdownButtonBuilder? selectedItemBuilder;
992 :
993 : /// The z-coordinate at which to place the menu when open.
994 : ///
995 : /// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12,
996 : /// 16, and 24. See [kElevationToShadow].
997 : ///
998 : /// Defaults to 8, the appropriate elevation for dropdown buttons.
999 : final int elevation;
1000 :
1001 : /// The text style to use for text in the dropdown button and the dropdown
1002 : /// menu that appears when you tap the button.
1003 : ///
1004 : /// To use a separate text style for selected item when it's displayed within
1005 : /// the dropdown button, consider using [selectedItemBuilder].
1006 : ///
1007 : /// {@tool dartpad --template=stateful_widget_scaffold}
1008 : ///
1009 : /// This sample shows a `GridRowDropdownButton` with a dropdown button text style
1010 : /// that is different than its menu items.
1011 : ///
1012 : /// ```dart
1013 : /// List<String> options = <String>['One', 'Two', 'Free', 'Four'];
1014 : /// String dropdownValue = 'One';
1015 : ///
1016 : /// @override
1017 : /// Widget build(BuildContext context) {
1018 : /// return Container(
1019 : /// alignment: Alignment.center,
1020 : /// color: Colors.blue,
1021 : /// child: GridRowDropdownButton<String>(
1022 : /// value: dropdownValue,
1023 : /// onChanged: (String? newValue) {
1024 : /// setState(() {
1025 : /// dropdownValue = newValue!;
1026 : /// });
1027 : /// },
1028 : /// style: const TextStyle(color: Colors.blue),
1029 : /// selectedItemBuilder: (BuildContext context) {
1030 : /// return options.map((String value) {
1031 : /// return Text(
1032 : /// dropdownValue,
1033 : /// style: const TextStyle(color: Colors.white),
1034 : /// );
1035 : /// }).toList();
1036 : /// },
1037 : /// items: options.map<GridRowDropdownMenuItem<String>>((String value) {
1038 : /// return GridRowDropdownMenuItem<String>(
1039 : /// value: value,
1040 : /// child: Text(value),
1041 : /// );
1042 : /// }).toList(),
1043 : /// ),
1044 : /// );
1045 : /// }
1046 : /// ```
1047 : /// {@end-tool}
1048 : ///
1049 : /// Defaults to the [TextTheme.subtitle1] value of the current
1050 : /// [ThemeData.textTheme] of the current [Theme].
1051 : final TextStyle? style;
1052 :
1053 : /// The widget to use for drawing the drop-down button's underline.
1054 : ///
1055 : /// Defaults to a 0.0 width bottom border with color 0xFFBDBDBD.
1056 : final Widget? underline;
1057 :
1058 : /// The widget to use for the drop-down button's icon.
1059 : ///
1060 : /// Defaults to an [Icon] with the [Icons.arrow_drop_down] glyph.
1061 : final Widget? icon;
1062 :
1063 : /// The color of any [Icon] descendant of [icon] if this button is disabled,
1064 : /// i.e. if [onChanged] is null.
1065 : ///
1066 : /// Defaults to [MaterialColor.shade400] of [Colors.grey] when the theme's
1067 : /// [ThemeData.brightness] is [Brightness.light] and to
1068 : /// [Colors.white10] when it is [Brightness.dark]
1069 : final Color? iconDisabledColor;
1070 :
1071 : /// The color of any [Icon] descendant of [icon] if this button is enabled,
1072 : /// i.e. if [onChanged] is defined.
1073 : ///
1074 : /// Defaults to [MaterialColor.shade700] of [Colors.grey] when the theme's
1075 : /// [ThemeData.brightness] is [Brightness.light] and to
1076 : /// [Colors.white70] when it is [Brightness.dark]
1077 : final Color? iconEnabledColor;
1078 :
1079 : /// The size to use for the drop-down button's down arrow icon button.
1080 : ///
1081 : /// Defaults to 24.0.
1082 : final double iconSize;
1083 :
1084 : /// Reduce the button's height.
1085 : ///
1086 : /// By default this button's height is the same as its menu items' heights.
1087 : /// If isDense is true, the button's height is reduced by about half. This
1088 : /// can be useful when the button is embedded in a container that adds
1089 : /// its own decorations, like [InputDecorator].
1090 : final bool isDense;
1091 :
1092 : /// Set the dropdown's inner contents to horizontally fill its parent.
1093 : ///
1094 : /// By default this button's inner width is the minimum size of its contents.
1095 : /// If [isExpanded] is true, the inner width is expanded to fill its
1096 : /// surrounding container.
1097 : final bool isExpanded;
1098 :
1099 : /// If null, then the menu item heights will vary according to each menu item's
1100 : /// intrinsic height.
1101 : ///
1102 : /// The default value is [kMinInteractiveDimension], which is also the minimum
1103 : /// height for menu items.
1104 : ///
1105 : /// If this value is null and there isn't enough vertical room for the menu,
1106 : /// then the menu's initial scroll offset may not align the selected item with
1107 : /// the dropdown button. That's because, in this case, the initial scroll
1108 : /// offset is computed as if all of the menu item heights were
1109 : /// [kMinInteractiveDimension].
1110 : final double? itemHeight;
1111 :
1112 : /// The color for the button's [Material] when it has the input focus.
1113 : final Color? focusColor;
1114 :
1115 : /// {@macro flutter.widgets.Focus.focusNode}
1116 : final FocusNode? focusNode;
1117 :
1118 : /// {@macro flutter.widgets.Focus.autofocus}
1119 : final bool autofocus;
1120 :
1121 : /// The background color of the dropdown.
1122 : ///
1123 : /// If it is not provided, the theme's [ThemeData.canvasColor] will be used
1124 : /// instead.
1125 : final Color? dropdownColor;
1126 :
1127 : /// The maximum height of the menu.
1128 : ///
1129 : /// The maximum height of the menu must be at least one row shorter than
1130 : /// the height of the app's view. This ensures that a tappable area
1131 : /// outside of the simple menu is present so the user can dismiss the menu.
1132 : ///
1133 : /// If this property is set above the maximum allowable height threshold
1134 : /// mentioned above, then the menu defaults to being padded at the top
1135 : /// and bottom of the menu by at one menu item's height.
1136 : final double? menuMaxHeight;
1137 :
1138 : /// Whether detected gestures should provide acoustic and/or haptic feedback.
1139 : ///
1140 : /// For example, on Android a tap will produce a clicking sound and a
1141 : /// long-press will produce a short vibration, when feedback is enabled.
1142 : ///
1143 : /// By default, platform-specific feedback is enabled.
1144 : ///
1145 : /// See also:
1146 : ///
1147 : /// * [Feedback] for providing platform-specific feedback to certain actions.
1148 : final bool? enableFeedback;
1149 :
1150 : /// Defines how the hint or the selected item is positioned within the button.
1151 : ///
1152 : /// This property must not be null. It defaults to [AlignmentDirectional.centerStart].
1153 : ///
1154 : /// See also:
1155 : ///
1156 : /// * [Alignment], a class with convenient constants typically used to
1157 : /// specify an [AlignmentGeometry].
1158 : /// * [AlignmentDirectional], like [Alignment] for specifying alignments
1159 : /// relative to text direction.
1160 : final AlignmentGeometry alignment;
1161 :
1162 0 : @override
1163 0 : State<GridRowDropdownButton<T>> createState() => _GridRowDropdownButtonState<T>();
1164 : }
1165 :
1166 : class _GridRowDropdownButtonState<T> extends State<GridRowDropdownButton<T>> with WidgetsBindingObserver {
1167 : int? _selectedIndex;
1168 : _GridRowDropdownRoute<T>? _dropdownRoute;
1169 : Orientation? _lastOrientation;
1170 : FocusNode? _internalNode;
1171 0 : FocusNode? get focusNode => widget.focusNode ?? _internalNode;
1172 : bool _hasPrimaryFocus = false;
1173 : late Map<Type, Action<Intent>> _actionMap;
1174 : late FocusHighlightMode _focusHighlightMode;
1175 :
1176 : // Only used if needed to create _internalNode.
1177 0 : FocusNode _createFocusNode() {
1178 0 : return FocusNode(debugLabel: '${widget.runtimeType}');
1179 : }
1180 :
1181 0 : @override
1182 : void initState() {
1183 0 : super.initState();
1184 0 : _updateSelectedIndex();
1185 0 : if (widget.focusNode == null) {
1186 0 : _internalNode ??= _createFocusNode();
1187 : }
1188 0 : _actionMap = <Type, Action<Intent>>{
1189 0 : ActivateIntent: CallbackAction<ActivateIntent>(
1190 0 : onInvoke: (ActivateIntent intent) => _handleTap(),
1191 : ),
1192 0 : ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(
1193 0 : onInvoke: (ButtonActivateIntent intent) => _handleTap(),
1194 : ),
1195 : };
1196 0 : focusNode!.addListener(_handleFocusChanged);
1197 0 : final FocusManager focusManager = WidgetsBinding.instance!.focusManager;
1198 0 : _focusHighlightMode = focusManager.highlightMode;
1199 0 : focusManager.addHighlightModeListener(_handleFocusHighlightModeChange);
1200 : }
1201 :
1202 0 : @override
1203 : void dispose() {
1204 0 : WidgetsBinding.instance!.removeObserver(this);
1205 0 : _removeGridRowDropdownRoute();
1206 0 : WidgetsBinding.instance!.focusManager.removeHighlightModeListener(_handleFocusHighlightModeChange);
1207 0 : focusNode!.removeListener(_handleFocusChanged);
1208 0 : _internalNode?.dispose();
1209 0 : super.dispose();
1210 : }
1211 :
1212 0 : void _removeGridRowDropdownRoute() {
1213 0 : _dropdownRoute?._dismiss();
1214 0 : _dropdownRoute = null;
1215 0 : _lastOrientation = null;
1216 : }
1217 :
1218 0 : void _handleFocusChanged() {
1219 0 : if (_hasPrimaryFocus != focusNode!.hasPrimaryFocus) {
1220 0 : setState(() {
1221 0 : _hasPrimaryFocus = focusNode!.hasPrimaryFocus;
1222 : });
1223 : }
1224 : }
1225 :
1226 0 : void _handleFocusHighlightModeChange(FocusHighlightMode mode) {
1227 0 : if (!mounted) {
1228 : return;
1229 : }
1230 0 : setState(() {
1231 0 : _focusHighlightMode = mode;
1232 : });
1233 : }
1234 :
1235 0 : @override
1236 : void didUpdateWidget(GridRowDropdownButton<T> oldWidget) {
1237 0 : super.didUpdateWidget(oldWidget);
1238 0 : if (widget.focusNode != oldWidget.focusNode) {
1239 0 : oldWidget.focusNode?.removeListener(_handleFocusChanged);
1240 0 : if (widget.focusNode == null) {
1241 0 : _internalNode ??= _createFocusNode();
1242 : }
1243 0 : _hasPrimaryFocus = focusNode!.hasPrimaryFocus;
1244 0 : focusNode!.addListener(_handleFocusChanged);
1245 : }
1246 0 : _updateSelectedIndex();
1247 : }
1248 :
1249 0 : void _updateSelectedIndex() {
1250 0 : if (widget.items == null
1251 0 : || widget.items!.isEmpty
1252 0 : || (widget.value == null &&
1253 0 : widget.items!
1254 0 : .where((GridRowDropdownMenuItem<T> item) => item.enabled && item.value == widget.value)
1255 0 : .isEmpty)) {
1256 0 : _selectedIndex = null;
1257 : return;
1258 : }
1259 :
1260 0 : assert(widget.items!.where((GridRowDropdownMenuItem<T> item) => item.value == widget.value).length == 1);
1261 0 : for (int itemIndex = 0; itemIndex < widget.items!.length; itemIndex++) {
1262 0 : if (widget.items![itemIndex].value == widget.value) {
1263 0 : _selectedIndex = itemIndex;
1264 : return;
1265 : }
1266 : }
1267 : }
1268 :
1269 0 : TextStyle? get _textStyle => widget.style ?? Theme.of(context).textTheme.subtitle1;
1270 :
1271 0 : void _handleTap() {
1272 0 : final TextDirection? textDirection = Directionality.maybeOf(context);
1273 0 : final EdgeInsetsGeometry menuMargin = ButtonTheme.of(context).alignedDropdown
1274 : ? _kAlignedMenuMargin
1275 : : _kUnalignedMenuMargin;
1276 :
1277 0 : final List<_MenuItem<T>> menuItems = <_MenuItem<T>>[
1278 0 : for (int index = 0; index < widget.items!.length; index += 1)
1279 0 : _MenuItem<T>(
1280 0 : item: widget.items![index],
1281 0 : onLayout: (Size size) {
1282 : // If [_dropdownRoute] is null and onLayout is called, this means
1283 : // that performLayout was called on a _GridRowDropdownRoute that has not
1284 : // left the widget tree but is already on its way out.
1285 : //
1286 : // Since onLayout is used primarily to collect the desired heights
1287 : // of each menu item before laying them out, not having the _GridRowDropdownRoute
1288 : // collect each item's height to lay out is fine since the route is
1289 : // already on its way out.
1290 0 : if (_dropdownRoute == null)
1291 : return;
1292 :
1293 0 : _dropdownRoute!.itemHeights[index] = size.height;
1294 : },
1295 : ),
1296 : ];
1297 :
1298 0 : final NavigatorState navigator = Navigator.of(context);
1299 0 : assert(_dropdownRoute == null);
1300 0 : final RenderBox itemBox = context.findRenderObject()! as RenderBox;
1301 0 : final Rect itemRect = itemBox.localToGlobal(Offset.zero, ancestor: navigator.context.findRenderObject()) & itemBox.size;
1302 0 : _dropdownRoute = _GridRowDropdownRoute<T>(
1303 : items: menuItems,
1304 0 : buttonRect: menuMargin.resolve(textDirection).inflateRect(itemRect),
1305 0 : padding: _kMenuItemPadding.resolve(textDirection),
1306 0 : selectedIndex: _selectedIndex ?? 0,
1307 0 : elevation: widget.elevation,
1308 0 : capturedThemes: InheritedTheme.capture(from: context, to: navigator.context),
1309 0 : style: _textStyle!,
1310 0 : barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
1311 0 : itemHeight: widget.itemHeight,
1312 0 : dropdownColor: widget.dropdownColor,
1313 0 : menuMaxHeight: widget.menuMaxHeight,
1314 0 : enableFeedback: widget.enableFeedback ?? true,
1315 : );
1316 :
1317 0 : navigator.push(_dropdownRoute!).then<void>((_GridRowDropdownRouteResult<T>? newValue) {
1318 0 : _removeGridRowDropdownRoute();
1319 0 : if (!mounted || newValue == null)
1320 : return;
1321 0 : widget.onChanged?.call(newValue.result);
1322 : });
1323 :
1324 0 : widget.onTap?.call();
1325 : }
1326 :
1327 : // When isDense is true, reduce the height of this button from _kMenuItemHeight to
1328 : // _kDenseButtonHeight, but don't make it smaller than the text that it contains.
1329 : // Similarly, we don't reduce the height of the button so much that its icon
1330 : // would be clipped.
1331 0 : double get _denseButtonHeight {
1332 0 : final double fontSize = _textStyle!.fontSize ?? Theme.of(context).textTheme.subtitle1!.fontSize!;
1333 0 : return math.max(fontSize, math.max(widget.iconSize, _kDenseButtonHeight));
1334 : }
1335 :
1336 0 : Color get _iconColor {
1337 : // These colors are not defined in the Material Design spec.
1338 0 : if (_enabled) {
1339 0 : if (widget.iconEnabledColor != null)
1340 0 : return widget.iconEnabledColor!;
1341 :
1342 0 : switch (Theme.of(context).brightness) {
1343 0 : case Brightness.light:
1344 0 : return Colors.grey.shade700;
1345 0 : case Brightness.dark:
1346 : return Colors.white70;
1347 : }
1348 : } else {
1349 0 : if (widget.iconDisabledColor != null)
1350 0 : return widget.iconDisabledColor!;
1351 :
1352 0 : switch (Theme.of(context).brightness) {
1353 0 : case Brightness.light:
1354 0 : return Colors.grey.shade400;
1355 0 : case Brightness.dark:
1356 : return Colors.white10;
1357 : }
1358 : }
1359 : }
1360 :
1361 0 : bool get _enabled => widget.items != null && widget.items!.isNotEmpty && widget.onChanged != null;
1362 :
1363 0 : Orientation _getOrientation(BuildContext context) {
1364 0 : Orientation? result = MediaQuery.maybeOf(context)?.orientation;
1365 : if (result == null) {
1366 : // If there's no MediaQuery, then use the window aspect to determine
1367 : // orientation.
1368 0 : final Size size = window.physicalSize;
1369 0 : result = size.width > size.height ? Orientation.landscape : Orientation.portrait;
1370 : }
1371 : return result;
1372 : }
1373 :
1374 0 : bool get _showHighlight {
1375 0 : switch (_focusHighlightMode) {
1376 0 : case FocusHighlightMode.touch:
1377 : return false;
1378 0 : case FocusHighlightMode.traditional:
1379 0 : return _hasPrimaryFocus;
1380 : }
1381 : }
1382 :
1383 0 : @override
1384 : Widget build(BuildContext context) {
1385 0 : assert(debugCheckHasMaterial(context));
1386 0 : assert(debugCheckHasMaterialLocalizations(context));
1387 0 : final Orientation newOrientation = _getOrientation(context);
1388 0 : _lastOrientation ??= newOrientation;
1389 0 : if (newOrientation != _lastOrientation) {
1390 0 : _removeGridRowDropdownRoute();
1391 0 : _lastOrientation = newOrientation;
1392 : }
1393 :
1394 : // The width of the button and the menu are defined by the widest
1395 : // item and the width of the hint.
1396 : // We should explicitly type the items list to be a list of <Widget>,
1397 : // otherwise, no explicit type adding items maybe trigger a crash/failure
1398 : // when hint and selectedItemBuilder are provided.
1399 0 : final List<Widget> items = widget.selectedItemBuilder == null
1400 0 : ? (widget.items != null ? List<Widget>.from(widget.items!) : <Widget>[])
1401 0 : : List<Widget>.from(widget.selectedItemBuilder!(context));
1402 :
1403 : int? hintIndex;
1404 0 : if (widget.hint != null || (!_enabled && widget.disabledHint != null)) {
1405 0 : Widget displayedHint = _enabled ? widget.hint! : widget.disabledHint ?? widget.hint!;
1406 0 : if (widget.selectedItemBuilder == null)
1407 0 : displayedHint = _GridRowDropdownMenuItemContainer(child: displayedHint);
1408 :
1409 0 : hintIndex = items.length;
1410 0 : items.add(DefaultTextStyle(
1411 0 : style: _textStyle!.copyWith(color: Theme.of(context).hintColor),
1412 0 : child: IgnorePointer(
1413 : ignoringSemantics: false,
1414 : child: displayedHint,
1415 : ),
1416 : ));
1417 : }
1418 :
1419 0 : final EdgeInsetsGeometry padding = ButtonTheme.of(context).alignedDropdown
1420 : ? _kAlignedButtonPadding
1421 : : _kUnalignedButtonPadding;
1422 :
1423 : // If value is null (then _selectedIndex is null) then we
1424 : // display the hint or nothing at all.
1425 : final Widget innerItemsWidget;
1426 0 : if (items.isEmpty) {
1427 0 : innerItemsWidget = Container();
1428 : } else {
1429 0 : innerItemsWidget = IndexedStack(
1430 0 : index: _selectedIndex ?? hintIndex,
1431 0 : alignment: widget.alignment,
1432 0 : children: widget.isDense ? items : items.map((Widget item) {
1433 0 : return widget.itemHeight != null
1434 0 : ? SizedBox(height: widget.itemHeight, child: item)
1435 0 : : Column(mainAxisSize: MainAxisSize.min, children: <Widget>[item]);
1436 0 : }).toList(),
1437 : );
1438 : }
1439 :
1440 : const Icon defaultIcon = Icon(Icons.arrow_drop_down);
1441 :
1442 0 : Widget result = DefaultTextStyle(
1443 0 : style: _enabled ? _textStyle! : _textStyle!.copyWith(color: Theme.of(context).disabledColor),
1444 0 : child: Container(
1445 0 : decoration: _showHighlight
1446 0 : ? BoxDecoration(
1447 0 : color: widget.focusColor ?? Theme.of(context).focusColor,
1448 : borderRadius: const BorderRadius.all(Radius.circular(4.0)),
1449 : )
1450 : : null,
1451 0 : padding: padding.resolve(Directionality.of(context)),
1452 0 : height: widget.isDense ? _denseButtonHeight : null,
1453 0 : child: Row(
1454 : mainAxisAlignment: MainAxisAlignment.spaceBetween,
1455 : mainAxisSize: MainAxisSize.min,
1456 0 : children: <Widget>[
1457 0 : if (widget.isExpanded)
1458 0 : Expanded(child: innerItemsWidget)
1459 : else
1460 0 : innerItemsWidget,
1461 0 : IconTheme(
1462 0 : data: IconThemeData(
1463 0 : color: _iconColor,
1464 0 : size: widget.iconSize,
1465 : ),
1466 0 : child: widget.icon ?? defaultIcon,
1467 : ),
1468 : ],
1469 : ),
1470 : ),
1471 : );
1472 :
1473 0 : if (!GridRowDropdownButtonHideUnderline.at(context)) {
1474 0 : final double bottom = (widget.isDense || widget.itemHeight == null) ? 0.0 : 8.0;
1475 0 : result = Stack(
1476 0 : children: <Widget>[
1477 : result,
1478 0 : Positioned(
1479 : left: 0.0,
1480 : right: 0.0,
1481 : bottom: bottom,
1482 0 : child: widget.underline ?? Container(
1483 : height: 1.0,
1484 : decoration: const BoxDecoration(
1485 : border: Border(
1486 : bottom: BorderSide(
1487 : color: Color(0xFFBDBDBD),
1488 : width: 0.0,
1489 : ),
1490 : ),
1491 : ),
1492 : ),
1493 : ),
1494 : ],
1495 : );
1496 : }
1497 :
1498 0 : final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
1499 : MaterialStateMouseCursor.clickable,
1500 : <MaterialState>{
1501 0 : if (!_enabled) MaterialState.disabled,
1502 : },
1503 : );
1504 :
1505 0 : return Semantics(
1506 : button: true,
1507 0 : child: Actions(
1508 0 : actions: _actionMap,
1509 0 : child: Focus(
1510 0 : canRequestFocus: _enabled,
1511 0 : focusNode: focusNode,
1512 0 : autofocus: widget.autofocus,
1513 0 : child: MouseRegion(
1514 : cursor: effectiveMouseCursor,
1515 0 : child: GestureDetector(
1516 0 : onTap: _enabled ? _handleTap : null,
1517 : behavior: HitTestBehavior.opaque,
1518 : child: result,
1519 : ),
1520 : ),
1521 : ),
1522 : ),
1523 : );
1524 : }
1525 : }
1526 :
1527 : /// A convenience widget that makes a [GridRowDropdownButton] into a [FormField].
1528 : class GridRowDropdownButtonFormField<T> extends FormField<T> {
1529 : /// Creates a [GridRowDropdownButton] widget that is a [FormField], wrapped in an
1530 : /// [InputDecorator].
1531 : ///
1532 : /// For a description of the `onSaved`, `validator`, or `autovalidateMode`
1533 : /// parameters, see [FormField]. For the rest (other than [decoration]), see
1534 : /// [GridRowDropdownButton].
1535 : ///
1536 : /// The `items`, `elevation`, `iconSize`, `isDense`, `isExpanded`,
1537 : /// `autofocus`, and `decoration` parameters must not be null.
1538 0 : GridRowDropdownButtonFormField({
1539 : Key? key,
1540 : required List<GridRowDropdownMenuItem<T>>? items,
1541 : GridRowDropdownButtonBuilder? selectedItemBuilder,
1542 : T? value,
1543 : Widget? hint,
1544 : Widget? disabledHint,
1545 : this.onChanged,
1546 : VoidCallback? onTap,
1547 : int elevation = 8,
1548 : TextStyle? style,
1549 : Widget? icon,
1550 : Color? iconDisabledColor,
1551 : Color? iconEnabledColor,
1552 : double iconSize = 24.0,
1553 : bool isDense = true,
1554 : bool isExpanded = false,
1555 : double? itemHeight,
1556 : Color? focusColor,
1557 : FocusNode? focusNode,
1558 : bool autofocus = false,
1559 : Color? dropdownColor,
1560 : InputDecoration? decoration,
1561 : FormFieldSetter<T>? onSaved,
1562 : FormFieldValidator<T>? validator,
1563 : @Deprecated(
1564 : 'Use autovalidateMode parameter which provide more specific '
1565 : 'behaviour related to auto validation. '
1566 : 'This feature was deprecated after v1.19.0.',
1567 : )
1568 : bool autovalidate = false,
1569 : AutovalidateMode? autovalidateMode,
1570 : double? menuMaxHeight,
1571 : bool? enableFeedback,
1572 : AlignmentGeometry alignment = AlignmentDirectional.centerStart,
1573 0 : }) : assert(items == null || items.isEmpty || value == null ||
1574 0 : items.where((GridRowDropdownMenuItem<T> item) {
1575 0 : return item.value == value;
1576 0 : }).length == 1,
1577 : "There should be exactly one item with [GridRowDropdownButton]'s value: "
1578 : '$value. \n'
1579 : 'Either zero or 2 or more [GridRowDropdownMenuItem]s were detected '
1580 : 'with the same value',
1581 : ),
1582 0 : assert(elevation != null),
1583 0 : assert(iconSize != null),
1584 0 : assert(isDense != null),
1585 0 : assert(isExpanded != null),
1586 0 : assert(itemHeight == null || itemHeight >= kMinInteractiveDimension),
1587 0 : assert(autofocus != null),
1588 0 : assert(autovalidate != null),
1589 : assert(
1590 0 : autovalidate == false ||
1591 0 : autovalidate == true && autovalidateMode == null,
1592 : 'autovalidate and autovalidateMode should not be used together.',
1593 : ),
1594 0 : decoration = decoration ?? InputDecoration(focusColor: focusColor),
1595 0 : super(
1596 : key: key,
1597 : onSaved: onSaved,
1598 : initialValue: value,
1599 : validator: validator,
1600 : autovalidateMode: autovalidate
1601 : ? AutovalidateMode.always
1602 : : (autovalidateMode ?? AutovalidateMode.disabled),
1603 0 : builder: (FormFieldState<T> field) {
1604 : final _GridRowDropdownButtonFormFieldState<T> state = field as _GridRowDropdownButtonFormFieldState<T>;
1605 0 : final InputDecoration decorationArg = decoration ?? InputDecoration(focusColor: focusColor);
1606 0 : final InputDecoration effectiveDecoration = decorationArg.applyDefaults(
1607 0 : Theme.of(field.context).inputDecorationTheme,
1608 : );
1609 : // An unfocusable Focus widget so that this widget can detect if its
1610 : // descendants have focus or not.
1611 0 : return Focus(
1612 : canRequestFocus: false,
1613 : skipTraversal: true,
1614 0 : child: Builder(builder: (BuildContext context) {
1615 0 : return InputDecorator(
1616 0 : decoration: effectiveDecoration.copyWith(errorText: field.errorText),
1617 0 : isEmpty: state.value == null,
1618 0 : isFocused: Focus.of(context).hasFocus,
1619 0 : child: GridRowDropdownButtonHideUnderline(
1620 0 : child: GridRowDropdownButton<T>(
1621 : items: items,
1622 : selectedItemBuilder: selectedItemBuilder,
1623 0 : value: state.value,
1624 : hint: hint,
1625 : disabledHint: disabledHint,
1626 0 : onChanged: onChanged == null ? null : state.didChange,
1627 : onTap: onTap,
1628 : elevation: elevation,
1629 : style: style,
1630 : icon: icon,
1631 : iconDisabledColor: iconDisabledColor,
1632 : iconEnabledColor: iconEnabledColor,
1633 : iconSize: iconSize,
1634 : isDense: isDense,
1635 : isExpanded: isExpanded,
1636 : itemHeight: itemHeight,
1637 : focusColor: focusColor,
1638 : focusNode: focusNode,
1639 : autofocus: autofocus,
1640 : dropdownColor: dropdownColor,
1641 : menuMaxHeight: menuMaxHeight,
1642 : enableFeedback: enableFeedback,
1643 : alignment: alignment,
1644 : ),
1645 : ),
1646 : );
1647 : }),
1648 : );
1649 : },
1650 : );
1651 :
1652 : /// {@macro flutter.material.dropdownButton.onChanged}
1653 : final ValueChanged<T?>? onChanged;
1654 :
1655 : /// The decoration to show around the dropdown button form field.
1656 : ///
1657 : /// By default, draws a horizontal line under the dropdown button field but
1658 : /// can be configured to show an icon, label, hint text, and error text.
1659 : ///
1660 : /// If not specified, an [InputDecorator] with the `focusColor` set to the
1661 : /// supplied `focusColor` (if any) will be used.
1662 : final InputDecoration decoration;
1663 :
1664 0 : @override
1665 0 : FormFieldState<T> createState() => _GridRowDropdownButtonFormFieldState<T>();
1666 : }
1667 :
1668 : class _GridRowDropdownButtonFormFieldState<T> extends FormFieldState<T> {
1669 0 : @override
1670 0 : GridRowDropdownButtonFormField<T> get widget => super.widget as GridRowDropdownButtonFormField<T>;
1671 :
1672 0 : @override
1673 : void didChange(T? value) {
1674 0 : super.didChange(value);
1675 0 : assert(widget.onChanged != null);
1676 0 : widget.onChanged!(value);
1677 : }
1678 :
1679 0 : @override
1680 : void didUpdateWidget(GridRowDropdownButtonFormField<T> oldWidget) {
1681 0 : super.didUpdateWidget(oldWidget);
1682 0 : if (oldWidget.initialValue != widget.initialValue) {
1683 0 : setValue(widget.initialValue);
1684 : }
1685 : }
1686 : }
|