mirror of
https://github.com/DungeonPaper/dungeon-paper-app.git
synced 2026-05-17 17:58:11 +00:00
240 lines
7.2 KiB
Dart
240 lines
7.2 KiB
Dart
import 'package:dungeon_paper/app/widgets/atoms/popup_menu_item_list_tile.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
class MenuButton<V> extends StatelessWidget {
|
|
const MenuButton({
|
|
super.key,
|
|
required Iterable<PopupMenuEntry<V>> items,
|
|
this.initialValue,
|
|
this.onCanceled,
|
|
this.tooltip,
|
|
this.elevation,
|
|
this.padding = const EdgeInsets.all(8.0),
|
|
this.child,
|
|
this.splashRadius,
|
|
this.icon,
|
|
this.iconSize,
|
|
this.offset = Offset.zero,
|
|
this.enabled = true,
|
|
this.shape,
|
|
this.color,
|
|
this.enableFeedback,
|
|
this.constraints,
|
|
this.position = PopupMenuPosition.over,
|
|
this.onSelected,
|
|
}) : _items = items,
|
|
builder = null;
|
|
|
|
const MenuButton.builder({
|
|
super.key,
|
|
required this.builder,
|
|
this.initialValue,
|
|
this.onCanceled,
|
|
this.tooltip,
|
|
this.elevation,
|
|
this.padding = const EdgeInsets.all(8.0),
|
|
this.child,
|
|
this.splashRadius,
|
|
this.icon,
|
|
this.iconSize,
|
|
this.offset = Offset.zero,
|
|
this.enabled = true,
|
|
this.shape,
|
|
this.color,
|
|
this.enableFeedback,
|
|
this.constraints,
|
|
this.position = PopupMenuPosition.over,
|
|
this.onSelected,
|
|
}) : _items = null,
|
|
assert(builder != null);
|
|
|
|
final Iterable<PopupMenuEntry<V>>? _items;
|
|
final Iterable<PopupMenuEntry<V>> Function()? builder;
|
|
final void Function(V value)? onSelected;
|
|
|
|
/// The value of the menu item, if any, that should be highlighted when the menu opens.
|
|
final V? initialValue;
|
|
|
|
/// Called when the user dismisses the popup menu without selecting an item.
|
|
///
|
|
/// If the user selects a value, [onSelected] is called instead.
|
|
final PopupMenuCanceled? onCanceled;
|
|
|
|
/// Text that describes the action that will occur when the button is pressed.
|
|
///
|
|
/// This text is displayed when the user long-presses on the button and is
|
|
/// used for accessibility.
|
|
final String? tooltip;
|
|
|
|
/// The z-coordinate at which to place the menu when open. This controls the
|
|
/// size of the shadow below the menu.
|
|
///
|
|
/// Defaults to 8, the appropriate elevation for popup menus.
|
|
final double? elevation;
|
|
|
|
/// Matches IconButton's 8 dps padding by default. In some cases, notably where
|
|
/// this button appears as the trailing element of a list item, it's useful to be able
|
|
/// to set the padding to zero.
|
|
final EdgeInsetsGeometry padding;
|
|
|
|
/// The splash radius.
|
|
///
|
|
/// If null, default splash radius of [InkWell] or [IconButton] is used.
|
|
final double? splashRadius;
|
|
|
|
/// If provided, [child] is the widget used for this button
|
|
/// and the button will utilize an [InkWell] for taps.
|
|
final Widget? child;
|
|
|
|
/// If provided, the [icon] is used for this button
|
|
/// and the button will behave like an [IconButton].
|
|
final Widget? icon;
|
|
|
|
/// The offset is applied relative to the initial position
|
|
/// set by the [position].
|
|
///
|
|
/// When not set, the offset defaults to [Offset.zero].
|
|
final Offset offset;
|
|
|
|
/// Whether this popup menu button is interactive.
|
|
///
|
|
/// Must be non-null, defaults to `true`
|
|
///
|
|
/// If `true` the button will respond to presses by displaying the menu.
|
|
///
|
|
/// If `false`, the button is styled with the disabled color from the
|
|
/// current [Theme] and will not respond to presses or show the popup
|
|
/// menu and [onSelected], [onCanceled] and [itemBuilder] will not be called.
|
|
///
|
|
/// This can be useful in situations where the app needs to show the button,
|
|
/// but doesn't currently have anything to show in the menu.
|
|
final bool enabled;
|
|
|
|
/// If provided, the shape used for the menu.
|
|
///
|
|
/// If this property is null, then [PopupMenuThemeData.shape] is used.
|
|
/// If [PopupMenuThemeData.shape] is also null, then the default shape for
|
|
/// [MaterialType.card] is used. This default shape is a rectangle with
|
|
/// rounded edges of BorderRadius.circular(2.0).
|
|
final ShapeBorder? shape;
|
|
|
|
/// If provided, the background color used for the menu.
|
|
///
|
|
/// If this property is null, then [PopupMenuThemeData.color] is used.
|
|
/// If [PopupMenuThemeData.color] is also null, then
|
|
/// Theme.of(context).cardColor is used.
|
|
final Color? color;
|
|
|
|
/// Whether detected gestures should provide acoustic and/or haptic feedback.
|
|
///
|
|
/// For example, on Android a tap will produce a clicking sound and a
|
|
/// long-press will produce a short vibration, when feedback is enabled.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [Feedback] for providing platform-specific feedback to certain actions.
|
|
final bool? enableFeedback;
|
|
|
|
/// If provided, the size of the [Icon].
|
|
///
|
|
/// If this property is null, then [IconThemeData.size] is used.
|
|
/// If [IconThemeData.size] is also null, then
|
|
/// default size is 24.0 pixels.
|
|
final double? iconSize;
|
|
|
|
/// Optional size constraints for the menu.
|
|
///
|
|
/// When unspecified, defaults to:
|
|
/// ```dart
|
|
/// const BoxConstraints(
|
|
/// minWidth: 2.0 * 56.0,
|
|
/// maxWidth: 5.0 * 56.0,
|
|
/// )
|
|
/// ```
|
|
///
|
|
/// The default constraints ensure that the menu width matches maximum width
|
|
/// recommended by the material design guidelines.
|
|
/// Specifying this parameter enables creation of menu wider than
|
|
/// the default maximum width.
|
|
final BoxConstraints? constraints;
|
|
|
|
/// Whether the popup menu is positioned over or under the popup menu button.
|
|
///
|
|
/// [offset] is used to change the position of the popup menu relative to the
|
|
/// position set by this parameter.
|
|
///
|
|
/// When not set, the position defaults to [PopupMenuPosition.over] which makes the
|
|
/// popup menu appear directly over the button that was used to create it.
|
|
final PopupMenuPosition position;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final items = (builder ?? () => _items!).call();
|
|
assert(items.isNotEmpty);
|
|
|
|
return PopupMenuButton<V>(
|
|
itemBuilder: (context) => items.map(
|
|
(e) {
|
|
if (e is MenuEntry) {
|
|
final MenuEntry entry = e as MenuEntry;
|
|
return PopupMenuItem<V>(
|
|
value: entry.value,
|
|
enabled: !entry.disabled,
|
|
child:
|
|
PopupMenuItemListTile(icon: entry.icon, label: entry.label),
|
|
);
|
|
} else {
|
|
return e;
|
|
}
|
|
},
|
|
).toList(),
|
|
onSelected: (key) {
|
|
(items.firstWhere((element) {
|
|
if (element is MenuEntry) {
|
|
return (element as MenuEntry).value == key;
|
|
}
|
|
return false;
|
|
}) as MenuEntry?)
|
|
?.onSelect
|
|
?.call();
|
|
onSelected?.call(key);
|
|
},
|
|
initialValue: initialValue,
|
|
onCanceled: onCanceled,
|
|
tooltip: tooltip,
|
|
elevation: elevation,
|
|
padding: padding,
|
|
splashRadius: splashRadius,
|
|
icon: icon,
|
|
iconSize: iconSize,
|
|
offset: offset,
|
|
enabled: enabled,
|
|
shape: shape,
|
|
color: color,
|
|
enableFeedback: enableFeedback,
|
|
constraints: constraints,
|
|
position: position,
|
|
child: child,
|
|
);
|
|
}
|
|
}
|
|
|
|
class MenuEntry<V> extends PopupMenuItem<V> {
|
|
final Widget label;
|
|
final Widget? icon;
|
|
|
|
final void Function()? onSelect;
|
|
final bool disabled;
|
|
|
|
MenuEntry({
|
|
super.key,
|
|
required this.label,
|
|
this.icon,
|
|
required super.value,
|
|
required this.onSelect,
|
|
this.disabled = false,
|
|
}) : super(
|
|
child: PopupMenuItemListTile(icon: icon, label: label),
|
|
);
|
|
}
|