shortcut_atlas 0.1.0
shortcut_atlas: ^0.1.0 copied to clipboard
Enterprise keyboard shortcuts for Flutter desktop: bind keys to actions and reveal a contextual cheat-sheet HUD by holding Alt.
example/lib/main.dart
import 'dart:convert';
import 'dart:ui' show ImageFilter;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:shortcut_atlas/shortcut_atlas.dart';
void main() => runApp(const DemoApp());
/// Which built-in HUD preset the demo is showing off.
enum HudStyle { glass, minimal, accent }
/// Persists user re-bindings to `shared_preferences` as a single JSON blob.
/// This is all it takes to make [Keymap] durable — encode/decode is handled by
/// the package's [ActivatorCodec] under the hood.
class SharedPrefsKeymapStore implements KeymapStore {
static const _key = 'shortcut_atlas.keymap';
@override
Future<Map<String, dynamic>> load() async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(_key);
if (raw == null || raw.isEmpty) return const {};
return (jsonDecode(raw) as Map).cast<String, dynamic>();
}
@override
Future<void> save(Map<String, dynamic> data) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_key, jsonEncode(data));
}
}
class DemoApp extends StatefulWidget {
const DemoApp({super.key});
@override
State<DemoApp> createState() => _DemoAppState();
}
class _DemoAppState extends State<DemoApp> {
static const List<(String, Color)> swatches = [
('Indigo', Color(0xFF5C6BC0)),
('Violet', Color(0xFF7C4DFF)),
('Blue', Color(0xFF2196F3)),
('Teal', Color(0xFF009688)),
('Green', Color(0xFF43A047)),
('Amber', Color(0xFFFFB300)),
('Orange', Color(0xFFFB8C00)),
('Rose', Color(0xFFEC407A)),
];
Color _seed = swatches.first.$2;
ThemeMode _mode = ThemeMode.dark;
HudStyle _hudStyle = HudStyle.glass;
// One store for the whole app; re-bindings survive restarts.
final SharedPrefsKeymapStore _keymapStore = SharedPrefsKeymapStore();
/// Maps the selected preset to a HUD theme. `glass` returns null so the
/// package's theme-derived default is used.
AtlasThemeData? _hudTheme(BuildContext context) => switch (_hudStyle) {
HudStyle.glass => null,
HudStyle.minimal => AtlasThemeData.minimal(context),
HudStyle.accent => AtlasThemeData.accent(context),
};
ThemeData _theme(Brightness brightness) => ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: _seed, brightness: brightness),
useMaterial3: true,
);
void _toggleDark() => setState(
() => _mode = _mode == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark);
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'shortcut_atlas demo',
theme: _theme(Brightness.light),
darkTheme: _theme(Brightness.dark),
themeMode: _mode,
// Install the shortcut system once, above the app's Navigator. The HUD
// inherits the accent automatically from the ambient theme. Passing a
// keymapStore makes user re-bindings persistent.
builder: (context, child) => ShortcutAtlas(
theme: _hudTheme(context),
config: AtlasConfig(keymapStore: _keymapStore),
child: child!,
),
home: HomeScreen(
swatches: swatches,
seed: _seed,
isDark: _mode == ThemeMode.dark,
hudStyle: _hudStyle,
onSeedChanged: (c) => setState(() => _seed = c),
onToggleDark: _toggleDark,
onHudStyleChanged: (s) => setState(() => _hudStyle = s),
),
);
}
}
/// One demo command: a key chord wired to a behavior, surfaced as a card, a HUD
/// row, and a palette entry from a single declaration.
class _Demo {
const _Demo(this.id, this.label, this.description, this.icon, this.activator,
this.group, this.onInvoke);
final String id;
final String label;
final String description;
final IconData icon;
final ShortcutActivator activator;
final ShortcutGroup group;
final VoidCallback onInvoke;
}
class HomeScreen extends StatefulWidget {
const HomeScreen({
super.key,
required this.swatches,
required this.seed,
required this.isDark,
required this.hudStyle,
required this.onSeedChanged,
required this.onToggleDark,
required this.onHudStyleChanged,
});
final List<(String, Color)> swatches;
final Color seed;
final bool isDark;
final HudStyle hudStyle;
final ValueChanged<Color> onSeedChanged;
final VoidCallback onToggleDark;
final ValueChanged<HudStyle> onHudStyleChanged;
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final List<String> _docs = ['Document 1', 'Document 2', 'Document 3'];
final FocusNode _searchFocus = FocusNode();
int _active = 0;
double _zoom = 1.0;
String _status = 'Hold Alt to reveal every shortcut. Press Ctrl/Cmd+K for the palette.';
void _say(String s) => setState(() => _status = s);
void _openTab(int i) {
if (i < _docs.length) {
setState(() => _active = i);
_say('Switched to ${_docs[i]}');
}
}
void _newTab() {
setState(() {
_docs.add('Document ${_docs.length + 1}');
_active = _docs.length - 1;
});
_say('Opened ${_docs[_active]}');
}
void _closeTab() {
if (_docs.length <= 1) {
_say('Cannot close the last document');
return;
}
setState(() {
final removed = _docs.removeAt(_active);
_active = _active.clamp(0, _docs.length - 1);
_say('Closed $removed');
});
}
Future<void> _newWindow() async {
_say('Opened a new window');
await Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const DocumentWindow()),
);
}
void _save() => _say('Saved ${_docs[_active]}');
void _find() {
_searchFocus.requestFocus();
_say('Find — focus moved to the search box');
}
void _zoomBy(double delta) {
setState(() => _zoom = (_zoom + delta).clamp(0.5, 2.0));
_say('Zoom ${(_zoom * 100).round()}%');
}
void _resetZoom() {
setState(() => _zoom = 1.0);
_say('Zoom reset to 100%');
}
void _openPalette() => ShortcutAtlas.of(context).openPalette();
@override
void dispose() {
_searchFocus.dispose();
super.dispose();
}
List<_Demo> get _demos => [
_Demo('file.newWindow', 'New Window', 'Open another editor window',
Icons.open_in_new_rounded, primary(LogicalKeyboardKey.keyN),
ShortcutGroup.file, _newWindow),
_Demo('file.newTab', 'New Tab', 'Add a document tab',
Icons.add_box_outlined, primary(LogicalKeyboardKey.keyT),
ShortcutGroup.tabs, _newTab),
_Demo('file.close', 'Close Tab', 'Close the active document',
Icons.close_rounded, primary(LogicalKeyboardKey.keyW),
ShortcutGroup.tabs, _closeTab),
_Demo('file.save', 'Save', 'Save the active document',
Icons.save_outlined, primary(LogicalKeyboardKey.keyS),
ShortcutGroup.file, _save),
_Demo('edit.find', 'Find', 'Focus the search box',
Icons.search_rounded, primary(LogicalKeyboardKey.keyF),
ShortcutGroup.edit, _find),
_Demo('view.zoomIn', 'Zoom In', 'Increase the zoom level',
Icons.zoom_in_rounded, primary(LogicalKeyboardKey.equal),
ShortcutGroup.view, () => _zoomBy(0.1)),
_Demo('view.zoomOut', 'Zoom Out', 'Decrease the zoom level',
Icons.zoom_out_rounded, primary(LogicalKeyboardKey.minus),
ShortcutGroup.view, () => _zoomBy(-0.1)),
_Demo('view.zoomReset', 'Reset Zoom', 'Back to 100%',
Icons.restart_alt_rounded, primary(LogicalKeyboardKey.digit0),
ShortcutGroup.view, _resetZoom),
_Demo('view.toggleTheme', 'Toggle Theme', 'Switch light / dark',
Icons.brightness_6_rounded, primary(LogicalKeyboardKey.keyD),
ShortcutGroup.view, widget.onToggleDark),
_Demo('view.palette', 'Command Palette', 'Search every command',
Icons.bolt_rounded, primary(LogicalKeyboardKey.keyK),
ShortcutGroup.general, _openPalette),
];
/// The single source of truth: every shortcut is declared once here and then
/// drives dispatch, the HUD, the palette, the menu bar, and the re-bind panel.
List<ShortcutCommand> _buildCommands(List<_Demo> demos) => [
for (final d in demos)
if (d.id != 'view.palette') // the palette opener is built in
command(d.id,
activator: d.activator,
label: d.label,
description: d.description,
group: d.group,
icon: d.icon,
onInvoke: d.onInvoke),
for (var i = 0; i < _docs.length && i < 9; i++)
command('tabs.open.$i',
activator: CharacterActivator('${i + 1}'),
label: 'Open ${_docs[i]}',
group: ShortcutGroup.tabs,
icon: Icons.tab_rounded,
onInvoke: () => _openTab(i)),
];
@override
Widget build(BuildContext context) {
final demos = _demos;
final commands = _buildCommands(demos);
return ShortcutScope(
kind: ScopeKind.screen,
autofocus: true,
commands: commands,
child: _Shell(demos: demos, commands: commands, state: this),
);
}
}
class _Shell extends StatelessWidget {
const _Shell({required this.demos, required this.commands, required this.state});
final List<_Demo> demos;
final List<ShortcutCommand> commands;
final _HomeScreenState state;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final dark = Theme.of(context).brightness == Brightness.dark;
final gradient = dark
? [
Color.alphaBlend(cs.primary.withValues(alpha: 0.30), cs.surface),
Color.alphaBlend(cs.tertiary.withValues(alpha: 0.26), cs.surface),
]
: [cs.primary, cs.tertiary];
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: gradient,
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: ClipRRect(
borderRadius: BorderRadius.circular(28),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
child: Container(
decoration: BoxDecoration(
color: cs.surface.withValues(alpha: 0.82),
borderRadius: BorderRadius.circular(28),
border:
Border.all(color: cs.outlineVariant.withValues(alpha: 0.6)),
),
padding: const EdgeInsets.all(28),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_AppMenuBar(commands: commands),
const SizedBox(height: 18),
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_Header(),
const SizedBox(height: 24),
_ThemeControls(state: state),
const SizedBox(height: 24),
_Documents(state: state),
const SizedBox(height: 24),
_SearchBox(state: state),
const SizedBox(height: 24),
_CardGrid(demos: demos),
const SizedBox(height: 22),
_HelpCard(),
],
),
),
),
const SizedBox(height: 16),
_StatusCard(status: state._status),
],
),
),
),
),
),
),
),
);
}
}
class _Header extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Row(
children: [
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: cs.primaryContainer,
borderRadius: BorderRadius.circular(18),
),
child: Icon(Icons.keyboard_command_key_rounded,
color: cs.onPrimaryContainer, size: 30),
),
const SizedBox(width: 18),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('shortcut_atlas',
style: Theme.of(context)
.textTheme
.headlineSmall
?.copyWith(fontWeight: FontWeight.w700)),
const SizedBox(height: 2),
Text(
'Keyboard shortcuts + a hold-Alt HUD for desktop. '
'Detected: ${AtlasPlatform.name} · primary modifier '
'${AtlasPlatform.primaryModifierLabel}.',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: cs.onSurfaceVariant),
),
],
),
),
],
);
}
}
class _ThemeControls extends StatelessWidget {
const _ThemeControls({required this.state});
final _HomeScreenState state;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final w = state.widget;
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Accent color',
style: Theme.of(context)
.textTheme
.labelMedium
?.copyWith(color: cs.onSurfaceVariant)),
const SizedBox(height: 10),
Wrap(
spacing: 10,
runSpacing: 10,
children: [
for (final (name, color) in w.swatches)
_SwatchDot(
name: name,
color: color,
selected: w.seed.toARGB32() == color.toARGB32(),
onTap: () => w.onSeedChanged(color),
),
],
),
],
),
),
const SizedBox(width: 20),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
SegmentedButton<bool>(
showSelectedIcon: false,
segments: const [
ButtonSegment(
value: false,
icon: Icon(Icons.light_mode_outlined, size: 18),
label: Text('Light')),
ButtonSegment(
value: true,
icon: Icon(Icons.dark_mode_outlined, size: 18),
label: Text('Dark')),
],
selected: {w.isDark},
onSelectionChanged: (_) => w.onToggleDark(),
),
const SizedBox(height: 10),
SegmentedButton<HudStyle>(
showSelectedIcon: false,
segments: const [
ButtonSegment(value: HudStyle.glass, label: Text('Glass')),
ButtonSegment(value: HudStyle.minimal, label: Text('Minimal')),
ButtonSegment(value: HudStyle.accent, label: Text('Accent')),
],
selected: {w.hudStyle},
onSelectionChanged: (s) => w.onHudStyleChanged(s.first),
),
],
),
],
);
}
}
class _Documents extends StatelessWidget {
const _Documents({required this.state});
final _HomeScreenState state;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text('Documents',
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.w600)),
const SizedBox(width: 10),
Text('press 1 – ${state._docs.length} to switch',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: cs.onSurfaceVariant)),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 10,
runSpacing: 10,
children: [
for (var i = 0; i < state._docs.length; i++)
ChoiceChip(
label: Text('${i + 1} · ${state._docs[i]}'),
selected: state._active == i,
onSelected: (_) => state._openTab(i),
),
],
),
const SizedBox(height: 8),
Text('Zoom ${(state._zoom * 100).round()}%',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: cs.onSurfaceVariant)),
],
);
}
}
class _SearchBox extends StatelessWidget {
const _SearchBox({required this.state});
final _HomeScreenState state;
@override
Widget build(BuildContext context) {
return TextField(
focusNode: state._searchFocus,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
labelText: 'Search (try typing 1, 2, 3 — bare shortcuts are guarded here)',
),
);
}
}
class _CardGrid extends StatelessWidget {
const _CardGrid({required this.demos});
final List<_Demo> demos;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('Shortcuts',
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
Text('Click a card, press its keys, or hold Alt to see them all.',
style: Theme.of(context).textTheme.bodySmall),
const SizedBox(height: 14),
LayoutBuilder(
builder: (context, constraints) {
final cols = (constraints.maxWidth / 300).floor().clamp(1, 3);
final cellWidth = (constraints.maxWidth - (cols - 1) * 14) / cols;
return Wrap(
spacing: 14,
runSpacing: 14,
children: [
for (final d in demos)
_ShortcutCard(demo: d, width: cellWidth),
],
);
},
),
],
);
}
}
class _ShortcutCard extends StatelessWidget {
const _ShortcutCard({required this.demo, required this.width});
final _Demo demo;
final double width;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final glyphs = GlyphSet.of(context).glyphsFor(demo.activator);
return SizedBox(
width: width,
child: Material(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: demo.onInvoke,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: cs.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Icon(demo.icon, color: cs.onPrimaryContainer, size: 22),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(demo.label,
style: const TextStyle(
fontSize: 15.5, fontWeight: FontWeight.w600)),
const SizedBox(height: 2),
Text(demo.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12, color: cs.onSurfaceVariant)),
const SizedBox(height: 8),
Row(
children: [
for (final g in glyphs) ...[
_KbdChip(g),
const SizedBox(width: 4),
],
],
),
],
),
),
],
),
),
),
),
);
}
}
class _KbdChip extends StatelessWidget {
const _KbdChip(this.glyph);
final String glyph;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Container(
constraints: const BoxConstraints(minWidth: 22, minHeight: 22),
padding: const EdgeInsets.symmetric(horizontal: 6),
alignment: Alignment.center,
decoration: BoxDecoration(
color: cs.surface,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: cs.outlineVariant),
),
child: Text(glyph,
style: TextStyle(
fontSize: 12, fontWeight: FontWeight.w600, color: cs.onSurface)),
);
}
}
class _SwatchDot extends StatelessWidget {
const _SwatchDot({
required this.name,
required this.color,
required this.selected,
required this.onTap,
});
final String name;
final Color color;
final bool selected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Tooltip(
message: name,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(20),
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
width: 32,
height: 32,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: selected ? cs.onSurface : Colors.transparent,
width: 2.5,
),
),
child: selected
? const Icon(Icons.check, size: 16, color: Colors.white)
: null,
),
),
);
}
}
class _HelpCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
const tips = [
'Hold Alt to reveal a contextual cheat-sheet of every active shortcut',
'Press Ctrl/Cmd+K to open the searchable command palette',
'Use the File / Edit / View menu bar — same commands, same chords',
'Click the tune icon (top-right) to remap any shortcut; it persists',
'Switch the HUD between Glass / Minimal / Accent presets above',
'Type 1/2/3 in the search box: bare shortcuts are auto-disabled while editing',
'Ctrl/Cmd+N opens a new window with its own scoped shortcuts',
];
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: cs.surfaceContainerLowest.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.outlineVariant),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.lightbulb_outline, color: cs.primary, size: 20),
const SizedBox(width: 8),
Text('Things to try',
style: Theme.of(context)
.textTheme
.titleSmall
?.copyWith(fontWeight: FontWeight.w700)),
],
),
const SizedBox(height: 14),
for (final tip in tips)
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 1),
child: Icon(Icons.check_circle_outline,
size: 17, color: cs.primary),
),
const SizedBox(width: 10),
Expanded(
child: Text(tip,
style: Theme.of(context).textTheme.bodyMedium)),
],
),
),
],
),
);
}
}
class _StatusCard extends StatelessWidget {
const _StatusCard({required this.status});
final String status;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(14),
),
child: Row(
children: [
Icon(Icons.bolt_rounded, size: 18, color: cs.onSurfaceVariant),
const SizedBox(width: 10),
Expanded(
child: Text(status,
style: const TextStyle(fontFamily: 'monospace', fontSize: 13)),
),
],
),
);
}
}
/// Feature 2 — a native-style menu bar generated from the same command list.
/// Each item shows the command's *effective* chord and runs the same behavior
/// the key would; the hint is display-only, so it never double-fires.
class _AppMenuBar extends StatelessWidget {
const _AppMenuBar({required this.commands});
final List<ShortcutCommand> commands;
ShortcutCommand? _byId(String id) {
for (final c in commands) {
if (c.id == id) return c;
}
return null;
}
ShortcutMenu _menu(String label, List<String> ids) {
final cmds = <ShortcutCommand>[];
for (final id in ids) {
final c = _byId(id);
if (c != null) cmds.add(c);
}
return ShortcutMenu(label, cmds);
}
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: ShortcutMenuBar(
menus: [
_menu('File', const [
'file.newWindow',
'file.newTab',
'file.close',
'file.save',
]),
_menu('Edit', const ['edit.find']),
_menu('View', const [
'view.zoomIn',
'view.zoomOut',
'view.zoomReset',
'view.toggleTheme',
]),
],
),
),
IconButton(
tooltip: 'Customize shortcuts',
icon: const Icon(Icons.tune_rounded),
onPressed: () => showDialog<void>(
context: context,
builder: (_) => _ShortcutSettingsDialog(commands: commands),
),
),
],
);
}
}
/// Feature 1 — a re-bind panel. Click a chip, press a new chord, and the change
/// flows everywhere (dispatch, HUD, palette, menu) and is persisted by the
/// app's [KeymapStore].
class _ShortcutSettingsDialog extends StatelessWidget {
const _ShortcutSettingsDialog({required this.commands});
final List<ShortcutCommand> commands;
@override
Widget build(BuildContext context) {
final controller = ShortcutAtlas.of(context);
final keymap = controller.keymap;
final rebindable = [
for (final c in commands)
if (!c.id.startsWith('tabs.open')) c,
];
return AlertDialog(
title: Row(
children: const [
Icon(Icons.tune_rounded),
SizedBox(width: 10),
Text('Customize Shortcuts'),
],
),
content: SizedBox(
width: 480,
child: ListenableBuilder(
listenable: keymap.revision,
builder: (context, _) => ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 440),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final c in rebindable)
Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: [
Icon(c.icon, size: 20),
const SizedBox(width: 12),
Expanded(child: Text(c.label)),
KeyRecorder(
key: ValueKey('${c.id}.${keymap.revision.value}'),
initial: controller.effectiveActivator(c),
onRecorded: (a) => keymap.rebind(c.id, a),
),
IconButton(
tooltip: 'Reset to default',
icon: const Icon(Icons.undo_rounded, size: 18),
onPressed: keymap.hasUserOverride(c.id)
? () => keymap.reset(c.id)
: null,
),
],
),
),
],
),
),
),
),
),
actions: [
TextButton(
onPressed: keymap.resetAll,
child: const Text('Reset all'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Done'),
),
],
);
}
}
/// A second "window" pushed by Ctrl/Cmd+N. It declares its own [ShortcutScope],
/// so while it is focused its shortcuts take precedence — a live demo of scope
/// nesting. (Real OS windows need a multi-window plugin; this is a route.)
class DocumentWindow extends StatelessWidget {
const DocumentWindow({super.key});
@override
Widget build(BuildContext context) {
return ShortcutScope(
kind: ScopeKind.screen,
autofocus: true,
commands: [
command('window.close', activator: primary(LogicalKeyboardKey.keyW),
label: 'Close Window', group: ShortcutGroup.file,
icon: Icons.close_rounded,
onInvoke: () => Navigator.of(context).maybePop()),
],
child: Scaffold(
appBar: AppBar(title: const Text('New Window')),
body: const Center(
child: Padding(
padding: EdgeInsets.all(32),
child: Text(
'This window has its own ShortcutScope.\n\n'
'Hold Alt to see its shortcuts, or press Ctrl/Cmd+W to close it.',
textAlign: TextAlign.center,
),
),
),
),
);
}
}