ca97826b80
Signed-off-by: fufesou <shuanglongchen@yeah.net>
719 lines
21 KiB
Dart
719 lines
21 KiB
Dart
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<T> extends mod_menu.PopupMenuEntry<T> {
|
|
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<mod_menu.PopupMenuEntry<T>> Function(BuildContext) itemBuilder;
|
|
final Widget child;
|
|
|
|
@override
|
|
final double height;
|
|
|
|
@override
|
|
bool represents(T? value) => false;
|
|
|
|
@override
|
|
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>> createState() =>
|
|
MyPopupMenuItemState<T, PopupMenuChildrenItem<T>>();
|
|
}
|
|
|
|
class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
|
|
extends State<W> {
|
|
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<T>(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<T>(
|
|
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});
|
|
}
|
|
|
|
typedef DismissCallback = Function();
|
|
|
|
abstract class MenuEntryBase<T> {
|
|
bool dismissOnClicked;
|
|
DismissCallback? dismissCallback;
|
|
RxBool? enabled;
|
|
|
|
MenuEntryBase({
|
|
this.dismissOnClicked = false,
|
|
this.enabled,
|
|
this.dismissCallback,
|
|
});
|
|
List<mod_menu.PopupMenuEntry<T>> build(BuildContext context, MenuConfig conf);
|
|
|
|
enabledStyle(BuildContext context) => TextStyle(
|
|
color: Theme.of(context).textTheme.titleLarge?.color,
|
|
fontSize: MenuConfig.fontSize,
|
|
fontWeight: FontWeight.normal);
|
|
disabledStyle() => TextStyle(
|
|
color: Colors.grey,
|
|
fontSize: MenuConfig.fontSize,
|
|
fontWeight: FontWeight.normal);
|
|
}
|
|
|
|
class MenuEntryDivider<T> extends MenuEntryBase<T> {
|
|
@override
|
|
List<mod_menu.PopupMenuEntry<T>> build(
|
|
BuildContext context, MenuConfig conf) {
|
|
return [
|
|
mod_menu.PopupMenuDivider(
|
|
height: conf.dividerHeight,
|
|
)
|
|
];
|
|
}
|
|
}
|
|
|
|
class MenuEntryRadioOption {
|
|
String text;
|
|
String value;
|
|
bool dismissOnClicked;
|
|
RxBool? enabled;
|
|
DismissCallback? dismissCallback;
|
|
|
|
MenuEntryRadioOption({
|
|
required this.text,
|
|
required this.value,
|
|
this.dismissOnClicked = false,
|
|
this.enabled,
|
|
this.dismissCallback,
|
|
});
|
|
}
|
|
|
|
typedef RadioOptionsGetter = List<MenuEntryRadioOption> Function();
|
|
typedef RadioCurOptionGetter = Future<String> Function();
|
|
typedef RadioOptionSetter = Future<void> Function(
|
|
String oldValue, String newValue);
|
|
|
|
class MenuEntryRadioUtils<T> {}
|
|
|
|
class MenuEntryRadios<T> extends MenuEntryBase<T> {
|
|
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,
|
|
dismissCallback,
|
|
RxBool? enabled,
|
|
}) : super(
|
|
dismissOnClicked: dismissOnClicked,
|
|
enabled: enabled,
|
|
dismissCallback: dismissCallback,
|
|
) {
|
|
() async {
|
|
_curOption.value = await curOptionGetter();
|
|
}();
|
|
}
|
|
|
|
List<MenuEntryRadioOption> 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<T> _buildMenuItem(
|
|
BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) {
|
|
Widget getTextChild() {
|
|
final enabledTextChild = Text(
|
|
opt.text,
|
|
style: enabledStyle(context),
|
|
);
|
|
final disabledTextChild = Text(
|
|
opt.text,
|
|
style: disabledStyle(),
|
|
);
|
|
if (opt.enabled == null) {
|
|
return enabledTextChild;
|
|
} else {
|
|
return Obx(
|
|
() => opt.enabled!.isTrue ? enabledTextChild : disabledTextChild);
|
|
}
|
|
}
|
|
|
|
final child = Container(
|
|
padding: padding,
|
|
alignment: AlignmentDirectional.centerStart,
|
|
constraints:
|
|
BoxConstraints(minHeight: conf.height, maxHeight: conf.height),
|
|
child: Row(
|
|
children: [
|
|
getTextChild(),
|
|
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: (opt.enabled ?? true.obs).isTrue
|
|
? conf.commonColor
|
|
: Colors.grey,
|
|
))
|
|
: const SizedBox.shrink()),
|
|
))),
|
|
],
|
|
),
|
|
);
|
|
onPressed() {
|
|
if (opt.dismissOnClicked && Navigator.canPop(context)) {
|
|
Navigator.pop(context);
|
|
if (opt.dismissCallback != null) {
|
|
opt.dismissCallback!();
|
|
}
|
|
}
|
|
setOption(opt.value);
|
|
}
|
|
|
|
return mod_menu.PopupMenuItem(
|
|
padding: EdgeInsets.zero,
|
|
height: conf.height,
|
|
child: Container(
|
|
width: conf.boxWidth,
|
|
child: opt.enabled == null
|
|
? TextButton(
|
|
child: child,
|
|
onPressed: onPressed,
|
|
)
|
|
: Obx(() => TextButton(
|
|
child: child,
|
|
onPressed: opt.enabled!.isTrue ? onPressed : null,
|
|
)),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
List<mod_menu.PopupMenuEntry<T>> build(
|
|
BuildContext context, MenuConfig conf) {
|
|
return options.map((opt) => _buildMenuItem(context, conf, opt)).toList();
|
|
}
|
|
}
|
|
|
|
class MenuEntrySubRadios<T> extends MenuEntryBase<T> {
|
|
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<MenuEntryRadioOption> 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<T> _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);
|
|
if (opt.dismissCallback != null) {
|
|
opt.dismissCallback!();
|
|
}
|
|
}
|
|
setOption(opt.value);
|
|
},
|
|
)),
|
|
);
|
|
}
|
|
|
|
@override
|
|
List<mod_menu.PopupMenuEntry<T>> 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<bool> Function();
|
|
typedef SwitchSetter = Future<void> Function(bool);
|
|
|
|
abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
|
final SwitchType switchType;
|
|
final String text;
|
|
final EdgeInsets? padding;
|
|
Rx<TextStyle>? textStyle;
|
|
|
|
MenuEntrySwitchBase({
|
|
required this.switchType,
|
|
required this.text,
|
|
required dismissOnClicked,
|
|
this.textStyle,
|
|
this.padding,
|
|
RxBool? enabled,
|
|
dismissCallback,
|
|
}) : super(
|
|
dismissOnClicked: dismissOnClicked,
|
|
enabled: enabled,
|
|
dismissCallback: dismissCallback,
|
|
);
|
|
|
|
RxBool get curOption;
|
|
Future<void> setOption(bool? option);
|
|
|
|
@override
|
|
List<mod_menu.PopupMenuEntry<T>> 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);
|
|
if (super.dismissCallback != null) {
|
|
super.dismissCallback!();
|
|
}
|
|
}
|
|
setOption(v);
|
|
},
|
|
);
|
|
} else {
|
|
return Checkbox(
|
|
value: curOption.value,
|
|
onChanged: (v) {
|
|
if (super.dismissOnClicked &&
|
|
Navigator.canPop(context)) {
|
|
Navigator.pop(context);
|
|
if (super.dismissCallback != null) {
|
|
super.dismissCallback!();
|
|
}
|
|
}
|
|
setOption(v);
|
|
},
|
|
);
|
|
}
|
|
})),
|
|
))
|
|
])),
|
|
onPressed: () {
|
|
if (super.dismissOnClicked && Navigator.canPop(context)) {
|
|
Navigator.pop(context);
|
|
if (super.dismissCallback != null) {
|
|
super.dismissCallback!();
|
|
}
|
|
}
|
|
setOption(!curOption.value);
|
|
},
|
|
)),
|
|
)
|
|
];
|
|
}
|
|
}
|
|
|
|
class MenuEntrySwitch<T> extends MenuEntrySwitchBase<T> {
|
|
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>? textStyle,
|
|
EdgeInsets? padding,
|
|
dismissOnClicked = false,
|
|
RxBool? enabled,
|
|
dismissCallback,
|
|
}) : super(
|
|
switchType: switchType,
|
|
text: text,
|
|
textStyle: textStyle,
|
|
padding: padding,
|
|
dismissOnClicked: dismissOnClicked,
|
|
enabled: enabled,
|
|
dismissCallback: dismissCallback,
|
|
) {
|
|
() 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<void> Function(bool);
|
|
|
|
class MenuEntrySwitch2<T> extends MenuEntrySwitchBase<T> {
|
|
final Switch2Getter getter;
|
|
final SwitchSetter setter;
|
|
|
|
MenuEntrySwitch2({
|
|
required SwitchType switchType,
|
|
required String text,
|
|
required this.getter,
|
|
required this.setter,
|
|
Rx<TextStyle>? textStyle,
|
|
EdgeInsets? padding,
|
|
dismissOnClicked = false,
|
|
RxBool? enabled,
|
|
dismissCallback,
|
|
}) : super(
|
|
switchType: switchType,
|
|
text: text,
|
|
textStyle: textStyle,
|
|
padding: padding,
|
|
dismissOnClicked: dismissOnClicked,
|
|
dismissCallback: dismissCallback,
|
|
);
|
|
|
|
@override
|
|
RxBool get curOption => getter();
|
|
@override
|
|
setOption(bool? option) async {
|
|
if (option != null) {
|
|
await setter(option);
|
|
}
|
|
}
|
|
}
|
|
|
|
class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
|
|
final String text;
|
|
final List<MenuEntryBase<T>> entries;
|
|
final EdgeInsets? padding;
|
|
|
|
MenuEntrySubMenu({
|
|
required this.text,
|
|
required this.entries,
|
|
this.padding,
|
|
RxBool? enabled,
|
|
}) : super(enabled: enabled);
|
|
|
|
@override
|
|
List<mod_menu.PopupMenuEntry<T>> 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: super.enabled!.value
|
|
? enabledStyle(context)
|
|
: disabledStyle(),
|
|
)),
|
|
Expanded(
|
|
child: Align(
|
|
alignment: Alignment.centerRight,
|
|
child: Obx(() => Icon(
|
|
Icons.keyboard_arrow_right,
|
|
color: super.enabled!.value ? conf.commonColor : Colors.grey,
|
|
)),
|
|
))
|
|
]),
|
|
)
|
|
];
|
|
}
|
|
}
|
|
|
|
class MenuEntryButton<T> extends MenuEntryBase<T> {
|
|
final Widget Function(TextStyle? style) childBuilder;
|
|
Function() proc;
|
|
final EdgeInsets? padding;
|
|
|
|
MenuEntryButton({
|
|
required this.childBuilder,
|
|
required this.proc,
|
|
this.padding,
|
|
dismissOnClicked = false,
|
|
RxBool? enabled,
|
|
dismissCallback,
|
|
}) : super(
|
|
dismissOnClicked: dismissOnClicked,
|
|
enabled: enabled,
|
|
dismissCallback: dismissCallback,
|
|
);
|
|
|
|
Widget _buildChild(BuildContext context, MenuConfig conf) {
|
|
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);
|
|
if (super.dismissCallback != null) {
|
|
super.dismissCallback!();
|
|
}
|
|
}
|
|
proc();
|
|
}
|
|
: null,
|
|
child: Container(
|
|
padding: padding,
|
|
alignment: AlignmentDirectional.centerStart,
|
|
constraints:
|
|
BoxConstraints(minHeight: conf.height, maxHeight: conf.height),
|
|
child: childBuilder(
|
|
super.enabled!.value ? enabledStyle(context) : disabledStyle()),
|
|
),
|
|
)));
|
|
}
|
|
|
|
@override
|
|
List<mod_menu.PopupMenuEntry<T>> 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;
|
|
}
|