Skip to content
2 changes: 1 addition & 1 deletion doc/website/source/guides/code-generation.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
70 changes: 70 additions & 0 deletions doc/website/source/guides/custom-shortcuts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
title: Custom shortcuts
navOrder: 10
---

# Custom shortcuts

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

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

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 SkipSlideIntent extends Intent {
const SkipSlideIntent();
}

class SkipSlideAction extends ContextAction<SkipSlideIntent> {
@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<SkipSlideIntent> {
const SkipSlideShortcut();

@override
Set<ShortcutActivator> get activators => const {SingleActivator(LogicalKeyboardKey.arrowRight, alt: true)};

@override
SkipSlideIntent get intent => const SkipSlideIntent();

@override
Action<SkipSlideIntent> get action => SkipSlideAction();
}
```

Then, add the shortcut to your presentation:

```dart
FlutterDeckApp(
configuration: const FlutterDeckConfiguration(
controls: FlutterDeckControlsConfiguration(
shortcuts: FlutterDeckShortcutsConfiguration(
customShortcuts: [
SkipSlideShortcut(),
],
),
),
),
)
```

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

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.
2 changes: 1 addition & 1 deletion doc/website/source/guides/presentation-state.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
2 changes: 1 addition & 1 deletion doc/website/source/playback/change-locale.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion doc/website/source/playback/presenter-view.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
1 change: 1 addition & 0 deletions packages/flutter_deck/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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<ShortcutActivator>` instead of a `SingleActivator`
- **Migration**: instead of:
Expand Down
5 changes: 5 additions & 0 deletions packages/flutter_deck/example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -37,6 +38,10 @@ class FlutterDeckExample extends StatelessWidget {
),
),
),
// Update controls and add custom shortcuts.
controls: const FlutterDeckControlsConfiguration(
shortcuts: FlutterDeckShortcutsConfiguration(customShortcuts: [SkipSlideShortcut()]),
),
// Set defaults for the footer.
footer: const FlutterDeckFooterConfiguration(showSlideNumbers: true, showSocialHandle: true),
// Set defaults for the header.
Expand Down
1 change: 1 addition & 0 deletions packages/flutter_deck/example/lib/shortcuts/shortcuts.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'skip_slide_shortcut.dart';
Original file line number Diff line number Diff line change
@@ -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<SkipSlideIntent> {
@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<SkipSlideIntent> {
const SkipSlideShortcut();

@override
Set<ShortcutActivator> get activators => const {SingleActivator(LogicalKeyboardKey.arrowRight, alt: true)};

@override
SkipSlideIntent get intent => const SkipSlideIntent();

@override
Action<SkipSlideIntent> get action => SkipSlideAction();
}
1 change: 1 addition & 0 deletions packages/flutter_deck/lib/flutter_deck.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions packages/flutter_deck/lib/src/controls/controls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_deck/src/controls/flutter_deck_shortcut.dart';

/// The configuration for the slide deck controls.
class FlutterDeckControlsConfiguration {
Expand All @@ -18,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(),
Expand Down Expand Up @@ -82,12 +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 [],
});

/// Creates a configuration for the slide deck keyboard shortcuts where they
Expand All @@ -108,4 +114,10 @@ class FlutterDeckShortcutsConfiguration {

/// The key combinations to use for toggling the navigation drawer.
final Set<ShortcutActivator> toggleNavigationDrawer;

/// Custom shortcuts for the slide deck.
///
/// This can be used to add custom shortcuts and their corresponding actions
/// to the slide deck.
final List<FlutterDeckShortcut> customShortcuts;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -100,13 +102,32 @@ class FlutterDeckControlsListener extends StatelessWidget {

final shortcuts = controls.shortcuts;

final allShortcuts = [
...shortcuts.nextSlide,
...shortcuts.previousSlide,
...shortcuts.toggleMarker,
...shortcuts.toggleNavigationDrawer,
for (final shortcut in shortcuts.customShortcuts) ...shortcut.activators,
];

final seen = <ShortcutActivator>{};

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(
actions: <Type, Action<Intent>>{
GoNextIntent: GoNextAction(controlsNotifier),
GoPreviousIntent: GoPreviousAction(controlsNotifier),
ToggleDrawerIntent: ToggleDrawerAction(controlsNotifier),
ToggleMarkerIntent: ToggleMarkerAction(controlsNotifier),
for (final shortcut in shortcuts.customShortcuts) shortcut.intentType: shortcut.action,
},
child: widget,
);
Expand All @@ -118,6 +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)
for (final activator in shortcut.activators) activator: shortcut.intent,
},
child: widget,
);
Expand Down
30 changes: 30 additions & 0 deletions packages/flutter_deck/lib/src/controls/flutter_deck_shortcut.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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].
abstract class FlutterDeckShortcut<T extends Intent> {
/// Creates a shortcut for the slide deck.
const FlutterDeckShortcut();

/// 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<T> get action;

/// The key combinations that will trigger the shortcut.
Set<ShortcutActivator> 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.
///
/// 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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,14 @@ 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);
});

test('disabled factory should create disabled configuration', () {
const configuration = FlutterDeckShortcutsConfiguration.disabled();

expect(configuration.enabled, false);
expect(configuration.customShortcuts, isEmpty);
});
});
}
Loading