Unify shortcuts on desktop/web, remove arrow-press for focus traversal from desktop.

Add focusNode support for btns
This commit is contained in:
Shawn 2023-11-29 15:59:25 -07:00
parent 04be1daf42
commit 1c853b1c39
4 changed files with 90 additions and 13 deletions

View File

@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart';
@ -9,6 +10,7 @@ import 'package:wonders/logic/artifact_api_service.dart';
import 'package:wonders/logic/timeline_logic.dart'; import 'package:wonders/logic/timeline_logic.dart';
import 'package:wonders/logic/unsplash_logic.dart'; import 'package:wonders/logic/unsplash_logic.dart';
import 'package:wonders/logic/wonders_logic.dart'; import 'package:wonders/logic/wonders_logic.dart';
import 'package:wonders/ui/common/app_shortcuts.dart';
void main() async { void main() async {
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
@ -36,6 +38,7 @@ class WondersApp extends StatelessWidget with GetItMixin {
locale: locale == null ? null : Locale(locale), locale: locale == null ? null : Locale(locale),
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
routerDelegate: appRouter.routerDelegate, routerDelegate: appRouter.routerDelegate,
shortcuts: AppShortcuts.defaults,
theme: ThemeData(fontFamily: $styles.text.body.fontFamily, useMaterial3: true), theme: ThemeData(fontFamily: $styles.text.body.fontFamily, useMaterial3: true),
localizationsDelegates: const [ localizationsDelegates: const [
AppLocalizations.delegate, AppLocalizations.delegate,

View File

@ -0,0 +1,47 @@
import 'package:flutter/foundation.dart';
import 'package:wonders/common_libs.dart';
class AppShortcuts {
static final Map<ShortcutActivator, Intent> _defaultWebAndDesktopShortcuts = <ShortcutActivator, Intent>{
// Activation
if (kIsWeb) ...{
// On the web, enter activates buttons, but not other controls.
SingleActivator(LogicalKeyboardKey.enter): ButtonActivateIntent(),
SingleActivator(LogicalKeyboardKey.numpadEnter): ButtonActivateIntent(),
} else ...{
SingleActivator(LogicalKeyboardKey.enter): ActivateIntent(),
SingleActivator(LogicalKeyboardKey.numpadEnter): ActivateIntent(),
SingleActivator(LogicalKeyboardKey.space): ActivateIntent(),
SingleActivator(LogicalKeyboardKey.gameButtonA): ActivateIntent(),
},
// Dismissal
SingleActivator(LogicalKeyboardKey.escape): DismissIntent(),
// Keyboard traversal.
SingleActivator(LogicalKeyboardKey.tab): NextFocusIntent(),
SingleActivator(LogicalKeyboardKey.tab, shift: true): PreviousFocusIntent(),
// Scrolling
SingleActivator(LogicalKeyboardKey.arrowUp): ScrollIntent(direction: AxisDirection.up),
SingleActivator(LogicalKeyboardKey.arrowDown): ScrollIntent(direction: AxisDirection.down),
SingleActivator(LogicalKeyboardKey.arrowLeft): ScrollIntent(direction: AxisDirection.left),
SingleActivator(LogicalKeyboardKey.arrowRight): ScrollIntent(direction: AxisDirection.right),
SingleActivator(LogicalKeyboardKey.pageUp):
ScrollIntent(direction: AxisDirection.up, type: ScrollIncrementType.page),
SingleActivator(LogicalKeyboardKey.pageDown):
ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page),
};
static Map<ShortcutActivator, Intent>? get defaults {
switch (defaultTargetPlatform) {
// fall back to default shortcuts for ios and android
case TargetPlatform.iOS:
case TargetPlatform.android:
return null;
// unify shortcuts for desktop/web
default:
return _defaultWebAndDesktopShortcuts;
}
}
}

View File

@ -22,6 +22,8 @@ class AppBtn extends StatelessWidget {
this.minimumSize, this.minimumSize,
this.bgColor, this.bgColor,
this.border, this.border,
this.focusNode,
this.onFocusChanged,
}) : _builder = null, }) : _builder = null,
super(key: key); super(key: key);
@ -36,6 +38,8 @@ class AppBtn extends StatelessWidget {
this.minimumSize, this.minimumSize,
this.bgColor, this.bgColor,
this.border, this.border,
this.focusNode,
this.onFocusChanged,
String? semanticLabel, String? semanticLabel,
String? text, String? text,
AppIcons? icon, AppIcons? icon,
@ -76,6 +80,8 @@ class AppBtn extends StatelessWidget {
this.isSecondary = false, this.isSecondary = false,
this.circular = false, this.circular = false,
this.minimumSize, this.minimumSize,
this.focusNode,
this.onFocusChanged,
}) : expand = false, }) : expand = false,
bgColor = Colors.transparent, bgColor = Colors.transparent,
border = null, border = null,
@ -86,6 +92,8 @@ class AppBtn extends StatelessWidget {
final VoidCallback? onPressed; final VoidCallback? onPressed;
late final String semanticLabel; late final String semanticLabel;
final bool enableFeedback; final bool enableFeedback;
final FocusNode? focusNode;
final void Function(bool hasFocus)? onFocusChanged;
// content: // content:
late final Widget? child; late final Widget? child;
@ -129,15 +137,20 @@ class AppBtn extends StatelessWidget {
); );
Widget button = _CustomFocusBuilder( Widget button = _CustomFocusBuilder(
focusNode: focusNode,
onFocusChanged: onFocusChanged,
builder: (context, focus) => Stack( builder: (context, focus) => Stack(
children: [ children: [
TextButton( Opacity(
onPressed: onPressed, opacity: onPressed == null ? 0.5 : 1.0,
style: style, child: TextButton(
focusNode: focus, onPressed: onPressed,
child: DefaultTextStyle( style: style,
style: DefaultTextStyle.of(context).style.copyWith(color: textColor), focusNode: focus,
child: content, child: DefaultTextStyle(
style: DefaultTextStyle.of(context).style.copyWith(color: textColor),
child: content,
),
), ),
), ),
if (focus.hasFocus) if (focus.hasFocus)
@ -154,7 +167,7 @@ class AppBtn extends StatelessWidget {
); );
// add press effect: // add press effect:
if (pressEffect) button = _ButtonPressEffect(button); if (pressEffect && onPressed != null) button = _ButtonPressEffect(button);
// add semantics? // add semantics?
if (semanticLabel.isEmpty) return button; if (semanticLabel.isEmpty) return button;
@ -199,15 +212,28 @@ class _ButtonPressEffectState extends State<_ButtonPressEffect> {
} }
class _CustomFocusBuilder extends StatefulWidget { class _CustomFocusBuilder extends StatefulWidget {
const _CustomFocusBuilder({Key? key, required this.builder}) : super(key: key); const _CustomFocusBuilder({Key? key, required this.builder, this.focusNode, this.onFocusChanged}) : super(key: key);
final Widget Function(BuildContext context, FocusNode focus) builder; final Widget Function(BuildContext context, FocusNode focus) builder;
final void Function(bool hasFocus)? onFocusChanged;
final FocusNode? focusNode;
@override @override
State<_CustomFocusBuilder> createState() => _CustomFocusBuilderState(); State<_CustomFocusBuilder> createState() => _CustomFocusBuilderState();
} }
class _CustomFocusBuilderState extends State<_CustomFocusBuilder> { class _CustomFocusBuilderState extends State<_CustomFocusBuilder> {
late final _focusNode = FocusNode()..addListener(() => setState(() {})); late final FocusNode _focusNode;
@override
void initState() {
_focusNode = widget.focusNode ?? FocusNode();
_focusNode.addListener(_handleFocusChanged);
super.initState();
}
void _handleFocusChanged() {
widget.onFocusChanged?.call(_focusNode.hasFocus);
setState(() {});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -14,7 +14,7 @@ class CircleBtn extends StatelessWidget {
static double defaultSize = 48; static double defaultSize = 48;
final VoidCallback onPressed; final VoidCallback? onPressed;
final Color? bgColor; final Color? bgColor;
final BorderSide? border; final BorderSide? border;
final Widget child; final Widget child;
@ -54,7 +54,7 @@ class CircleIconBtn extends StatelessWidget {
static double defaultSize = 28; static double defaultSize = 28;
final AppIcons icon; final AppIcons icon;
final VoidCallback onPressed; final VoidCallback? onPressed;
final BorderSide? border; final BorderSide? border;
final Color? bgColor; final Color? bgColor;
final Color? color; final Color? color;
@ -103,6 +103,7 @@ class BackBtn extends StatelessWidget {
semanticLabel: $strings.circleButtonsSemanticClose, semanticLabel: $strings.circleButtonsSemanticClose,
bgColor: bgColor, bgColor: bgColor,
iconColor: iconColor); iconColor: iconColor);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CircleIconBtn( return CircleIconBtn(