diff --git a/.github/cspell.json b/.github/cspell.json index 24a1831..e24f842 100644 --- a/.github/cspell.json +++ b/.github/cspell.json @@ -15,6 +15,6 @@ } ], "useGitignore": true, - "words": ["flutter_deck", "Mangirdas", "Kazlauskas"], - "ignorePaths": ["doc/website/source/showcase.md"] + "words": ["flutter_deck", "Mangirdas", "Kazlauskas", "subclassing"], + "ignorePaths": ["**/doc/website/source/showcase.md"] } diff --git a/README.md b/README.md index b91c7fc..4fad97d 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ FlutterDeckApp( ), controls: const FlutterDeckControlsConfiguration( presenterToolbarVisible: true, + gestures: FlutterDeckGesturesConfiguration.mobileOnly(), shortcuts: FlutterDeckShortcutsConfiguration( enabled: true, nextSlide: SingleActivator(LogicalKeyboardKey.arrowRight), diff --git a/doc/website/source/get-started.md b/doc/website/source/get-started.md index 385cdf8..0026137 100644 --- a/doc/website/source/get-started.md +++ b/doc/website/source/get-started.md @@ -52,6 +52,7 @@ FlutterDeckApp( ), controls: const FlutterDeckControlsConfiguration( presenterToolbarVisible: true, + gestures: FlutterDeckGesturesConfiguration.mobileOnly(), shortcuts: FlutterDeckShortcutsConfiguration( enabled: true, nextSlide: SingleActivator(LogicalKeyboardKey.arrowRight), diff --git a/doc/website/source/playback/controls.md b/doc/website/source/playback/controls.md index d4a2239..7adb755 100644 --- a/doc/website/source/playback/controls.md +++ b/doc/website/source/playback/controls.md @@ -2,7 +2,8 @@ title: Controls navOrder: 1 --- -By default, every slide deck comes with a presenter toolbar that can be used to control the slide deck. Also, some of the controls can be accessed by using keyboard shortcuts. + +By default, every slide deck comes with a presenter toolbar that can be used to control the slide deck. Also, some of the controls can be accessed by using keyboard shortcuts or touch gestures. To disable all the controls (e.g. you use your own UI to control the slide deck), set the `controls` property for the slide deck configuration to `FlutterDeckControlsConfiguration.disabled()`: @@ -24,12 +25,23 @@ FlutterDeckConfiguration( ) ``` -To disable the keyboard shortcuts, set the `shortcuts` property to `FlutterDeckShortcutsConfiguration(enabled: false)`: +To disable the keyboard shortcuts, set the `shortcuts` property to `FlutterDeckShortcutsConfiguration.disabled()`: + +```dart +FlutterDeckConfiguration( + controls: const FlutterDeckControlsConfiguration( + shortcuts: FlutterDeckShortcutsConfiguration.disabled(), + ), + <...> +) +``` + +To disable the touch gestures, set the `gestures` property to `FlutterDeckGesturesConfiguration.disabled()`: ```dart FlutterDeckConfiguration( controls: const FlutterDeckControlsConfiguration( - shortcuts: FlutterDeckShortcutsConfiguration(enabled: false), + gestures: FlutterDeckGesturesConfiguration.disabled(), ), <...> ) diff --git a/packages/flutter_deck/CHANGELOG.md b/packages/flutter_deck/CHANGELOG.md index 4a9153e..725a69b 100644 --- a/packages/flutter_deck/CHANGELOG.md +++ b/packages/flutter_deck/CHANGELOG.md @@ -1,3 +1,11 @@ +# NEXT + +- fix: show controls on tap on mobile devices (iOS and Android) +- fix: controls are auto-hidden when the cursor is over them +- feat: add swipe left/right gestures to navigate between slides +- feat: add control gestures configuration +- docs: update documentation website + # 0.17.0 - feat: allow using any widget as a slide diff --git a/packages/flutter_deck/README.md b/packages/flutter_deck/README.md index c666ccf..ac1e46b 100644 --- a/packages/flutter_deck/README.md +++ b/packages/flutter_deck/README.md @@ -88,6 +88,7 @@ FlutterDeckApp( ), controls: const FlutterDeckControlsConfiguration( presenterToolbarVisible: true, + gestures: FlutterDeckGesturesConfiguration.mobileOnly(), shortcuts: FlutterDeckShortcutsConfiguration( enabled: true, nextSlide: SingleActivator(LogicalKeyboardKey.arrowRight), diff --git a/packages/flutter_deck/lib/src/configuration/flutter_deck_configuration.dart b/packages/flutter_deck/lib/src/configuration/flutter_deck_configuration.dart index a7d0b3e..5c48e1c 100644 --- a/packages/flutter_deck/lib/src/configuration/flutter_deck_configuration.dart +++ b/packages/flutter_deck/lib/src/configuration/flutter_deck_configuration.dart @@ -38,8 +38,9 @@ class FlutterDeckConfiguration { /// for the [FlutterDeckSlide]. final FlutterDeckBackgroundConfiguration background; - /// Configures the controls for the slide deck. By default, the presenter - /// toolbar is visible and the default keyboard controls are enabled. + /// Configures the controls for the slide deck. By default, the presenter + /// toolbar is visible, the default keyboard controls are enabled, and + /// gestures are enabled on mobile platforms only. /// /// The default keyboard shortcuts are: /// - Next slide: \[ArrowRight\] diff --git a/packages/flutter_deck/lib/src/controls/flutter_deck_controls.dart b/packages/flutter_deck/lib/src/controls/flutter_deck_controls.dart index fcaccc7..3ad9fc8 100644 --- a/packages/flutter_deck/lib/src/controls/flutter_deck_controls.dart +++ b/packages/flutter_deck/lib/src/controls/flutter_deck_controls.dart @@ -69,25 +69,31 @@ class _Controls extends StatelessWidget { @override Widget build(BuildContext context) { + final controlsNotifier = context.flutterDeck.controlsNotifier; + return Theme( data: ThemeData.light(), - child: Builder( - builder: (context) => Container( - margin: FlutterDeckLayout.slidePadding, - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(32), - color: Theme.of(context).colorScheme.surface, - ), - child: const Row( - mainAxisSize: MainAxisSize.min, - children: [ - _PreviousButton(), - _SlideNumberButton(), - _NextButton(), - _MarkerControls(), - _OptionsMenuButton(), - ], + child: MouseRegion( + onEnter: (_) => controlsNotifier.toggleControlsVisibleDuration(), + onExit: (_) => controlsNotifier.toggleControlsVisibleDuration(), + child: Builder( + builder: (context) => Container( + margin: FlutterDeckLayout.slidePadding, + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32), + color: Theme.of(context).colorScheme.surface, + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + _PreviousButton(), + _SlideNumberButton(), + _NextButton(), + _MarkerControls(), + _OptionsMenuButton(), + ], + ), ), ), ), diff --git a/packages/flutter_deck/lib/src/controls/flutter_deck_controls_configuration.dart b/packages/flutter_deck/lib/src/controls/flutter_deck_controls_configuration.dart index 432e2e4..b60dbfd 100644 --- a/packages/flutter_deck/lib/src/controls/flutter_deck_controls_configuration.dart +++ b/packages/flutter_deck/lib/src/controls/flutter_deck_controls_configuration.dart @@ -1,11 +1,12 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; /// The configuration for the slide deck controls. class FlutterDeckControlsConfiguration { /// Creates a configuration for the slide deck controls. By default, the - /// presenter toolbar is visible and the default keyboard controls are - /// enabled. + /// presenter toolbar is visible, the default keyboard controls are + /// enabled, and gestures are enabled on mobile platforms only. /// /// The default keyboard shortcuts are: /// - Next slide: \[ArrowRight\] @@ -19,22 +20,61 @@ class FlutterDeckControlsConfiguration { /// - [LogicalKeyboardKey] for a list of all available keys. const FlutterDeckControlsConfiguration({ this.presenterToolbarVisible = true, + this.gestures = const FlutterDeckGesturesConfiguration.mobileOnly(), this.shortcuts = const FlutterDeckShortcutsConfiguration(), }); /// Creates a configuration for the slide deck controls where they are /// disabled. const FlutterDeckControlsConfiguration.disabled() - : presenterToolbarVisible = false, - shortcuts = const FlutterDeckShortcutsConfiguration(enabled: false); + : this( + presenterToolbarVisible: false, + gestures: const FlutterDeckGesturesConfiguration.disabled(), + shortcuts: const FlutterDeckShortcutsConfiguration.disabled(), + ); /// Whether the presenter toolbar is visible or not. final bool presenterToolbarVisible; + /// The configuration for the slide deck controls gestures. + final FlutterDeckGesturesConfiguration gestures; + /// The configuration for the slide deck keyboard shortcuts. final FlutterDeckShortcutsConfiguration shortcuts; } +/// The configuration for the slide deck control gestures. +/// +/// The gesture controls are only available on [supportedPlatforms]. By default, +/// gestures are enabled on all platforms. +class FlutterDeckGesturesConfiguration { + /// Creates a configuration for the slide deck control gestures. + const FlutterDeckGesturesConfiguration({ + this.supportedPlatforms = const {...TargetPlatform.values}, + }); + + /// Creates a configuration for the slide deck control gestures where they are + /// disabled. + const FlutterDeckGesturesConfiguration.disabled() + : this(supportedPlatforms: const {}); + + /// Creates a configuration for the slide deck control gestures where they are + /// enabled on mobile platforms only. + const FlutterDeckGesturesConfiguration.mobileOnly() + : this( + supportedPlatforms: const { + TargetPlatform.android, + TargetPlatform.iOS, + }, + ); + + /// The platforms where gestures are enabled. + final Set supportedPlatforms; + + /// Whether gestures are enabled on the current platform or not. + bool get enabled => supportedPlatforms.contains(defaultTargetPlatform); +} + /// The configuration for the slide deck keyboard shortcuts. class FlutterDeckShortcutsConfiguration { /// Creates a configuration for the slide deck keyboard shortcuts. By default, @@ -59,6 +99,10 @@ class FlutterDeckShortcutsConfiguration { const SingleActivator(LogicalKeyboardKey.period), }); + /// Creates a configuration for the slide deck keyboard shortcuts where they + /// are disabled. + const FlutterDeckShortcutsConfiguration.disabled() : this(enabled: false); + /// Whether keyboard shortcuts are enabled or not. final bool enabled; diff --git a/packages/flutter_deck/lib/src/controls/flutter_deck_controls_listener.dart b/packages/flutter_deck/lib/src/controls/flutter_deck_controls_listener.dart index 6f764c6..dc3a00e 100644 --- a/packages/flutter_deck/lib/src/controls/flutter_deck_controls_listener.dart +++ b/packages/flutter_deck/lib/src/controls/flutter_deck_controls_listener.dart @@ -2,8 +2,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_deck/src/controls/actions/actions.dart'; import 'package:flutter_deck/src/controls/flutter_deck_controls_notifier.dart'; import 'package:flutter_deck/src/flutter_deck.dart'; +import 'package:flutter_deck/src/widgets/internal/internal.dart'; -/// A widget that handles controls (actions and shortcuts) for the slide deck. +/// A widget that handles controls (actions, gestures and shortcuts) for the +/// slide deck. /// /// Key bindings are defined in global deck configuration. The following /// shortcuts are supported: @@ -24,11 +26,15 @@ class FlutterDeckControlsListener extends StatelessWidget { /// [child] is the widget that will be wrapped by this widget. It should be /// the root of the slide deck. /// - /// [notifier] is the [FlutterDeckControlsNotifier] that will be used to - /// control the slide deck. + /// [controlsNotifier] is the [FlutterDeckControlsNotifier] that will be used + /// to control the slide deck. + /// + /// [markerNotifier] is the [FlutterDeckMarkerNotifier] that will be used to + /// control the slide deck's marker. const FlutterDeckControlsListener({ required this.child, - required this.notifier, + required this.controlsNotifier, + required this.markerNotifier, super.key, }); @@ -36,7 +42,32 @@ class FlutterDeckControlsListener extends StatelessWidget { final Widget child; /// The notifier used to control the slide deck. - final FlutterDeckControlsNotifier notifier; + final FlutterDeckControlsNotifier controlsNotifier; + + /// The notifier used to control the slide deck's marker. + final FlutterDeckMarkerNotifier markerNotifier; + + void _onHorizontalSwipe(DragEndDetails? details) { + final velocity = details?.primaryVelocity; + + if (velocity == null) return; + + velocity > 0 ? controlsNotifier.previous() : controlsNotifier.next(); + } + + void _onMouseHover(PointerEvent event) { + controlsNotifier.showControls(); + } + + void _onTap() { + final controlsVisible = controlsNotifier.controlsVisible; + + controlsNotifier.showControls(); + + if (!controlsVisible || markerNotifier.enabled) return; + + controlsNotifier.next(); + } @override Widget build(BuildContext context) { @@ -45,13 +76,29 @@ class FlutterDeckControlsListener extends StatelessWidget { Widget widget = Focus( autofocus: true, child: ListenableBuilder( - listenable: notifier, + listenable: controlsNotifier, builder: (context, child) => MouseRegion( - cursor: notifier.controlsVisible + cursor: controlsNotifier.controlsVisible ? MouseCursor.defer : SystemMouseCursors.none, - onHover: (_) => notifier.showControls(), - child: child, + onHover: _onMouseHover, + child: ListenableBuilder( + listenable: markerNotifier, + builder: (context, child) { + if (!controls.gestures.enabled) return child!; + + if (markerNotifier.enabled) { + return GestureDetector(onTap: _onTap, child: child); + } + + return GestureDetector( + onHorizontalDragEnd: _onHorizontalSwipe, + onTap: _onTap, + child: child, + ); + }, + child: child, + ), ), child: child, ), @@ -62,10 +109,10 @@ class FlutterDeckControlsListener extends StatelessWidget { if (controls.presenterToolbarVisible || shortcuts.enabled) { widget = Actions( actions: >{ - GoNextIntent: GoNextAction(notifier), - GoPreviousIntent: GoPreviousAction(notifier), - ToggleDrawerIntent: ToggleDrawerAction(notifier), - ToggleMarkerIntent: ToggleMarkerAction(notifier), + GoNextIntent: GoNextAction(controlsNotifier), + GoPreviousIntent: GoPreviousAction(controlsNotifier), + ToggleDrawerIntent: ToggleDrawerAction(controlsNotifier), + ToggleMarkerIntent: ToggleMarkerAction(controlsNotifier), }, child: widget, ); diff --git a/packages/flutter_deck/lib/src/controls/flutter_deck_controls_notifier.dart b/packages/flutter_deck/lib/src/controls/flutter_deck_controls_notifier.dart index fc123d1..46b2bf4 100644 --- a/packages/flutter_deck/lib/src/controls/flutter_deck_controls_notifier.dart +++ b/packages/flutter_deck/lib/src/controls/flutter_deck_controls_notifier.dart @@ -8,6 +8,8 @@ import 'package:flutter_deck/src/flutter_deck_router.dart'; import 'package:flutter_deck/src/widgets/internal/drawer/drawer.dart'; import 'package:flutter_deck/src/widgets/internal/marker/marker.dart'; +const _defaultControlsVisibleDuration = Duration(seconds: 3); + /// The [ChangeNotifier] used to control the slide deck and handle cursor and /// deck controls visibility. class FlutterDeckControlsNotifier @@ -33,6 +35,7 @@ class FlutterDeckControlsNotifier final FlutterDeckRouter _router; var _controlsVisible = false; + var _controlsVisibleDuration = _defaultControlsVisibleDuration; Timer? _controlsVisibleTimer; Set _disabledIntents = {}; @@ -116,7 +119,7 @@ class FlutterDeckControlsNotifier _setControlsVisible(true); _controlsVisibleTimer = Timer( - const Duration(seconds: 3), + _controlsVisibleDuration, () => _setControlsVisible(false), ); } @@ -130,6 +133,16 @@ class FlutterDeckControlsNotifier notifyListeners(); } + /// Toggle the cursor and controls visibility duration. + /// + /// The value toggles between 3 seconds and infinite (no auto-hide). + void toggleControlsVisibleDuration() { + _controlsVisibleDuration = + _controlsVisibleDuration > _defaultControlsVisibleDuration + ? _defaultControlsVisibleDuration + : const Duration(days: 1); // Infinite enough for this use case... + } + /// Whether the given [intent] is disabled. bool intentDisabled(Intent intent) => _disabledIntents.contains(intent); } diff --git a/packages/flutter_deck/lib/src/flutter_deck_app.dart b/packages/flutter_deck/lib/src/flutter_deck_app.dart index 7037535..5ca3bc4 100644 --- a/packages/flutter_deck/lib/src/flutter_deck_app.dart +++ b/packages/flutter_deck/lib/src/flutter_deck_app.dart @@ -270,7 +270,8 @@ class _FlutterDeckAppState extends State { presenterController: _presenterController, themeNotifier: _themeNotifier, child: FlutterDeckControlsListener( - notifier: _controlsNotifier, + controlsNotifier: _controlsNotifier, + markerNotifier: _markerNotifier, child: FlutterDeckTheme( data: theme, child: child!, diff --git a/packages/flutter_deck/lib/src/flutter_deck_slide.dart b/packages/flutter_deck/lib/src/flutter_deck_slide.dart index 22c8a46..51df7dd 100644 --- a/packages/flutter_deck/lib/src/flutter_deck_slide.dart +++ b/packages/flutter_deck/lib/src/flutter_deck_slide.dart @@ -404,6 +404,7 @@ class FlutterDeckSlide extends FlutterDeckSlideWidget { notifier: context.flutterDeck.markerNotifier, child: Scaffold( drawer: const FlutterDeckDrawer(), + drawerEnableOpenDragGesture: false, body: _SlideBody(child: _builder(context)), ), ),