import 'dart:core'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../common.dart'; import './material_mod_popup_menu.dart' as mod_menu; // https://stackoverflow.com/questions/68318314/flutter-popup-menu-inside-popup-menu class PopupMenuChildrenItem extends mod_menu.PopupMenuEntry { const PopupMenuChildrenItem({ key, this.height = kMinInteractiveDimension, this.padding, this.enabled, this.textStyle, this.onTap, this.position = mod_menu.PopupMenuPosition.overSide, this.offset = Offset.zero, required this.itemBuilder, required this.child, }) : super(key: key); final mod_menu.PopupMenuPosition position; final Offset offset; final TextStyle? textStyle; final EdgeInsets? padding; final RxBool? enabled; final void Function()? onTap; final List> Function(BuildContext) itemBuilder; final Widget child; @override final double height; @override bool represents(T? value) => false; @override MyPopupMenuItemState> createState() => MyPopupMenuItemState>(); } class MyPopupMenuItemState> extends State { RxBool enabled = true.obs; @override void initState() { super.initState(); if (widget.enabled != null) { enabled.value = widget.enabled!.value; } } @protected void handleTap(T value) { widget.onTap?.call(); Navigator.pop(context, value); } @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); TextStyle style = widget.textStyle ?? popupMenuTheme.textStyle ?? theme.textTheme.subtitle1!; return Obx(() => mod_menu.PopupMenuButton( enabled: enabled.value, position: widget.position, offset: widget.offset, onSelected: handleTap, itemBuilder: widget.itemBuilder, padding: EdgeInsets.zero, child: AnimatedDefaultTextStyle( style: style, duration: kThemeChangeDuration, child: Container( alignment: AlignmentDirectional.centerStart, constraints: BoxConstraints( minHeight: widget.height, maxHeight: widget.height), padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: 16), child: widget.child, ), ), )); } } class MenuConfig { // adapt to the screen height static const fontSize = 14.0; static const midPadding = 10.0; static const iconScale = 0.8; static const iconWidth = 12.0; static const iconHeight = 12.0; final double height; final double dividerHeight; final double? boxWidth; final Color commonColor; const MenuConfig( {required this.commonColor, this.height = kMinInteractiveDimension, this.dividerHeight = 16.0, this.boxWidth}); } abstract class MenuEntryBase { bool dismissOnClicked; RxBool? enabled; MenuEntryBase({ this.dismissOnClicked = false, this.enabled, }); List> build(BuildContext context, MenuConfig conf); } class MenuEntryDivider extends MenuEntryBase { @override List> build( BuildContext context, MenuConfig conf) { return [ mod_menu.PopupMenuDivider( height: conf.dividerHeight, ) ]; } } class MenuEntryRadioOption { String text; String value; bool dismissOnClicked; RxBool? enabled; MenuEntryRadioOption({ required this.text, required this.value, this.dismissOnClicked = false, this.enabled, }); } typedef RadioOptionsGetter = List Function(); typedef RadioCurOptionGetter = Future Function(); typedef RadioOptionSetter = Future Function( String oldValue, String newValue); class MenuEntryRadioUtils {} class MenuEntryRadios extends MenuEntryBase { final String text; final RadioOptionsGetter optionsGetter; final RadioCurOptionGetter curOptionGetter; final RadioOptionSetter optionSetter; final RxString _curOption = "".obs; final EdgeInsets? padding; MenuEntryRadios({ required this.text, required this.optionsGetter, required this.curOptionGetter, required this.optionSetter, this.padding, dismissOnClicked = false, RxBool? enabled, }) : super(dismissOnClicked: dismissOnClicked, enabled: enabled) { () async { _curOption.value = await curOptionGetter(); }(); } List get options => optionsGetter(); RxString get curOption => _curOption; setOption(String option) async { await optionSetter(_curOption.value, option); if (_curOption.value != option) { final opt = await curOptionGetter(); if (_curOption.value != opt) { _curOption.value = opt; } } } mod_menu.PopupMenuEntry _buildMenuItem( BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) { return mod_menu.PopupMenuItem( padding: EdgeInsets.zero, height: conf.height, child: Container( width: conf.boxWidth, child: TextButton( child: Container( padding: padding, alignment: AlignmentDirectional.centerStart, constraints: BoxConstraints( minHeight: conf.height, maxHeight: conf.height), child: Row( children: [ Text( opt.text, style: TextStyle( color: Theme.of(context).textTheme.titleLarge?.color, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), Expanded( child: Align( alignment: Alignment.centerRight, child: Transform.scale( scale: MenuConfig.iconScale, child: Obx(() => opt.value == curOption.value ? IconButton( padding: const EdgeInsets.fromLTRB( 8.0, 0.0, 8.0, 0.0), hoverColor: Colors.transparent, focusColor: Colors.transparent, onPressed: () {}, icon: Icon( Icons.check, color: conf.commonColor, )) : const SizedBox.shrink()), ))), ], ), ), onPressed: () { if (opt.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); } setOption(opt.value); }, )), ); } @override List> build( BuildContext context, MenuConfig conf) { return options.map((opt) => _buildMenuItem(context, conf, opt)).toList(); } } class MenuEntrySubRadios extends MenuEntryBase { final String text; final RadioOptionsGetter optionsGetter; final RadioCurOptionGetter curOptionGetter; final RadioOptionSetter optionSetter; final RxString _curOption = "".obs; final EdgeInsets? padding; MenuEntrySubRadios({ required this.text, required this.optionsGetter, required this.curOptionGetter, required this.optionSetter, this.padding, dismissOnClicked = false, RxBool? enabled, }) : super( dismissOnClicked: dismissOnClicked, enabled: enabled, ) { () async { _curOption.value = await curOptionGetter(); }(); } List get options => optionsGetter(); RxString get curOption => _curOption; setOption(String option) async { await optionSetter(_curOption.value, option); if (_curOption.value != option) { final opt = await curOptionGetter(); if (_curOption.value != opt) { _curOption.value = opt; } } } mod_menu.PopupMenuEntry _buildSecondMenu( BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) { return mod_menu.PopupMenuItem( padding: EdgeInsets.zero, height: conf.height, child: Container( width: conf.boxWidth, child: TextButton( child: Container( padding: padding, alignment: AlignmentDirectional.centerStart, constraints: BoxConstraints( minHeight: conf.height, maxHeight: conf.height), child: Row( children: [ Text( opt.text, style: TextStyle( color: Theme.of(context).textTheme.titleLarge?.color, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), Expanded( child: Align( alignment: Alignment.centerRight, child: Transform.scale( scale: MenuConfig.iconScale, child: Obx(() => opt.value == curOption.value ? IconButton( padding: EdgeInsets.zero, hoverColor: Colors.transparent, focusColor: Colors.transparent, onPressed: () {}, icon: Icon( Icons.check, color: conf.commonColor, )) : const SizedBox.shrink())), )), ], ), ), onPressed: () { if (opt.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); } setOption(opt.value); }, )), ); } @override List> build( BuildContext context, MenuConfig conf) { return [ PopupMenuChildrenItem( enabled: super.enabled, padding: padding, height: conf.height, itemBuilder: (BuildContext context) => options.map((opt) => _buildSecondMenu(context, conf, opt)).toList(), child: Row(children: [ const SizedBox(width: MenuConfig.midPadding), Text( text, style: TextStyle( color: Theme.of(context).textTheme.titleLarge?.color, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), Expanded( child: Align( alignment: Alignment.centerRight, child: Icon( Icons.keyboard_arrow_right, color: conf.commonColor, ), )) ]), ) ]; } } enum SwitchType { sswitch, scheckbox, } typedef SwitchGetter = Future Function(); typedef SwitchSetter = Future Function(bool); abstract class MenuEntrySwitchBase extends MenuEntryBase { final SwitchType switchType; final String text; final EdgeInsets? padding; Rx? textStyle; MenuEntrySwitchBase({ required this.switchType, required this.text, required dismissOnClicked, this.textStyle, this.padding, RxBool? enabled, }) : super(dismissOnClicked: dismissOnClicked, enabled: enabled); RxBool get curOption; Future setOption(bool? option); @override List> build( BuildContext context, MenuConfig conf) { textStyle ??= TextStyle( color: Theme.of(context).textTheme.titleLarge?.color, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal) .obs; return [ mod_menu.PopupMenuItem( padding: EdgeInsets.zero, height: conf.height, child: Container( width: conf.boxWidth, child: TextButton( child: Container( padding: padding, alignment: AlignmentDirectional.centerStart, height: conf.height, child: Row(children: [ Obx(() => Text( text, style: textStyle!.value, )), Expanded( child: Align( alignment: Alignment.centerRight, child: Transform.scale( scale: MenuConfig.iconScale, child: Obx(() { if (switchType == SwitchType.sswitch) { return Switch( value: curOption.value, onChanged: (v) { if (super.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); } setOption(v); }, ); } else { return Checkbox( value: curOption.value, onChanged: (v) { if (super.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); } setOption(v); }, ); } })), )) ])), onPressed: () { if (super.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); } setOption(!curOption.value); }, )), ) ]; } } class MenuEntrySwitch extends MenuEntrySwitchBase { final SwitchGetter getter; final SwitchSetter setter; final RxBool _curOption = false.obs; MenuEntrySwitch({ required SwitchType switchType, required String text, required this.getter, required this.setter, Rx? textStyle, EdgeInsets? padding, dismissOnClicked = false, RxBool? enabled, }) : super( switchType: switchType, text: text, textStyle: textStyle, padding: padding, dismissOnClicked: dismissOnClicked, enabled: enabled, ) { () async { _curOption.value = await getter(); }(); } @override RxBool get curOption => _curOption; @override setOption(bool? option) async { if (option != null) { await setter(option); final opt = await getter(); if (_curOption.value != opt) { _curOption.value = opt; } } } } typedef Switch2Getter = RxBool Function(); typedef Switch2Setter = Future Function(bool); class MenuEntrySwitch2 extends MenuEntrySwitchBase { final Switch2Getter getter; final SwitchSetter setter; MenuEntrySwitch2({ required SwitchType switchType, required String text, required this.getter, required this.setter, Rx? textStyle, EdgeInsets? padding, dismissOnClicked = false, RxBool? enabled, }) : super( switchType: switchType, text: text, textStyle: textStyle, padding: padding, dismissOnClicked: dismissOnClicked); @override RxBool get curOption => getter(); @override setOption(bool? option) async { if (option != null) { await setter(option); } } } class MenuEntrySubMenu extends MenuEntryBase { final String text; final List> entries; final EdgeInsets? padding; MenuEntrySubMenu({ required this.text, required this.entries, this.padding, RxBool? enabled, }) : super(enabled: enabled); @override List> build( BuildContext context, MenuConfig conf) { super.enabled ??= true.obs; return [ PopupMenuChildrenItem( enabled: super.enabled, height: conf.height, padding: padding, position: mod_menu.PopupMenuPosition.overSide, itemBuilder: (BuildContext context) => entries .map((entry) => entry.build(context, conf)) .expand((i) => i) .toList(), child: Row(children: [ const SizedBox(width: MenuConfig.midPadding), Obx(() => Text( text, style: TextStyle( color: super.enabled!.value ? Theme.of(context).textTheme.titleLarge?.color : Colors.grey, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), )), Expanded( child: Align( alignment: Alignment.centerRight, child: Obx(() => Icon( Icons.keyboard_arrow_right, color: super.enabled!.value ? conf.commonColor : Colors.grey, )), )) ]), ) ]; } } class MenuEntryButton extends MenuEntryBase { final Widget Function(TextStyle? style) childBuilder; Function() proc; final EdgeInsets? padding; MenuEntryButton({ required this.childBuilder, required this.proc, this.padding, dismissOnClicked = false, RxBool? enabled, }) : super( dismissOnClicked: dismissOnClicked, enabled: enabled, ); Widget _buildChild(BuildContext context, MenuConfig conf) { final enabledStyle = TextStyle( color: Theme.of(context).textTheme.titleLarge?.color, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal); const disabledStyle = TextStyle( color: Colors.grey, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal); super.enabled ??= true.obs; return Obx(() => Container( width: conf.boxWidth, child: TextButton( onPressed: super.enabled!.value ? () { if (super.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); } proc(); } : null, child: Container( padding: padding, alignment: AlignmentDirectional.centerStart, constraints: BoxConstraints(minHeight: conf.height, maxHeight: conf.height), child: childBuilder( super.enabled!.value ? enabledStyle : disabledStyle), ), ))); } @override List> build( BuildContext context, MenuConfig conf) { return [ mod_menu.PopupMenuItem( padding: EdgeInsets.zero, height: conf.height, child: _buildChild(context, conf), ) ]; } } class CustomPopupMenuTheme { static const Color commonColor = MyTheme.accent; // kMinInteractiveDimension static const double height = 20.0; static const double dividerHeight = 3.0; }