rustdesk/flutter/lib/desktop/widgets/popup_menu.dart

576 lines
16 KiB
Dart
Raw Normal View History

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<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),
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<T> {
bool dismissOnClicked;
RxBool? enabled;
MenuEntryBase({
this.dismissOnClicked = false,
this.enabled,
});
List<mod_menu.PopupMenuEntry<T>> build(BuildContext context, MenuConfig conf);
}
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;
MenuEntryRadioOption({
required this.text,
required this.value,
this.dismissOnClicked = false,
this.enabled,
});
}
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;
MenuEntryRadios({
required this.text,
required this.optionsGetter,
required this.curOptionGetter,
required this.optionSetter,
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> _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<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;
MenuEntrySubRadios({
required this.text,
required this.optionsGetter,
required this.curOptionGetter,
required this.optionSetter,
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: 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<mod_menu.PopupMenuEntry<T>> build(
BuildContext context, MenuConfig conf) {
return [
PopupMenuChildrenItem(
enabled: super.enabled,
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<bool> Function();
typedef SwitchSetter = Future<void> Function(bool);
abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
final String text;
Rx<TextStyle>? textStyle;
MenuEntrySwitchBase({
required this.text,
required dismissOnClicked,
this.textStyle,
RxBool? enabled,
}) : super(dismissOnClicked: dismissOnClicked, enabled: enabled);
RxBool get curOption;
Future<void> setOption(bool option);
@override
List<mod_menu.PopupMenuEntry<T>> build(
BuildContext context, MenuConfig conf) {
textStyle ??= const TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal)
.obs;
return [
mod_menu.PopupMenuItem(
padding: EdgeInsets.zero,
height: conf.height,
child: TextButton(
child: Container(
alignment: AlignmentDirectional.centerStart,
height: conf.height,
child: Row(children: [
Obx(() => Text(
text,
style: textStyle!.value,
)),
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<T> extends MenuEntrySwitchBase<T> {
final SwitchGetter getter;
final SwitchSetter setter;
final RxBool _curOption = false.obs;
MenuEntrySwitch({
required String text,
required this.getter,
required this.setter,
Rx<TextStyle>? textStyle,
dismissOnClicked = false,
RxBool? enabled,
}) : super(
text: text,
textStyle: textStyle,
dismissOnClicked: dismissOnClicked,
enabled: enabled,
) {
() 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<void> Function(bool);
class MenuEntrySwitch2<T> extends MenuEntrySwitchBase<T> {
final Switch2Getter getter;
final SwitchSetter setter;
MenuEntrySwitch2({
required String text,
required this.getter,
required this.setter,
Rx<TextStyle>? textStyle,
dismissOnClicked = false,
RxBool? enabled,
}) : super(
text: text,
textStyle: textStyle,
dismissOnClicked: dismissOnClicked);
@override
RxBool get curOption => getter();
@override
setOption(bool option) async {
await setter(option);
}
}
class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
final String text;
final List<MenuEntryBase<T>> entries;
MenuEntrySubMenu({
required this.text,
required this.entries,
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: 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),
Obx(() => Text(
text,
style: TextStyle(
color: super.enabled!.value ? Colors.black : 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<T> extends MenuEntryBase<T> {
final Widget Function(TextStyle? style) childBuilder;
Function() proc;
MenuEntryButton({
required this.childBuilder,
required this.proc,
dismissOnClicked = false,
RxBool? enabled,
}) : super(
dismissOnClicked: dismissOnClicked,
enabled: enabled,
);
Widget _buildChild(BuildContext context, MenuConfig conf) {
const enabledStyle = TextStyle(
color: Colors.black,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal);
const disabledStyle = TextStyle(
color: Colors.grey,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal);
super.enabled ??= true.obs;
return Obx(() => TextButton(
onPressed: super.enabled!.value
? () {
if (super.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
}
proc();
}
: null,
child: Container(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(minHeight: conf.height),
child: childBuilder(
super.enabled!.value ? enabledStyle : 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),
)
];
}
}