import 'dart:core'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:get/get.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.enable = true, 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 bool enable; 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 { @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 mod_menu.PopupMenuButton( enabled: widget.enable, 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), 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 Color commonColor; const MenuConfig( {required this.commonColor, this.height = kMinInteractiveDimension, this.dividerHeight = 16.0}); } abstract class MenuEntryBase { bool dismissOnClicked; MenuEntryBase({this.dismissOnClicked = false}); 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; MenuEntryRadioOption( {required this.text, required this.value, this.dismissOnClicked = false}); } 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; MenuEntryRadios( {required this.text, required this.optionsGetter, required this.curOptionGetter, required this.optionSetter, dismissOnClicked = false}) : super(dismissOnClicked: dismissOnClicked) { () 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: TextButton( child: Container( alignment: AlignmentDirectional.centerStart, constraints: BoxConstraints(minHeight: conf.height), child: Row( children: [ Text( opt.text, style: TextStyle( color: MyTheme.color(context).text, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), Expanded( child: Align( alignment: Alignment.centerRight, child: SizedBox( width: 20.0, height: 20.0, child: Obx(() => opt.value == curOption.value ? 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; MenuEntrySubRadios( {required this.text, required this.optionsGetter, required this.curOptionGetter, required this.optionSetter, dismissOnClicked = false}) : super(dismissOnClicked: dismissOnClicked) { () 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: TextButton( child: Container( alignment: AlignmentDirectional.centerStart, constraints: BoxConstraints(minHeight: conf.height), child: Row( children: [ Text( opt.text, style: TextStyle( color: MyTheme.color(context).text, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), Expanded( child: Align( alignment: Alignment.centerRight, child: SizedBox( width: 20.0, height: 20.0, child: Obx(() => opt.value == curOption.value ? 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( padding: EdgeInsets.zero, 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: MyTheme.color(context).text, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), Expanded( child: Align( alignment: Alignment.centerRight, child: Icon( Icons.keyboard_arrow_right, color: conf.commonColor, ), )) ]), ) ]; } } typedef SwitchGetter = Future Function(); typedef SwitchSetter = Future Function(bool); abstract class MenuEntrySwitchBase extends MenuEntryBase { final String text; final Rx? textStyle; MenuEntrySwitchBase( {required this.text, required dismissOnClicked, this.textStyle}) : super(dismissOnClicked: dismissOnClicked); RxBool get curOption; Future setOption(bool option); @override List> build( BuildContext context, MenuConfig conf) { return [ mod_menu.PopupMenuItem( padding: EdgeInsets.zero, height: conf.height, child: TextButton( child: Container( alignment: AlignmentDirectional.centerStart, height: conf.height, child: Row(children: [ () { if (textStyle != null) { final style = textStyle!; return Obx(() => Text( text, style: style.value, )); } else { return Text( text, style: const TextStyle( color: Colors.black, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ); } }(), Expanded( child: Align( alignment: Alignment.centerRight, child: Obx(() => Switch( 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 String text, required this.getter, required this.setter, Rx? textStyle, dismissOnClicked = false}) : super( text: text, textStyle: textStyle, dismissOnClicked: dismissOnClicked) { () async { _curOption.value = await getter(); }(); } @override RxBool get curOption => _curOption; @override setOption(bool option) async { 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 String text, required this.getter, required this.setter, Rx? textStyle, dismissOnClicked = false}) : super( text: text, textStyle: textStyle, dismissOnClicked: dismissOnClicked); @override RxBool get curOption => getter(); @override setOption(bool option) async { await setter(option); } } class MenuEntrySubMenu extends MenuEntryBase { final String text; final List> entries; MenuEntrySubMenu({required this.text, required this.entries}); @override List> build( BuildContext context, MenuConfig conf) { return [ PopupMenuChildrenItem( height: conf.height, padding: EdgeInsets.zero, 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), Text( text, style: TextStyle( color: MyTheme.color(context).text, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), Expanded( child: Align( alignment: Alignment.centerRight, child: Icon( Icons.keyboard_arrow_right, color: conf.commonColor, ), )) ]), ) ]; } } class MenuEntryButton extends MenuEntryBase { final Widget Function(TextStyle? style) childBuilder; Function() proc; MenuEntryButton( {required this.childBuilder, required this.proc, dismissOnClicked = false}) : super(dismissOnClicked: dismissOnClicked); @override List> build( BuildContext context, MenuConfig conf) { return [ mod_menu.PopupMenuItem( padding: EdgeInsets.zero, height: conf.height, child: TextButton( child: Container( alignment: AlignmentDirectional.centerStart, constraints: BoxConstraints(minHeight: conf.height), child: childBuilder( TextStyle( color: MyTheme.color(context).text, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), )), onPressed: () { if (super.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); } proc(); }, ), ) ]; } }