Perché gestire tastiera e menu contestuali
Con l'espansione di Flutter verso desktop (Windows, macOS, Linux) e web, offrire un'esperienza utente completa significa supportare anche input diversi dal tocco. Le scorciatoie da tastiera (Ctrl+S, Ctrl+Z) e i menu contestuali (tasto destro) sono ormai attesi dagli utenti di applicazioni desktop.
Flutter offre un sistema potente e disaccoppiato per gestire tutto questo tramite i widget Shortcuts, Actions e le classi Intent, oltre a MenuAnchor per i menu. Vediamo come metterli insieme.
Il modello Intent-Action
Il cuore del sistema si basa su tre concetti:
- Intent: rappresenta cosa vogliamo fare (un'intenzione), senza specificare come.
- Action: contiene la logica che esegue l'intent.
- Shortcuts: mappa combinazioni di tasti sugli intent.
Questo disaccoppiamento permette di associare la stessa azione a più trigger (tastiera, pulsante, menu).
// Definiamo un intent personalizzato
class SaveIntent extends Intent {
const SaveIntent();
}
class UndoIntent extends Intent {
const UndoIntent();
}
Mappare le scorciatoie
Usiamo il widget Shortcuts per associare combinazioni di tasti agli intent:
Shortcuts(
shortcuts: <ShortcutActivator, Intent>{
SingleActivator(LogicalKeyboardKey.keyS, control: true):
const SaveIntent(),
SingleActivator(LogicalKeyboardKey.keyZ, control: true):
const UndoIntent(),
},
child: Actions(
actions: <Type, Action<Intent>>{
SaveIntent: CallbackAction<SaveIntent>(
onInvoke: (intent) {
debugPrint('Salvataggio in corso...');
return null;
},
),
UndoIntent: CallbackAction<UndoIntent>(
onInvoke: (intent) {
debugPrint('Annulla ultima azione');
return null;
},
),
},
child: const Focus(
autofocus: true,
child: MyEditor(),
),
),
)
Nota importante: le scorciatoie funzionano solo se un widget nella gerarchia ha il focus. Per questo avvolgiamo il contenuto in un
Focusconautofocus: true.
Action riutilizzabili con classi dedicate
Per logica più complessa, conviene creare classi Action dedicate invece di CallbackAction:
class SaveAction extends Action<SaveIntent> {
SaveAction(this.controller);
final DocumentController controller;
@override
Object? invoke(SaveIntent intent) {
controller.save();
return null;
}
@override
bool isEnabled(SaveIntent intent) => controller.hasChanges;
}
Il metodo isEnabled permette di abilitare o disabilitare l'azione dinamicamente: se restituisce false, la scorciatoia non verrà eseguita e i menu collegati appariranno disabilitati.
Menu contestuali con MenuAnchor
Da Flutter 3.7+ è disponibile MenuAnchor, un widget Material 3 per creare menu a comparsa. Colleghiamolo al tasto destro del mouse:
class ContextMenuArea extends StatelessWidget {
const ContextMenuArea({super.key});
@override
Widget build(BuildContext context) {
final controller = MenuController();
return GestureDetector(
onSecondaryTapDown: (details) {
controller.open(position: details.localPosition);
},
child: MenuAnchor(
controller: controller,
menuChildren: [
MenuItemButton(
leadingIcon: const Icon(Icons.save),
shortcut: const SingleActivator(
LogicalKeyboardKey.keyS,
control: true,
),
onPressed: () => Actions.invoke(context, const SaveIntent()),
child: const Text('Salva'),
),
MenuItemButton(
leadingIcon: const Icon(Icons.undo),
onPressed: () => Actions.invoke(context, const UndoIntent()),
child: const Text('Annulla'),
),
],
child: Container(
color: Colors.grey.shade100,
alignment: Alignment.center,
child: const Text('Clic destro qui'),
),
),
);
}
}
Il parametro shortcut di MenuItemButton mostra automaticamente la combinazione di tasti accanto alla voce, migliorando la scopribilità delle funzionalità.
Barra dei menu con MenuBar
Per le app desktop possiamo aggiungere una vera barra dei menu in stile nativo con MenuBar:
MenuBar(
children: [
SubmenuButton(
menuChildren: [
MenuItemButton(
shortcut: const SingleActivator(
LogicalKeyboardKey.keyS,
control: true,
),
onPressed: () => Actions.invoke(context, const SaveIntent()),
child: const Text('Salva'),
),
MenuItemButton(
onPressed: () {},
child: const Text('Esci'),
),
],
child: const Text('File'),
),
],
)
Best practice
- Centralizza le Actions: definisci gli
Intentuna volta e collega ogni trigger (menu, pulsante, scorciatoia) allo stesso intent conActions.invoke. - Rispetta le convenzioni di piattaforma: usa
LogicalKeyboardKey.meta(Cmd) su macOS al posto dicontrol. ConsideraSingleActivator(..., meta: true)per macOS. - Gestisci il focus: le scorciatoie richiedono un
FocusNodeattivo. Verifica sempre che la sezione interattiva possa ricevere il focus. - Sfrutta isEnabled: mantieni coerente lo stato tra menu disabilitati e scorciatoie inattive.
Conclusione
Il trittico Shortcuts, Actions e Intent, combinato con MenuAnchor e MenuBar, permette di costruire esperienze desktop e web professionali in Flutter, con un'architettura pulita e disaccoppiata. Iniziare a integrarli fin dalle prime fasi del progetto rende l'app più accessibile ed efficiente per gli utenti che usano la tastiera.
