From 3de0bacccc8a9d8c86557a32030b953a2869824f Mon Sep 17 00:00:00 2001 From: Mangirdas Kazlauskas Date: Sun, 22 Feb 2026 00:30:40 +0200 Subject: [PATCH 01/14] feat: add custom shortcuts and actions support --- doc/website/source/guides/custom-shortcuts.md | 85 ++++++++++++++ .../flutter_deck_controls_configuration.dart | 36 ++++++ .../flutter_deck_controls_listener.dart | 13 +++ ...tter_deck_controls_configuration_test.dart | 4 + .../flutter_deck_controls_listener_test.dart | 104 ++++++++++++++++++ 5 files changed, 242 insertions(+) create mode 100644 doc/website/source/guides/custom-shortcuts.md diff --git a/doc/website/source/guides/custom-shortcuts.md b/doc/website/source/guides/custom-shortcuts.md new file mode 100644 index 0000000..816b082 --- /dev/null +++ b/doc/website/source/guides/custom-shortcuts.md @@ -0,0 +1,85 @@ +--- +title: Custom shortcuts +navOrder: 10 +--- + +The `flutter_deck` package allows you to define custom keyboard shortcuts and actions for your presentation. This is useful if you want to add new functionality or override default behavior. + +## Adding Custom Shortcuts + +To add custom shortcuts, you need to provide a `FlutterDeckShortcutsConfiguration` to your `FlutterDeckControlsConfiguration`. This configuration accepts `customShortcuts` and `customActions` maps. + +### Example: Skip Slide Shortcut + +As an example, let's implement a shortcut to skip the current slide and jump to the next one, without going through its remaining steps. + +First, define a custom `Intent` for the action: + +```dart +import 'package:flutter/widgets.dart'; + +class SkipSlideIntent extends Intent { + const SkipSlideIntent(); +} +``` + +Next, define the corresponding `Action`. The framework allows you to access the `FlutterDeck` instance inside an action by extending `ContextAction`: + +```dart +import 'package:flutter/widgets.dart'; +import 'package:flutter_deck/flutter_deck.dart'; + +class SkipSlideAction extends ContextAction { + const SkipSlideAction(); + + @override + Object? invoke(SkipSlideIntent intent, [BuildContext? context]) { + if (context == null) return null; + + final flutterDeck = context.flutterDeck; + final router = flutterDeck.router; + final currentIndex = router.currentSlideIndex; + + // Skip to the next slide + if (currentIndex < router.slides.length - 1) { + flutterDeck.goToSlide(currentIndex + 2); + } + + return null; + } +} +``` + +Finally, map the intent and action in your `FlutterDeckApp` configuration. For example, to map the `SkipSlideIntent` to `Meta + Right Arrow`: + +```dart +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_deck/flutter_deck.dart'; + +class MyPresentation extends StatelessWidget { + @override + Widget build(BuildContext context) { + return FlutterDeckApp( + configuration: FlutterDeckConfiguration( + controls: FlutterDeckControlsConfiguration( + shortcuts: FlutterDeckShortcutsConfiguration( + customShortcuts: const { + SingleActivator(LogicalKeyboardKey.arrowRight, meta: true): SkipSlideIntent(), + }, + customActions: { + SkipSlideIntent: const SkipSlideAction(), + }, + ), + ), + ), + slides: [ + // ... your slides + ], + ); + } +} +``` + +> [!NOTE] +> Custom shortcuts cannot use the same key combinations as the default shortcuts (`nextSlide`, `previousSlide`, `toggleMarker`, `toggleNavigationDrawer`). Doing so will result in an assertion error. Defaults can be removed or disabled via their respective fields if you need to reuse their key combinations. 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 11ad1a6..9b05395 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,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_deck/src/flutter_deck.dart'; /// The configuration for the slide deck controls. class FlutterDeckControlsConfiguration { @@ -88,6 +89,8 @@ class FlutterDeckShortcutsConfiguration { this.previousSlide = const {SingleActivator(LogicalKeyboardKey.arrowLeft)}, this.toggleMarker = const {SingleActivator(LogicalKeyboardKey.keyM)}, this.toggleNavigationDrawer = const {SingleActivator(LogicalKeyboardKey.period)}, + this.customShortcuts = const {}, + this.customActions = const {}, }); /// Creates a configuration for the slide deck keyboard shortcuts where they @@ -108,4 +111,37 @@ class FlutterDeckShortcutsConfiguration { /// The key combinations to use for toggling the navigation drawer. final Set toggleNavigationDrawer; + + /// Custom shortcuts for the slide deck. + /// + /// This can be used to add custom shortcuts to the slide deck. The + /// [ShortcutActivator] is the key combination that will trigger the + /// shortcut, and the [Intent] is the intent that will be invoked when + /// the shortcut is triggered. + /// + /// To handle the intent, provide a corresponding [Action] in the + /// [FlutterDeckShortcutsConfiguration.customActions] map. + final Map customShortcuts; + + /// Custom actions for the slide deck. + /// + /// This can be used to add custom actions to the slide deck. The + /// [Type] is the type of the [Intent] that the action will handle, and + /// the [Action] is the action that will be invoked when the intent is + /// triggered. + /// + /// To access the [FlutterDeck] via these action intents, you can use a + /// [ContextAction] instead of a regular [Action]. For example: + /// + /// ```dart + /// class MyCustomAction extends ContextAction { + /// @override + /// Object? invoke(MyCustomIntent intent, BuildContext context) { + /// final flutterDeck = context.flutterDeck; + /// // Do something with the flutter_deck instance... + /// return null; + /// } + /// } + /// ``` + final Map> customActions; } 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 de0f6ca..508e1f7 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 @@ -100,6 +100,17 @@ class FlutterDeckControlsListener extends StatelessWidget { final shortcuts = controls.shortcuts; + assert( + shortcuts.customShortcuts.keys.every( + (activator) => + !shortcuts.nextSlide.contains(activator) && + !shortcuts.previousSlide.contains(activator) && + !shortcuts.toggleMarker.contains(activator) && + !shortcuts.toggleNavigationDrawer.contains(activator), + ), + 'Custom shortcuts must not clash with default shortcuts.', + ); + if (controls.presenterToolbarVisible || shortcuts.enabled) { widget = Actions( actions: >{ @@ -107,6 +118,7 @@ class FlutterDeckControlsListener extends StatelessWidget { GoPreviousIntent: GoPreviousAction(controlsNotifier), ToggleDrawerIntent: ToggleDrawerAction(controlsNotifier), ToggleMarkerIntent: ToggleMarkerAction(controlsNotifier), + ...shortcuts.customActions, }, child: widget, ); @@ -118,6 +130,7 @@ class FlutterDeckControlsListener extends StatelessWidget { for (final activator in shortcuts.previousSlide) activator: const GoPreviousIntent(), for (final activator in shortcuts.toggleMarker) activator: const ToggleMarkerIntent(), for (final activator in shortcuts.toggleNavigationDrawer) activator: const ToggleDrawerIntent(), + ...shortcuts.customShortcuts, }, child: widget, ); diff --git a/packages/flutter_deck/test/src/controls/flutter_deck_controls_configuration_test.dart b/packages/flutter_deck/test/src/controls/flutter_deck_controls_configuration_test.dart index 813f1cd..4887a06 100644 --- a/packages/flutter_deck/test/src/controls/flutter_deck_controls_configuration_test.dart +++ b/packages/flutter_deck/test/src/controls/flutter_deck_controls_configuration_test.dart @@ -68,12 +68,16 @@ void main() { expect(configuration.previousSlide, const {SingleActivator(LogicalKeyboardKey.arrowLeft)}); expect(configuration.toggleMarker, const {SingleActivator(LogicalKeyboardKey.keyM)}); expect(configuration.toggleNavigationDrawer, const {SingleActivator(LogicalKeyboardKey.period)}); + expect(configuration.customShortcuts, isEmpty); + expect(configuration.customActions, isEmpty); }); test('disabled factory should create disabled configuration', () { const configuration = FlutterDeckShortcutsConfiguration.disabled(); expect(configuration.enabled, false); + expect(configuration.customShortcuts, isEmpty); + expect(configuration.customActions, isEmpty); }); }); } diff --git a/packages/flutter_deck/test/src/controls/flutter_deck_controls_listener_test.dart b/packages/flutter_deck/test/src/controls/flutter_deck_controls_listener_test.dart index c0883f5..48f3945 100644 --- a/packages/flutter_deck/test/src/controls/flutter_deck_controls_listener_test.dart +++ b/packages/flutter_deck/test/src/controls/flutter_deck_controls_listener_test.dart @@ -172,5 +172,109 @@ void main() { verify(mockControlsNotifier.previous()).called(1); }); + + testWidgets('should allow custom shortcut and action', (tester) async { + var invoked = false; + BuildContext? actionContext; + + final customFlutterDeck = FlutterDeck( + configuration: FlutterDeckConfiguration( + controls: FlutterDeckControlsConfiguration( + shortcuts: FlutterDeckShortcutsConfiguration( + customShortcuts: const {SingleActivator(LogicalKeyboardKey.keyA): _MockIntent()}, + customActions: { + _MockIntent: _MockAction((context) { + invoked = true; + actionContext = context; + }), + }, + ), + ), + ), + router: MockFlutterDeckRouter(), + speakerInfo: null, + controlsNotifier: mockControlsNotifier, + drawerNotifier: MockFlutterDeckDrawerNotifier(), + localizationNotifier: MockFlutterDeckLocalizationNotifier(), + markerNotifier: mockMarkerNotifier, + presenterController: MockFlutterDeckPresenterController(), + themeNotifier: MockFlutterDeckThemeNotifier(), + localizationsDelegates: const [], + supportedLocales: const [Locale('en')], + plugins: const [], + ); + + await tester.pumpWidget( + MaterialApp( + home: FlutterDeckProvider( + flutterDeck: customFlutterDeck, + child: FlutterDeckControlsListener( + controlsNotifier: mockControlsNotifier, + markerNotifier: mockMarkerNotifier, + child: const SizedBox(), + ), + ), + ), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.keyA); + + expect(invoked, isTrue); + expect(actionContext?.flutterDeck, customFlutterDeck); + }); + + testWidgets('should assert when custom shortcuts clash with defaults', (tester) async { + final customFlutterDeck = FlutterDeck( + configuration: const FlutterDeckConfiguration( + controls: FlutterDeckControlsConfiguration( + shortcuts: FlutterDeckShortcutsConfiguration( + customShortcuts: {SingleActivator(LogicalKeyboardKey.arrowRight): _MockIntent()}, + ), + ), + ), + router: MockFlutterDeckRouter(), + speakerInfo: null, + controlsNotifier: mockControlsNotifier, + drawerNotifier: MockFlutterDeckDrawerNotifier(), + localizationNotifier: MockFlutterDeckLocalizationNotifier(), + markerNotifier: mockMarkerNotifier, + presenterController: MockFlutterDeckPresenterController(), + themeNotifier: MockFlutterDeckThemeNotifier(), + localizationsDelegates: const [], + supportedLocales: const [Locale('en')], + plugins: const [], + ); + + await tester.pumpWidget( + MaterialApp( + home: FlutterDeckProvider( + flutterDeck: customFlutterDeck, + child: FlutterDeckControlsListener( + controlsNotifier: mockControlsNotifier, + markerNotifier: mockMarkerNotifier, + child: const SizedBox(), + ), + ), + ), + ); + + expect(tester.takeException(), isAssertionError); + }); }); } + +class _MockIntent extends Intent { + const _MockIntent(); +} + +class _MockAction extends ContextAction<_MockIntent> { + _MockAction(this.onInvoke); + + final void Function(BuildContext) onInvoke; + + @override + Object? invoke(_MockIntent intent, [BuildContext? context]) { + onInvoke(context!); + return null; + } +} From fd3f83ac33d2d64d42166d4e4ffaac5b079076d2 Mon Sep 17 00:00:00 2001 From: Mangirdas Kazlauskas Date: Sun, 22 Feb 2026 00:40:44 +0200 Subject: [PATCH 02/14] refactor: shortcuts assertion --- .../flutter_deck_controls_listener.dart | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) 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 508e1f7..3b47178 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 @@ -100,16 +100,23 @@ class FlutterDeckControlsListener extends StatelessWidget { final shortcuts = controls.shortcuts; - assert( - shortcuts.customShortcuts.keys.every( - (activator) => - !shortcuts.nextSlide.contains(activator) && - !shortcuts.previousSlide.contains(activator) && - !shortcuts.toggleMarker.contains(activator) && - !shortcuts.toggleNavigationDrawer.contains(activator), - ), - 'Custom shortcuts must not clash with default shortcuts.', - ); + final allShortcuts = [ + ...shortcuts.nextSlide, + ...shortcuts.previousSlide, + ...shortcuts.toggleMarker, + ...shortcuts.toggleNavigationDrawer, + ...shortcuts.customShortcuts.keys, + ]; + + final seen = {}; + + for (final shortcut in allShortcuts) { + assert( + seen.add(shortcut), + 'Shortcuts must not clash with each other. ' + 'Multiple actions are mapped to the "$shortcut" shortcut.', + ); + } if (controls.presenterToolbarVisible || shortcuts.enabled) { widget = Actions( From c8de266df1a8525b071e807f7c36bf1742a62c2e Mon Sep 17 00:00:00 2001 From: Mangirdas Kazlauskas Date: Sun, 22 Feb 2026 00:54:54 +0200 Subject: [PATCH 03/14] refactor: custom shortcuts --- doc/website/source/guides/custom-shortcuts.md | 97 ++++++++----------- .../lib/src/controls/controls.dart | 1 + .../flutter_deck_controls_configuration.dart | 42 ++------ .../flutter_deck_controls_listener.dart | 6 +- .../src/controls/flutter_deck_shortcut.dart | 23 +++++ .../marker/flutter_deck_marker_painter.dart | 6 +- ...tter_deck_controls_configuration_test.dart | 2 - .../flutter_deck_controls_listener_test.dart | 27 ++++-- 8 files changed, 94 insertions(+), 110 deletions(-) create mode 100644 packages/flutter_deck/lib/src/controls/flutter_deck_shortcut.dart diff --git a/doc/website/source/guides/custom-shortcuts.md b/doc/website/source/guides/custom-shortcuts.md index 816b082..2ad228c 100644 --- a/doc/website/source/guides/custom-shortcuts.md +++ b/doc/website/source/guides/custom-shortcuts.md @@ -3,82 +3,63 @@ title: Custom shortcuts navOrder: 10 --- -The `flutter_deck` package allows you to define custom keyboard shortcuts and actions for your presentation. This is useful if you want to add new functionality or override default behavior. +# Custom Shortcuts -## Adding Custom Shortcuts +Flutter Deck allows you to define custom shortcuts to control your presentation using the keyboard. By default, it includes shortcuts for navigating between slides, toggling the marker, and opening the navigation drawer. You can add your own shortcuts to trigger specific actions. -To add custom shortcuts, you need to provide a `FlutterDeckShortcutsConfiguration` to your `FlutterDeckControlsConfiguration`. This configuration accepts `customShortcuts` and `customActions` maps. +## Defining Custom Shortcuts -### Example: Skip Slide Shortcut +To add custom shortcuts, you need to configure the `FlutterDeckShortcutsConfiguration` when setting up your `FlutterDeckApp`. This configuration requires a list of `FlutterDeckShortcut` objects, which bind a `ShortcutActivator`, an `Intent`, and an `Action`. -As an example, let's implement a shortcut to skip the current slide and jump to the next one, without going through its remaining steps. +### Example: Skip to the Next Topic -First, define a custom `Intent` for the action: +Imagine you have a long presentation and want a quick way to skip ahead 5 slides. You can accomplish this by creating a custom intent, a corresponding action, and then bundling them into a `FlutterDeckShortcut`. ```dart -import 'package:flutter/widgets.dart'; - -class SkipSlideIntent extends Intent { - const SkipSlideIntent(); +class SkipTopicIntent extends Intent { + const SkipTopicIntent(); } -``` - -Next, define the corresponding `Action`. The framework allows you to access the `FlutterDeck` instance inside an action by extending `ContextAction`: - -```dart -import 'package:flutter/widgets.dart'; -import 'package:flutter_deck/flutter_deck.dart'; - -class SkipSlideAction extends ContextAction { - const SkipSlideAction(); +class SkipTopicAction extends ContextAction { @override - Object? invoke(SkipSlideIntent intent, [BuildContext? context]) { - if (context == null) return null; - - final flutterDeck = context.flutterDeck; - final router = flutterDeck.router; - final currentIndex = router.currentSlideIndex; - - // Skip to the next slide - if (currentIndex < router.slides.length - 1) { - flutterDeck.goToSlide(currentIndex + 2); + Object? invoke(SkipTopicIntent intent, BuildContext context) { + // Jump ahead 5 slides + for (var i = 0; i < 5; i++) { + context.flutterDeck.next(); } - return null; } } + +// In your FlutterDeckApp configuration: +FlutterDeckApp( + configuration: const FlutterDeckConfiguration( + controls: FlutterDeckControlsConfiguration( + shortcuts: FlutterDeckShortcutsConfiguration( + customShortcuts: [ + FlutterDeckShortcut( + activator: SingleActivator(LogicalKeyboardKey.keyS, control: true), + intent: SkipTopicIntent(), + action: SkipTopicAction(), + ), + ], + ), + ), + ), + // ... other app setup +) ``` -Finally, map the intent and action in your `FlutterDeckApp` configuration. For example, to map the `SkipSlideIntent` to `Meta + Right Arrow`: +In this example, pressing `Ctrl + S` triggers the `SkipTopicIntent`. The `SkipTopicAction`, extending `ContextAction`, gains access to the `BuildContext` allowing it to call `context.flutterDeck.next()` repeatedly. If the shortcut doesn't require context, you can simply extend `Action`. -```dart -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_deck/flutter_deck.dart'; +## Avoiding Shortcut Clashes -class MyPresentation extends StatelessWidget { - @override - Widget build(BuildContext context) { - return FlutterDeckApp( - configuration: FlutterDeckConfiguration( - controls: FlutterDeckControlsConfiguration( - shortcuts: FlutterDeckShortcutsConfiguration( - customShortcuts: const { - SingleActivator(LogicalKeyboardKey.arrowRight, meta: true): SkipSlideIntent(), - }, - customActions: { - SkipSlideIntent: const SkipSlideAction(), - }, - ), - ), - ), - slides: [ - // ... your slides - ], - ); - } -} +Flutter Deck automatically checks if your custom shortcuts clash with the default ones (like the arrow keys for navigation or 'M' for the marker). If a clash is detected, it will throw an `AssertionError` during development, explicitly stating which shortcut key is causing the problem. + +For instance, trying to assign a custom action to the right arrow key will trigger the following error: + +```text +Shortcuts must not clash with each other. Multiple actions are mapped to the "LogicalKeySet(LogicalKeyboardKey#00115(keyId: "0x100000015", keyLabel: "Arrow Right", debugName: "Arrow Right"))" shortcut. ``` > [!NOTE] diff --git a/packages/flutter_deck/lib/src/controls/controls.dart b/packages/flutter_deck/lib/src/controls/controls.dart index ae54447..3be58ec 100644 --- a/packages/flutter_deck/lib/src/controls/controls.dart +++ b/packages/flutter_deck/lib/src/controls/controls.dart @@ -2,5 +2,6 @@ export 'flutter_deck_controls.dart'; export 'flutter_deck_controls_configuration.dart'; export 'flutter_deck_controls_listener.dart'; export 'flutter_deck_controls_notifier.dart'; +export 'flutter_deck_shortcut.dart'; export 'fullscreen/fullscreen.dart'; export 'l10n/l10n.dart'; 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 9b05395..d8b8658 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,7 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_deck/src/flutter_deck.dart'; +import 'package:flutter_deck/src/controls/flutter_deck_shortcut.dart'; /// The configuration for the slide deck controls. class FlutterDeckControlsConfiguration { @@ -19,6 +19,8 @@ class FlutterDeckControlsConfiguration { /// - [SingleActivator] for more information on how to define a key /// combination. /// - [LogicalKeyboardKey] for a list of all available keys. + /// - [FlutterDeckShortcut] for more information on how to define a + /// custom shortcut. const FlutterDeckControlsConfiguration({ this.presenterToolbarVisible = true, this.gestures = const FlutterDeckGesturesConfiguration.mobileOnly(), @@ -83,14 +85,15 @@ class FlutterDeckShortcutsConfiguration { /// - [SingleActivator] for more information on how to define a key /// combination. /// - [LogicalKeyboardKey] for a list of all available keys. + /// - [FlutterDeckShortcut] for more information on how to define a + /// custom shortcut. const FlutterDeckShortcutsConfiguration({ this.enabled = true, this.nextSlide = const {SingleActivator(LogicalKeyboardKey.arrowRight)}, this.previousSlide = const {SingleActivator(LogicalKeyboardKey.arrowLeft)}, this.toggleMarker = const {SingleActivator(LogicalKeyboardKey.keyM)}, this.toggleNavigationDrawer = const {SingleActivator(LogicalKeyboardKey.period)}, - this.customShortcuts = const {}, - this.customActions = const {}, + this.customShortcuts = const [], }); /// Creates a configuration for the slide deck keyboard shortcuts where they @@ -114,34 +117,7 @@ class FlutterDeckShortcutsConfiguration { /// Custom shortcuts for the slide deck. /// - /// This can be used to add custom shortcuts to the slide deck. The - /// [ShortcutActivator] is the key combination that will trigger the - /// shortcut, and the [Intent] is the intent that will be invoked when - /// the shortcut is triggered. - /// - /// To handle the intent, provide a corresponding [Action] in the - /// [FlutterDeckShortcutsConfiguration.customActions] map. - final Map customShortcuts; - - /// Custom actions for the slide deck. - /// - /// This can be used to add custom actions to the slide deck. The - /// [Type] is the type of the [Intent] that the action will handle, and - /// the [Action] is the action that will be invoked when the intent is - /// triggered. - /// - /// To access the [FlutterDeck] via these action intents, you can use a - /// [ContextAction] instead of a regular [Action]. For example: - /// - /// ```dart - /// class MyCustomAction extends ContextAction { - /// @override - /// Object? invoke(MyCustomIntent intent, BuildContext context) { - /// final flutterDeck = context.flutterDeck; - /// // Do something with the flutter_deck instance... - /// return null; - /// } - /// } - /// ``` - final Map> customActions; + /// This can be used to add custom shortcuts and their corresponding actions + /// to the slide deck. + final List customShortcuts; } 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 3b47178..941166e 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 @@ -105,7 +105,7 @@ class FlutterDeckControlsListener extends StatelessWidget { ...shortcuts.previousSlide, ...shortcuts.toggleMarker, ...shortcuts.toggleNavigationDrawer, - ...shortcuts.customShortcuts.keys, + ...shortcuts.customShortcuts.map((e) => e.activator), ]; final seen = {}; @@ -125,7 +125,7 @@ class FlutterDeckControlsListener extends StatelessWidget { GoPreviousIntent: GoPreviousAction(controlsNotifier), ToggleDrawerIntent: ToggleDrawerAction(controlsNotifier), ToggleMarkerIntent: ToggleMarkerAction(controlsNotifier), - ...shortcuts.customActions, + for (final shortcut in shortcuts.customShortcuts) shortcut.intent.runtimeType: shortcut.action, }, child: widget, ); @@ -137,7 +137,7 @@ class FlutterDeckControlsListener extends StatelessWidget { for (final activator in shortcuts.previousSlide) activator: const GoPreviousIntent(), for (final activator in shortcuts.toggleMarker) activator: const ToggleMarkerIntent(), for (final activator in shortcuts.toggleNavigationDrawer) activator: const ToggleDrawerIntent(), - ...shortcuts.customShortcuts, + for (final shortcut in shortcuts.customShortcuts) shortcut.activator: shortcut.intent, }, child: widget, ); diff --git a/packages/flutter_deck/lib/src/controls/flutter_deck_shortcut.dart b/packages/flutter_deck/lib/src/controls/flutter_deck_shortcut.dart new file mode 100644 index 0000000..f8f49af --- /dev/null +++ b/packages/flutter_deck/lib/src/controls/flutter_deck_shortcut.dart @@ -0,0 +1,23 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_deck/src/flutter_deck.dart'; + +/// A shortcut for the slide deck. +/// +/// This configuration is used to map a key combination to an [Intent] and an +/// [Action]. +class FlutterDeckShortcut { + /// Creates a shortcut for the slide deck. + const FlutterDeckShortcut({required this.activator, required this.intent, required this.action}); + + /// The key combination that will trigger the shortcut. + final ShortcutActivator activator; + + /// The intent that will be invoked when the shortcut is triggered. + final Intent intent; + + /// The action that will be invoked when the intent is triggered. + /// + /// To access [FlutterDeck] via this action, you can use a [ContextAction] + /// instead of a regular [Action]. + final Action action; +} diff --git a/packages/flutter_deck/lib/src/widgets/internal/marker/flutter_deck_marker_painter.dart b/packages/flutter_deck/lib/src/widgets/internal/marker/flutter_deck_marker_painter.dart index 8650fbc..a6b5afa 100644 --- a/packages/flutter_deck/lib/src/widgets/internal/marker/flutter_deck_marker_painter.dart +++ b/packages/flutter_deck/lib/src/widgets/internal/marker/flutter_deck_marker_painter.dart @@ -7,11 +7,7 @@ class FlutterDeckMarkerPainter extends CustomPainter { /// Creates a [FlutterDeckMarkerPainter]. /// /// The [configuration] and [paths] arguments must not be null. - const FlutterDeckMarkerPainter({ - required this.configuration, - required this.paths, - this.version = 0, - }); + const FlutterDeckMarkerPainter({required this.configuration, required this.paths, this.version = 0}); /// The configuration of the marker. final FlutterDeckMarkerConfiguration configuration; diff --git a/packages/flutter_deck/test/src/controls/flutter_deck_controls_configuration_test.dart b/packages/flutter_deck/test/src/controls/flutter_deck_controls_configuration_test.dart index 4887a06..c713855 100644 --- a/packages/flutter_deck/test/src/controls/flutter_deck_controls_configuration_test.dart +++ b/packages/flutter_deck/test/src/controls/flutter_deck_controls_configuration_test.dart @@ -69,7 +69,6 @@ void main() { expect(configuration.toggleMarker, const {SingleActivator(LogicalKeyboardKey.keyM)}); expect(configuration.toggleNavigationDrawer, const {SingleActivator(LogicalKeyboardKey.period)}); expect(configuration.customShortcuts, isEmpty); - expect(configuration.customActions, isEmpty); }); test('disabled factory should create disabled configuration', () { @@ -77,7 +76,6 @@ void main() { expect(configuration.enabled, false); expect(configuration.customShortcuts, isEmpty); - expect(configuration.customActions, isEmpty); }); }); } diff --git a/packages/flutter_deck/test/src/controls/flutter_deck_controls_listener_test.dart b/packages/flutter_deck/test/src/controls/flutter_deck_controls_listener_test.dart index 48f3945..1d6771c 100644 --- a/packages/flutter_deck/test/src/controls/flutter_deck_controls_listener_test.dart +++ b/packages/flutter_deck/test/src/controls/flutter_deck_controls_listener_test.dart @@ -181,13 +181,16 @@ void main() { configuration: FlutterDeckConfiguration( controls: FlutterDeckControlsConfiguration( shortcuts: FlutterDeckShortcutsConfiguration( - customShortcuts: const {SingleActivator(LogicalKeyboardKey.keyA): _MockIntent()}, - customActions: { - _MockIntent: _MockAction((context) { - invoked = true; - actionContext = context; - }), - }, + customShortcuts: [ + FlutterDeckShortcut( + activator: const SingleActivator(LogicalKeyboardKey.keyA), + intent: const _MockIntent(), + action: _MockAction((context) { + invoked = true; + actionContext = context; + }), + ), + ], ), ), ), @@ -225,10 +228,16 @@ void main() { testWidgets('should assert when custom shortcuts clash with defaults', (tester) async { final customFlutterDeck = FlutterDeck( - configuration: const FlutterDeckConfiguration( + configuration: FlutterDeckConfiguration( controls: FlutterDeckControlsConfiguration( shortcuts: FlutterDeckShortcutsConfiguration( - customShortcuts: {SingleActivator(LogicalKeyboardKey.arrowRight): _MockIntent()}, + customShortcuts: [ + FlutterDeckShortcut( + activator: const SingleActivator(LogicalKeyboardKey.arrowRight), + intent: const _MockIntent(), + action: _MockAction((_) {}), + ), + ], ), ), ), From 8ec86c329e1407321e4c3f5ef105088a4f13320a Mon Sep 17 00:00:00 2001 From: Mangirdas Kazlauskas Date: Sun, 22 Feb 2026 01:10:24 +0200 Subject: [PATCH 04/14] refactor: use abstract class --- doc/website/source/guides/custom-shortcuts.md | 24 +++++++---- .../src/controls/flutter_deck_shortcut.dart | 10 ++--- .../flutter_deck_controls_listener_test.dart | 42 +++++++++++++------ 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/doc/website/source/guides/custom-shortcuts.md b/doc/website/source/guides/custom-shortcuts.md index 2ad228c..669e536 100644 --- a/doc/website/source/guides/custom-shortcuts.md +++ b/doc/website/source/guides/custom-shortcuts.md @@ -9,11 +9,11 @@ Flutter Deck allows you to define custom shortcuts to control your presentation ## Defining Custom Shortcuts -To add custom shortcuts, you need to configure the `FlutterDeckShortcutsConfiguration` when setting up your `FlutterDeckApp`. This configuration requires a list of `FlutterDeckShortcut` objects, which bind a `ShortcutActivator`, an `Intent`, and an `Action`. +To add custom shortcuts, you need to configure the `FlutterDeckShortcutsConfiguration` when setting up your `FlutterDeckApp`. This configuration requires a list of custom shortcuts that extend the abstract `FlutterDeckShortcut` class. This class binds a `ShortcutActivator` (like a key press), an `Intent`, and an `Action` together. ### Example: Skip to the Next Topic -Imagine you have a long presentation and want a quick way to skip ahead 5 slides. You can accomplish this by creating a custom intent, a corresponding action, and then bundling them into a `FlutterDeckShortcut`. +Imagine you have a long presentation and want a quick way to skip ahead 5 slides. You can accomplish this by creating a custom intent and a corresponding action. Then, implement the `FlutterDeckShortcut` interface to bundle them. ```dart class SkipTopicIntent extends Intent { @@ -31,17 +31,27 @@ class SkipTopicAction extends ContextAction { } } +class SkipTopicShortcut extends FlutterDeckShortcut { + const SkipTopicShortcut(); + + @override + ShortcutActivator get activator => + const SingleActivator(LogicalKeyboardKey.keyS, control: true); + + @override + SkipTopicIntent get intent => const SkipTopicIntent(); + + @override + Action get action => SkipTopicAction(); +} + // In your FlutterDeckApp configuration: FlutterDeckApp( configuration: const FlutterDeckConfiguration( controls: FlutterDeckControlsConfiguration( shortcuts: FlutterDeckShortcutsConfiguration( customShortcuts: [ - FlutterDeckShortcut( - activator: SingleActivator(LogicalKeyboardKey.keyS, control: true), - intent: SkipTopicIntent(), - action: SkipTopicAction(), - ), + SkipTopicShortcut(), ], ), ), diff --git a/packages/flutter_deck/lib/src/controls/flutter_deck_shortcut.dart b/packages/flutter_deck/lib/src/controls/flutter_deck_shortcut.dart index f8f49af..fdebb29 100644 --- a/packages/flutter_deck/lib/src/controls/flutter_deck_shortcut.dart +++ b/packages/flutter_deck/lib/src/controls/flutter_deck_shortcut.dart @@ -5,19 +5,19 @@ import 'package:flutter_deck/src/flutter_deck.dart'; /// /// This configuration is used to map a key combination to an [Intent] and an /// [Action]. -class FlutterDeckShortcut { +abstract class FlutterDeckShortcut { /// Creates a shortcut for the slide deck. - const FlutterDeckShortcut({required this.activator, required this.intent, required this.action}); + const FlutterDeckShortcut(); /// The key combination that will trigger the shortcut. - final ShortcutActivator activator; + ShortcutActivator get activator; /// The intent that will be invoked when the shortcut is triggered. - final Intent intent; + T get intent; /// The action that will be invoked when the intent is triggered. /// /// To access [FlutterDeck] via this action, you can use a [ContextAction] /// instead of a regular [Action]. - final Action action; + Action get action; } diff --git a/packages/flutter_deck/test/src/controls/flutter_deck_controls_listener_test.dart b/packages/flutter_deck/test/src/controls/flutter_deck_controls_listener_test.dart index 1d6771c..2ce419c 100644 --- a/packages/flutter_deck/test/src/controls/flutter_deck_controls_listener_test.dart +++ b/packages/flutter_deck/test/src/controls/flutter_deck_controls_listener_test.dart @@ -182,10 +182,8 @@ void main() { controls: FlutterDeckControlsConfiguration( shortcuts: FlutterDeckShortcutsConfiguration( customShortcuts: [ - FlutterDeckShortcut( - activator: const SingleActivator(LogicalKeyboardKey.keyA), - intent: const _MockIntent(), - action: _MockAction((context) { + _MockShortcut( + _MockAction((context) { invoked = true; actionContext = context; }), @@ -230,15 +228,7 @@ void main() { final customFlutterDeck = FlutterDeck( configuration: FlutterDeckConfiguration( controls: FlutterDeckControlsConfiguration( - shortcuts: FlutterDeckShortcutsConfiguration( - customShortcuts: [ - FlutterDeckShortcut( - activator: const SingleActivator(LogicalKeyboardKey.arrowRight), - intent: const _MockIntent(), - action: _MockAction((_) {}), - ), - ], - ), + shortcuts: FlutterDeckShortcutsConfiguration(customShortcuts: [_MockClashingShortcut(_MockAction((_) {}))]), ), ), router: MockFlutterDeckRouter(), @@ -287,3 +277,29 @@ class _MockAction extends ContextAction<_MockIntent> { return null; } } + +class _MockShortcut extends FlutterDeckShortcut<_MockIntent> { + const _MockShortcut(this.action); + + @override + ShortcutActivator get activator => const SingleActivator(LogicalKeyboardKey.keyA); + + @override + _MockIntent get intent => const _MockIntent(); + + @override + final Action<_MockIntent> action; +} + +class _MockClashingShortcut extends FlutterDeckShortcut<_MockIntent> { + const _MockClashingShortcut(this.action); + + @override + ShortcutActivator get activator => const SingleActivator(LogicalKeyboardKey.arrowRight); + + @override + _MockIntent get intent => const _MockIntent(); + + @override + final Action<_MockIntent> action; +} From 14789c2443bc5a893922c0d181dfdfc445b90952 Mon Sep 17 00:00:00 2001 From: Mangirdas Kazlauskas Date: Sun, 22 Feb 2026 01:30:50 +0200 Subject: [PATCH 05/14] feat: add example --- doc/website/source/guides/custom-shortcuts.md | 62 +++++++++---------- packages/flutter_deck/example/lib/main.dart | 4 ++ .../example/lib/shortcuts/shortcuts.dart | 1 + .../lib/shortcuts/skip_slide_shortcut.dart | 33 ++++++++++ packages/flutter_deck/lib/flutter_deck.dart | 1 + 5 files changed, 67 insertions(+), 34 deletions(-) create mode 100644 packages/flutter_deck/example/lib/shortcuts/shortcuts.dart create mode 100644 packages/flutter_deck/example/lib/shortcuts/skip_slide_shortcut.dart diff --git a/doc/website/source/guides/custom-shortcuts.md b/doc/website/source/guides/custom-shortcuts.md index 669e536..62d14d6 100644 --- a/doc/website/source/guides/custom-shortcuts.md +++ b/doc/website/source/guides/custom-shortcuts.md @@ -3,74 +3,68 @@ title: Custom shortcuts navOrder: 10 --- -# Custom Shortcuts +# Custom shortcuts -Flutter Deck allows you to define custom shortcuts to control your presentation using the keyboard. By default, it includes shortcuts for navigating between slides, toggling the marker, and opening the navigation drawer. You can add your own shortcuts to trigger specific actions. +It is possible to define custom shortcuts to control your presentation using the keyboard. By default, it includes shortcuts for navigating between slides, toggling the marker, and opening the navigation drawer. You can add your own shortcuts to trigger specific actions. -## Defining Custom Shortcuts +## Defining custom shortcuts To add custom shortcuts, you need to configure the `FlutterDeckShortcutsConfiguration` when setting up your `FlutterDeckApp`. This configuration requires a list of custom shortcuts that extend the abstract `FlutterDeckShortcut` class. This class binds a `ShortcutActivator` (like a key press), an `Intent`, and an `Action` together. -### Example: Skip to the Next Topic +### Example: Skip to the next slide -Imagine you have a long presentation and want a quick way to skip ahead 5 slides. You can accomplish this by creating a custom intent and a corresponding action. Then, implement the `FlutterDeckShortcut` interface to bundle them. +In flutter_deck, slides can have multiple steps. This example shows how to add a custom shortcut to skip to the next slide, ignoring the steps. ```dart -class SkipTopicIntent extends Intent { - const SkipTopicIntent(); +class SkipSlideIntent extends Intent { + const SkipSlideIntent(); } -class SkipTopicAction extends ContextAction { +class SkipSlideAction extends ContextAction { @override - Object? invoke(SkipTopicIntent intent, BuildContext context) { - // Jump ahead 5 slides - for (var i = 0; i < 5; i++) { - context.flutterDeck.next(); - } + Object? invoke(SkipSlideIntent intent, [BuildContext? context]) { + if (context == null) return null; + + final currentSlide = context.flutterDeck.router.currentSlideIndex + 1; + + context.flutterDeck.router.goToSlide(currentSlide + 1); + return null; } } -class SkipTopicShortcut extends FlutterDeckShortcut { - const SkipTopicShortcut(); +class SkipSlideShortcut extends FlutterDeckShortcut { + const SkipSlideShortcut(); @override - ShortcutActivator get activator => - const SingleActivator(LogicalKeyboardKey.keyS, control: true); + ShortcutActivator get activator => const SingleActivator(LogicalKeyboardKey.keyS, control: true); @override - SkipTopicIntent get intent => const SkipTopicIntent(); + SkipSlideIntent get intent => const SkipSlideIntent(); @override - Action get action => SkipTopicAction(); + Action get action => SkipSlideAction(); } +``` + +Then, add the shortcut to your presentation: -// In your FlutterDeckApp configuration: +```dart FlutterDeckApp( configuration: const FlutterDeckConfiguration( controls: FlutterDeckControlsConfiguration( shortcuts: FlutterDeckShortcutsConfiguration( customShortcuts: [ - SkipTopicShortcut(), + SkipSlideShortcut(), ], ), ), ), - // ... other app setup ) ``` -In this example, pressing `Ctrl + S` triggers the `SkipTopicIntent`. The `SkipTopicAction`, extending `ContextAction`, gains access to the `BuildContext` allowing it to call `context.flutterDeck.next()` repeatedly. If the shortcut doesn't require context, you can simply extend `Action`. - -## Avoiding Shortcut Clashes - -Flutter Deck automatically checks if your custom shortcuts clash with the default ones (like the arrow keys for navigation or 'M' for the marker). If a clash is detected, it will throw an `AssertionError` during development, explicitly stating which shortcut key is causing the problem. +In this example, pressing `Ctrl + S` triggers the `SkipSlideIntent`. The `SkipSlideAction`, extending `ContextAction`, gains access to the `BuildContext` allowing it to call `context.flutterDeck.router.goToSlide()` to skip to the next slide. If the shortcut doesn't require context, you can simply extend `Action`. -For instance, trying to assign a custom action to the right arrow key will trigger the following error: - -```text -Shortcuts must not clash with each other. Multiple actions are mapped to the "LogicalKeySet(LogicalKeyboardKey#00115(keyId: "0x100000015", keyLabel: "Arrow Right", debugName: "Arrow Right"))" shortcut. -``` +## Avoiding shortcut clashes -> [!NOTE] -> Custom shortcuts cannot use the same key combinations as the default shortcuts (`nextSlide`, `previousSlide`, `toggleMarker`, `toggleNavigationDrawer`). Doing so will result in an assertion error. Defaults can be removed or disabled via their respective fields if you need to reuse their key combinations. +The flutter_deck framework automatically checks if your custom shortcuts clash with any of the default shortcuts or any other custom shortcuts. If a clash is detected, it will throw an `AssertionError` during development, explicitly stating which shortcut key is causing the problem. diff --git a/packages/flutter_deck/example/lib/main.dart b/packages/flutter_deck/example/lib/main.dart index eb69b62..7ea4f1c 100644 --- a/packages/flutter_deck/example/lib/main.dart +++ b/packages/flutter_deck/example/lib/main.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_deck/flutter_deck.dart'; import 'package:flutter_deck_example/l10n/l10n.dart'; +import 'package:flutter_deck_example/shortcuts/shortcuts.dart'; import 'package:flutter_deck_example/slides/slides.dart'; import 'package:flutter_deck_example/templates/templates.dart'; import 'package:flutter_deck_pdf_export/flutter_deck_pdf_export.dart'; @@ -37,6 +38,9 @@ class FlutterDeckExample extends StatelessWidget { ), ), ), + controls: const FlutterDeckControlsConfiguration( + shortcuts: FlutterDeckShortcutsConfiguration(customShortcuts: [SkipSlideShortcut()]), + ), // Set defaults for the footer. footer: const FlutterDeckFooterConfiguration(showSlideNumbers: true, showSocialHandle: true), // Set defaults for the header. diff --git a/packages/flutter_deck/example/lib/shortcuts/shortcuts.dart b/packages/flutter_deck/example/lib/shortcuts/shortcuts.dart new file mode 100644 index 0000000..0af5c17 --- /dev/null +++ b/packages/flutter_deck/example/lib/shortcuts/shortcuts.dart @@ -0,0 +1 @@ +export 'skip_slide_shortcut.dart'; diff --git a/packages/flutter_deck/example/lib/shortcuts/skip_slide_shortcut.dart b/packages/flutter_deck/example/lib/shortcuts/skip_slide_shortcut.dart new file mode 100644 index 0000000..5876fe0 --- /dev/null +++ b/packages/flutter_deck/example/lib/shortcuts/skip_slide_shortcut.dart @@ -0,0 +1,33 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_deck/flutter_deck.dart'; + +class SkipSlideIntent extends Intent { + const SkipSlideIntent(); +} + +class SkipSlideAction extends ContextAction { + @override + Object? invoke(SkipSlideIntent intent, [BuildContext? context]) { + if (context == null) return null; + + final currentSlide = context.flutterDeck.router.currentSlideIndex + 1; + + context.flutterDeck.router.goToSlide(currentSlide + 1); + + return null; + } +} + +class SkipSlideShortcut extends FlutterDeckShortcut { + const SkipSlideShortcut(); + + @override + ShortcutActivator get activator => const SingleActivator(LogicalKeyboardKey.keyS, control: true); + + @override + SkipSlideIntent get intent => const SkipSlideIntent(); + + @override + Action get action => SkipSlideAction(); +} diff --git a/packages/flutter_deck/lib/flutter_deck.dart b/packages/flutter_deck/lib/flutter_deck.dart index 8bce9b0..31c7e32 100644 --- a/packages/flutter_deck/lib/flutter_deck.dart +++ b/packages/flutter_deck/lib/flutter_deck.dart @@ -2,6 +2,7 @@ library; export 'src/configuration/configuration.dart'; +export 'src/controls/flutter_deck_shortcut.dart'; export 'src/flutter_deck.dart'; export 'src/flutter_deck_app.dart'; export 'src/flutter_deck_slide.dart'; From 6b014e8dcd09a3b2db866c56e87a19494ee28bc6 Mon Sep 17 00:00:00 2001 From: Mangirdas Kazlauskas Date: Sun, 22 Feb 2026 01:32:17 +0200 Subject: [PATCH 06/14] chore: add release notes --- packages/flutter_deck/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/flutter_deck/CHANGELOG.md b/packages/flutter_deck/CHANGELOG.md index 9daabae..2bdeaaa 100644 --- a/packages/flutter_deck/CHANGELOG.md +++ b/packages/flutter_deck/CHANGELOG.md @@ -1,5 +1,6 @@ # NEXT +- feat: add `FlutterDeckShortcut` to define custom shortcuts and actions - feat: allow defining multiple key combinations for a single shortcut - **BREAKING**: `nextSlide`, `previousSlide`, `toggleMarker` and `toggleNavigationDrawer` properties in the `FlutterDeckShortcutsConfiguration` class now accept a `Set` instead of a `SingleActivator` - **Migration**: instead of: From 14f6c50100f4e526fa203102081c83a2c7df191d Mon Sep 17 00:00:00 2001 From: Mangirdas Kazlauskas Date: Sun, 22 Feb 2026 01:40:44 +0200 Subject: [PATCH 07/14] fix: runtimeType check --- .../lib/src/controls/flutter_deck_controls_listener.dart | 4 +++- .../lib/src/controls/flutter_deck_shortcut.dart | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) 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 941166e..7de4193 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 @@ -15,6 +15,8 @@ import 'package:flutter_deck/src/widgets/internal/internal.dart'; /// * `toggleMarker` - Toggle the slide deck's marker. /// * `toggleNavigationDrawer` - Toggle the navigation drawer. /// +/// Also, custom shortcuts and actions can be defined by the user. +/// /// Cursor visibility is also handled by this widget. The cursor will be hidden /// after 3 seconds of inactivity. /// @@ -125,7 +127,7 @@ class FlutterDeckControlsListener extends StatelessWidget { GoPreviousIntent: GoPreviousAction(controlsNotifier), ToggleDrawerIntent: ToggleDrawerAction(controlsNotifier), ToggleMarkerIntent: ToggleMarkerAction(controlsNotifier), - for (final shortcut in shortcuts.customShortcuts) shortcut.intent.runtimeType: shortcut.action, + for (final shortcut in shortcuts.customShortcuts) shortcut.intentType: shortcut.action, }, child: widget, ); diff --git a/packages/flutter_deck/lib/src/controls/flutter_deck_shortcut.dart b/packages/flutter_deck/lib/src/controls/flutter_deck_shortcut.dart index fdebb29..edb80af 100644 --- a/packages/flutter_deck/lib/src/controls/flutter_deck_shortcut.dart +++ b/packages/flutter_deck/lib/src/controls/flutter_deck_shortcut.dart @@ -20,4 +20,11 @@ abstract class FlutterDeckShortcut { /// To access [FlutterDeck] via this action, you can use a [ContextAction] /// instead of a regular [Action]. Action get action; + + /// The type of the intent that will be invoked when the shortcut is + /// triggered. + /// + /// This is used to map the intent type to the action in the controls + /// listener. Do not override this, it relies on the generic type [T]. + Type get intentType => T; } From c13f587f29f44d59e1f26ab6a560ebbb688b3136 Mon Sep 17 00:00:00 2001 From: Mangirdas Kazlauskas Date: Sun, 22 Feb 2026 01:43:12 +0200 Subject: [PATCH 08/14] refactor: update shortcut --- doc/website/source/guides/custom-shortcuts.md | 2 +- .../flutter_deck/example/lib/shortcuts/skip_slide_shortcut.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/website/source/guides/custom-shortcuts.md b/doc/website/source/guides/custom-shortcuts.md index 62d14d6..f254d27 100644 --- a/doc/website/source/guides/custom-shortcuts.md +++ b/doc/website/source/guides/custom-shortcuts.md @@ -37,7 +37,7 @@ class SkipSlideShortcut extends FlutterDeckShortcut { const SkipSlideShortcut(); @override - ShortcutActivator get activator => const SingleActivator(LogicalKeyboardKey.keyS, control: true); + ShortcutActivator get activator => const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true); @override SkipSlideIntent get intent => const SkipSlideIntent(); diff --git a/packages/flutter_deck/example/lib/shortcuts/skip_slide_shortcut.dart b/packages/flutter_deck/example/lib/shortcuts/skip_slide_shortcut.dart index 5876fe0..c2a0e9e 100644 --- a/packages/flutter_deck/example/lib/shortcuts/skip_slide_shortcut.dart +++ b/packages/flutter_deck/example/lib/shortcuts/skip_slide_shortcut.dart @@ -23,7 +23,7 @@ class SkipSlideShortcut extends FlutterDeckShortcut { const SkipSlideShortcut(); @override - ShortcutActivator get activator => const SingleActivator(LogicalKeyboardKey.keyS, control: true); + ShortcutActivator get activator => const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true); @override SkipSlideIntent get intent => const SkipSlideIntent(); From fc1749a62781a6d89bc226371ac7ba8d4b515f96 Mon Sep 17 00:00:00 2001 From: Mangirdas Kazlauskas Date: Sun, 22 Feb 2026 01:44:04 +0200 Subject: [PATCH 09/14] docs: update --- doc/website/source/guides/custom-shortcuts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/website/source/guides/custom-shortcuts.md b/doc/website/source/guides/custom-shortcuts.md index f254d27..24d56e2 100644 --- a/doc/website/source/guides/custom-shortcuts.md +++ b/doc/website/source/guides/custom-shortcuts.md @@ -63,7 +63,7 @@ FlutterDeckApp( ) ``` -In this example, pressing `Ctrl + S` triggers the `SkipSlideIntent`. The `SkipSlideAction`, extending `ContextAction`, gains access to the `BuildContext` allowing it to call `context.flutterDeck.router.goToSlide()` to skip to the next slide. If the shortcut doesn't require context, you can simply extend `Action`. +In this example, pressing `Alt + →` triggers the `SkipSlideIntent`. The `SkipSlideAction`, extending `ContextAction`, gains access to the `BuildContext` allowing it to call `context.flutterDeck.router.goToSlide()` to skip to the next slide. If the shortcut doesn't require context, you can simply extend `Action`. ## Avoiding shortcut clashes From cd3c34a8d4246c1fba357f026581ab4d9cd9dda6 Mon Sep 17 00:00:00 2001 From: Mangirdas Kazlauskas Date: Sun, 22 Feb 2026 01:46:07 +0200 Subject: [PATCH 10/14] docs: update example code docs --- packages/flutter_deck/example/lib/main.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/flutter_deck/example/lib/main.dart b/packages/flutter_deck/example/lib/main.dart index 7ea4f1c..36b2974 100644 --- a/packages/flutter_deck/example/lib/main.dart +++ b/packages/flutter_deck/example/lib/main.dart @@ -38,6 +38,7 @@ class FlutterDeckExample extends StatelessWidget { ), ), ), + // Update controls and add custom shortcuts. controls: const FlutterDeckControlsConfiguration( shortcuts: FlutterDeckShortcutsConfiguration(customShortcuts: [SkipSlideShortcut()]), ), From e3d0edcb1bb1acb9e4a60bb2f9d7dd100de2f344 Mon Sep 17 00:00:00 2001 From: Mangirdas Kazlauskas Date: Sun, 22 Feb 2026 01:53:20 +0200 Subject: [PATCH 11/14] refactor: multiple activators --- doc/website/source/guides/custom-shortcuts.md | 4 ++-- .../example/lib/shortcuts/skip_slide_shortcut.dart | 2 +- .../lib/src/controls/flutter_deck_controls_listener.dart | 5 +++-- .../flutter_deck/lib/src/controls/flutter_deck_shortcut.dart | 4 ++-- .../src/controls/flutter_deck_controls_listener_test.dart | 4 ++-- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/doc/website/source/guides/custom-shortcuts.md b/doc/website/source/guides/custom-shortcuts.md index 24d56e2..1fe7525 100644 --- a/doc/website/source/guides/custom-shortcuts.md +++ b/doc/website/source/guides/custom-shortcuts.md @@ -9,7 +9,7 @@ It is possible to define custom shortcuts to control your presentation using the ## Defining custom shortcuts -To add custom shortcuts, you need to configure the `FlutterDeckShortcutsConfiguration` when setting up your `FlutterDeckApp`. This configuration requires a list of custom shortcuts that extend the abstract `FlutterDeckShortcut` class. This class binds a `ShortcutActivator` (like a key press), an `Intent`, and an `Action` together. +To add custom shortcuts, you need to configure the `FlutterDeckShortcutsConfiguration` when setting up your `FlutterDeckApp`. This configuration requires a list of custom shortcuts that extend the abstract `FlutterDeckShortcut` class. This class binds `ShortcutActivator`s (like key presses), an `Intent`, and an `Action` together. ### Example: Skip to the next slide @@ -37,7 +37,7 @@ class SkipSlideShortcut extends FlutterDeckShortcut { const SkipSlideShortcut(); @override - ShortcutActivator get activator => const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true); + Set get activators => const {SingleActivator(LogicalKeyboardKey.arrowRight, alt: true)}; @override SkipSlideIntent get intent => const SkipSlideIntent(); diff --git a/packages/flutter_deck/example/lib/shortcuts/skip_slide_shortcut.dart b/packages/flutter_deck/example/lib/shortcuts/skip_slide_shortcut.dart index c2a0e9e..8f39b5a 100644 --- a/packages/flutter_deck/example/lib/shortcuts/skip_slide_shortcut.dart +++ b/packages/flutter_deck/example/lib/shortcuts/skip_slide_shortcut.dart @@ -23,7 +23,7 @@ class SkipSlideShortcut extends FlutterDeckShortcut { const SkipSlideShortcut(); @override - ShortcutActivator get activator => const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true); + Set get activators => const {SingleActivator(LogicalKeyboardKey.arrowRight, alt: true)}; @override SkipSlideIntent get intent => const SkipSlideIntent(); 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 7de4193..9cd7c5c 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 @@ -107,7 +107,7 @@ class FlutterDeckControlsListener extends StatelessWidget { ...shortcuts.previousSlide, ...shortcuts.toggleMarker, ...shortcuts.toggleNavigationDrawer, - ...shortcuts.customShortcuts.map((e) => e.activator), + for (final shortcut in shortcuts.customShortcuts) ...shortcut.activators, ]; final seen = {}; @@ -139,7 +139,8 @@ class FlutterDeckControlsListener extends StatelessWidget { for (final activator in shortcuts.previousSlide) activator: const GoPreviousIntent(), for (final activator in shortcuts.toggleMarker) activator: const ToggleMarkerIntent(), for (final activator in shortcuts.toggleNavigationDrawer) activator: const ToggleDrawerIntent(), - for (final shortcut in shortcuts.customShortcuts) shortcut.activator: shortcut.intent, + for (final shortcut in shortcuts.customShortcuts) + for (final activator in shortcut.activators) activator: shortcut.intent, }, child: widget, ); diff --git a/packages/flutter_deck/lib/src/controls/flutter_deck_shortcut.dart b/packages/flutter_deck/lib/src/controls/flutter_deck_shortcut.dart index edb80af..ab0b493 100644 --- a/packages/flutter_deck/lib/src/controls/flutter_deck_shortcut.dart +++ b/packages/flutter_deck/lib/src/controls/flutter_deck_shortcut.dart @@ -9,8 +9,8 @@ abstract class FlutterDeckShortcut { /// Creates a shortcut for the slide deck. const FlutterDeckShortcut(); - /// The key combination that will trigger the shortcut. - ShortcutActivator get activator; + /// The key combinations that will trigger the shortcut. + Set get activators; /// The intent that will be invoked when the shortcut is triggered. T get intent; diff --git a/packages/flutter_deck/test/src/controls/flutter_deck_controls_listener_test.dart b/packages/flutter_deck/test/src/controls/flutter_deck_controls_listener_test.dart index 2ce419c..bb1152d 100644 --- a/packages/flutter_deck/test/src/controls/flutter_deck_controls_listener_test.dart +++ b/packages/flutter_deck/test/src/controls/flutter_deck_controls_listener_test.dart @@ -282,7 +282,7 @@ class _MockShortcut extends FlutterDeckShortcut<_MockIntent> { const _MockShortcut(this.action); @override - ShortcutActivator get activator => const SingleActivator(LogicalKeyboardKey.keyA); + Set get activators => const {SingleActivator(LogicalKeyboardKey.keyA)}; @override _MockIntent get intent => const _MockIntent(); @@ -295,7 +295,7 @@ class _MockClashingShortcut extends FlutterDeckShortcut<_MockIntent> { const _MockClashingShortcut(this.action); @override - ShortcutActivator get activator => const SingleActivator(LogicalKeyboardKey.arrowRight); + Set get activators => const {SingleActivator(LogicalKeyboardKey.arrowRight)}; @override _MockIntent get intent => const _MockIntent(); From 5aecbd0ee270f2ce8e23c742148ca4b1ea1292f9 Mon Sep 17 00:00:00 2001 From: Mangirdas Kazlauskas Date: Sun, 22 Feb 2026 01:55:47 +0200 Subject: [PATCH 12/14] refactor: order class members --- .../lib/src/controls/flutter_deck_shortcut.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/flutter_deck/lib/src/controls/flutter_deck_shortcut.dart b/packages/flutter_deck/lib/src/controls/flutter_deck_shortcut.dart index ab0b493..4898a52 100644 --- a/packages/flutter_deck/lib/src/controls/flutter_deck_shortcut.dart +++ b/packages/flutter_deck/lib/src/controls/flutter_deck_shortcut.dart @@ -9,18 +9,18 @@ abstract class FlutterDeckShortcut { /// Creates a shortcut for the slide deck. const FlutterDeckShortcut(); - /// The key combinations that will trigger the shortcut. - Set get activators; - - /// The intent that will be invoked when the shortcut is triggered. - T get intent; - /// The action that will be invoked when the intent is triggered. /// /// To access [FlutterDeck] via this action, you can use a [ContextAction] /// instead of a regular [Action]. Action get action; + /// The key combinations that will trigger the shortcut. + Set get activators; + + /// The intent that will be invoked when the shortcut is triggered. + T get intent; + /// The type of the intent that will be invoked when the shortcut is /// triggered. /// From 96fb64bf955e96d851ef9fc941b3bde14198245f Mon Sep 17 00:00:00 2001 From: Mangirdas Kazlauskas Date: Sun, 22 Feb 2026 02:01:10 +0200 Subject: [PATCH 13/14] docs: guides order --- doc/website/source/guides/code-generation.md | 2 +- doc/website/source/guides/presentation-state.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/website/source/guides/code-generation.md b/doc/website/source/guides/code-generation.md index c2decae..2f45604 100644 --- a/doc/website/source/guides/code-generation.md +++ b/doc/website/source/guides/code-generation.md @@ -1,6 +1,6 @@ --- title: Code generation -navOrder: 10 +navOrder: 12 --- This package comes with a [mason](https://pub.dev/packages/mason) template that can be used to generate a new slide for the slide deck. diff --git a/doc/website/source/guides/presentation-state.md b/doc/website/source/guides/presentation-state.md index beea654..7de340e 100644 --- a/doc/website/source/guides/presentation-state.md +++ b/doc/website/source/guides/presentation-state.md @@ -1,6 +1,6 @@ --- title: Presentation state -navOrder: 9 +navOrder: 11 --- The slide deck state is managed by the `FlutterDeck` widget. This widget is responsible for managing the state of the slide deck, its configuration, navigation, and other details. By using the `FlutterDeck` extensions, you can access the slide deck state and its methods from anywhere in the app: From 24929a80f9dfaaf8e10c533d48150322096c3837 Mon Sep 17 00:00:00 2001 From: Mangirdas Kazlauskas Date: Sun, 22 Feb 2026 02:01:52 +0200 Subject: [PATCH 14/14] docs: playback order --- doc/website/source/playback/change-locale.md | 2 +- doc/website/source/playback/presenter-view.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/website/source/playback/change-locale.md b/doc/website/source/playback/change-locale.md index c5bf7f8..3754fbf 100644 --- a/doc/website/source/playback/change-locale.md +++ b/doc/website/source/playback/change-locale.md @@ -1,6 +1,6 @@ --- title: Change locale -navOrder: 5 +navOrder: 4 --- You can change the locale of the slide deck at runtime. The updated locale will be applied to the whole slide deck. The locale can be changed using the presenter toolbar. diff --git a/doc/website/source/playback/presenter-view.md b/doc/website/source/playback/presenter-view.md index e51832c..35f6516 100644 --- a/doc/website/source/playback/presenter-view.md +++ b/doc/website/source/playback/presenter-view.md @@ -1,6 +1,6 @@ --- title: Presenter view -navOrder: 6 +navOrder: 5 --- The presenter view allows you to control your presentation from a separate screen or (even) device. It displays the current slide, speaker notes, and a timer.