import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/basic/popup/menu_row.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/map/buttons/button.dart'; import 'package:aves/widgets/common/map/buttons/coordinate_filter.dart'; import 'package:aves/widgets/common/map/compass.dart'; import 'package:aves/widgets/common/map/map_action_delegate.dart'; import 'package:aves_map/aves_map.dart'; import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; class MapButtonPanel extends StatefulWidget { final AvesMapController controller; final ValueNotifier boundsNotifier; final void Function(BuildContext context)? openMapPage; const MapButtonPanel({ super.key, required this.controller, required this.boundsNotifier, this.openMapPage, }); @override State createState() => _MapButtonPanelState(); } class _MapButtonPanelState extends State { late MapActionDelegate _actionDelegate; @override void initState() { super.initState(); _updateDelegate(); } @override void didUpdateWidget(covariant MapButtonPanel oldWidget) { super.didUpdateWidget(oldWidget); _updateDelegate(); } void _updateDelegate() => _actionDelegate = MapActionDelegate(widget.controller); @override Widget build(BuildContext context) { final iconTheme = IconTheme.of(context); final iconSize = Size.square(iconTheme.size!); Widget? navigationButton; switch (context.select((v) => v.navigationButton)) { case MapNavigationButton.back: if (!settings.useTvLayout) { navigationButton = MapOverlayButton.icon( icon: const BackButtonIcon(), onPressed: () => Navigator.maybeOf(context)?.pop(), tooltip: MaterialLocalizations.of(context).backButtonTooltip, ); } case MapNavigationButton.close: navigationButton = MapOverlayButton.icon( icon: const CloseButtonIcon(), onPressed: SystemNavigator.pop, tooltip: MaterialLocalizations.of(context).closeButtonTooltip, ); case MapNavigationButton.map: final _openMapPage = widget.openMapPage; if (_openMapPage != null) { navigationButton = MapOverlayButton.icon( icon: const Icon(AIcons.showFullscreenCorners), onPressed: () => _openMapPage.call(context), tooltip: context.l10n.openMapPageTooltip, ); } case MapNavigationButton.none: break; } final showCoordinateFilter = context.select((v) => v.showCoordinateFilter); final visualDensity = context.select((v) => v.visualDensity); final double padding = 8 + visualDensity.horizontal * 2; final actions = [ MapAction.openMapApp, MapAction.addShortcut, ].where((action) => _actionDelegate.isVisible(context, action)).toList(); return Positioned.fill( child: TooltipTheme( data: TooltipTheme.of(context).copyWith( preferBelow: false, ), child: SafeArea( bottom: false, child: Stack( children: [ Positioned( left: padding, right: padding, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.only(top: padding), child: Column( mainAxisSize: MainAxisSize.min, children: [ if (navigationButton != null) ...[ navigationButton, SizedBox(height: padding), ], ValueListenableBuilder( valueListenable: widget.boundsNotifier, builder: (context, bounds, child) { final degrees = bounds.rotation; final opacity = degrees == 0 ? .0 : 1.0; return IgnorePointer( ignoring: opacity == 0, child: AnimatedOpacity( opacity: opacity, duration: context.select((v) => v.viewerOverlayAnimation), child: MapOverlayButton.icon( icon: Transform( origin: iconSize.center(Offset.zero), transform: Matrix4.rotationZ(degToRadian(degrees)), child: CustomPaint( painter: CompassPainter( color: iconTheme.color!, ), size: iconSize, ), ), onPressed: widget.controller.resetRotation, tooltip: context.l10n.mapPointNorthUpTooltip, ), ), ); }, ), ], ), ), showCoordinateFilter ? Expanded( child: OverlayCoordinateFilterChip( boundsNotifier: widget.boundsNotifier, padding: padding, ), ) : const Spacer(), Padding( padding: EdgeInsets.only(top: padding), // key is expected by test driver child: Column( children: [ if (actions.length == 1) _buildActionButton(context, actions.first), if (actions.length > 1) MapOverlayButton(builder: (context, visualDensity, child) { final animations = context.read().accessibilityAnimations; return PopupMenuButton( itemBuilder: (context) => actions .map((action) => PopupMenuItem( value: action, child: MenuRow( text: action.getText(context), icon: action.getIcon(), ), )) .toList(), onSelected: (action) async { // wait for the popup menu to hide before proceeding with the action await Future.delayed(animations.popUpAnimationDelay * timeDilation); _actionDelegate.onActionSelected(context, action); }, iconSize: MapOverlayButton.iconSize(visualDensity), popUpAnimationStyle: animations.popUpAnimationStyle, ); }), SizedBox(height: padding), // key is expected by test driver _buildActionButton(context, MapAction.selectStyle, buttonKey: const Key('map-menu-layers')), ], ), ), ], ), ), Positioned( right: padding, bottom: padding, child: Column( mainAxisSize: MainAxisSize.min, children: [ _buildActionButton(context, MapAction.zoomIn), SizedBox(height: padding), _buildActionButton(context, MapAction.zoomOut), ], ), ), ], ), ), ), ); } Widget _buildActionButton(BuildContext context, MapAction action, {Key? buttonKey}) => MapOverlayButton.icon( buttonKey: buttonKey, icon: action.getIcon(), onPressed: () => _actionDelegate.onActionSelected(context, action), tooltip: action.getText(context), ); }