Unify shortcuts on desktop/web, remove arrow-press for focus traversal from desktop.
Add focusNode support for btns
This commit is contained in:
parent
04be1daf42
commit
1c853b1c39
@ -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,
|
||||||
|
47
lib/ui/common/app_shortcuts.dart
Normal file
47
lib/ui/common/app_shortcuts.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,9 +137,13 @@ 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(
|
||||||
|
opacity: onPressed == null ? 0.5 : 1.0,
|
||||||
|
child: TextButton(
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
style: style,
|
style: style,
|
||||||
focusNode: focus,
|
focusNode: focus,
|
||||||
@ -140,6 +152,7 @@ class AppBtn extends StatelessWidget {
|
|||||||
child: content,
|
child: content,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
if (focus.hasFocus)
|
if (focus.hasFocus)
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
@ -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) {
|
||||||
|
@ -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(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user