From 85b11b2bc209fee6869884b5e36f448eb3dc2dff Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Tue, 2 Dec 2025 20:44:22 +0100 Subject: [PATCH] Add custom keybindings support --- app/lib/actions/background.dart | 23 +- app/lib/actions/change_path.dart | 17 +- app/lib/actions/change_tool.dart | 35 ++- app/lib/actions/color_palette.dart | 12 +- app/lib/actions/exit.dart | 18 +- app/lib/actions/export.dart | 32 ++- app/lib/actions/full_screen.dart | 16 +- app/lib/actions/hide_ui.dart | 18 +- app/lib/actions/image_export.dart | 27 +- app/lib/actions/new.dart | 29 ++- app/lib/actions/next.dart | 18 +- app/lib/actions/packs.dart | 25 +- app/lib/actions/paste.dart | 23 +- app/lib/actions/pdf_export.dart | 23 +- app/lib/actions/previous.dart | 18 +- app/lib/actions/redo.dart | 21 +- app/lib/actions/save.dart | 21 +- app/lib/actions/select.dart | 25 +- app/lib/actions/settings.dart | 22 +- app/lib/actions/shortcuts.dart | 78 ++++++ app/lib/actions/svg_export.dart | 29 ++- app/lib/actions/undo.dart | 21 +- app/lib/actions/zoom.dart | 29 ++- app/lib/handlers/handler.dart | 5 +- app/lib/l10n/app_en.arb | 3 +- app/lib/main.dart | 2 + app/lib/services/keybinder_store.dart | 18 ++ app/lib/settings/inputs/keyboard.dart | 146 +++++++++-- app/lib/views/app_bar.dart | 28 +-- app/lib/views/main.dart | 341 +++++++------------------- app/lib/widgets/search.dart | 11 + app/pubspec.lock | 11 +- app/pubspec.yaml | 5 + 33 files changed, 737 insertions(+), 413 deletions(-) create mode 100644 app/lib/actions/shortcuts.dart create mode 100644 app/lib/services/keybinder_store.dart diff --git a/app/lib/actions/background.dart b/app/lib/actions/background.dart index c61d2b000a33..9b360fce5f4f 100644 --- a/app/lib/actions/background.dart +++ b/app/lib/actions/background.dart @@ -1,23 +1,34 @@ import 'package:butterfly/bloc/document_bloc.dart'; import 'package:butterfly/dialogs/background/dialog.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:keybinder/keybinder.dart'; class BackgroundIntent extends Intent { - final BuildContext context; - - const BackgroundIntent(this.context); + const BackgroundIntent(); } +final backgroundShortcut = ShortcutDefinition( + id: 'background', + intent: const BackgroundIntent(), + defaultActivator: const SingleActivator( + LogicalKeyboardKey.keyB, + control: true, + ), +); + class BackgroundAction extends Action { - BackgroundAction(); + final BuildContext context; + + BackgroundAction(this.context); @override Future invoke(BackgroundIntent intent) { return showDialog( - context: intent.context, + context: context, builder: (context) => BlocProvider.value( - value: intent.context.read(), + value: this.context.read(), child: const BackgroundDialog(), ), ); diff --git a/app/lib/actions/change_path.dart b/app/lib/actions/change_path.dart index 6f01f3391c4b..25fa6420b081 100644 --- a/app/lib/actions/change_path.dart +++ b/app/lib/actions/change_path.dart @@ -2,22 +2,29 @@ import 'package:butterfly/api/file_system.dart'; import 'package:butterfly/bloc/document_bloc.dart'; import 'package:butterfly/dialogs/file_system/move.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:keybinder/keybinder.dart'; import '../cubits/settings.dart'; class ChangePathIntent extends Intent { - final BuildContext context; - - const ChangePathIntent(this.context); + const ChangePathIntent(); } +final changePathShortcut = ShortcutDefinition( + id: 'change_path', + intent: const ChangePathIntent(), + defaultActivator: const SingleActivator(LogicalKeyboardKey.keyS, alt: true), +); + class ChangePathAction extends Action { - ChangePathAction(); + final BuildContext context; + + ChangePathAction(this.context); @override Future invoke(ChangePathIntent intent) async { - final context = intent.context; final bloc = context.read(); final state = bloc.state; if (state is! DocumentLoadSuccess || state.location.path == '') return; diff --git a/app/lib/actions/change_tool.dart b/app/lib/actions/change_tool.dart index fa656d0b11a0..5d868c279f4c 100644 --- a/app/lib/actions/change_tool.dart +++ b/app/lib/actions/change_tool.dart @@ -1,25 +1,48 @@ import 'package:butterfly/cubits/current_index.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:keybinder/keybinder.dart'; import '../bloc/document_bloc.dart'; class ChangeToolIntent extends Intent { - final BuildContext context; final int index; - const ChangeToolIntent(this.context, this.index); + const ChangeToolIntent(this.index); } +final changeToolShortcuts = List.generate(10, (index) { + final key = [ + LogicalKeyboardKey.digit1, + LogicalKeyboardKey.digit2, + LogicalKeyboardKey.digit3, + LogicalKeyboardKey.digit4, + LogicalKeyboardKey.digit5, + LogicalKeyboardKey.digit6, + LogicalKeyboardKey.digit7, + LogicalKeyboardKey.digit8, + LogicalKeyboardKey.digit9, + LogicalKeyboardKey.digit0, + ][index]; + return ShortcutDefinition( + id: 'tool_$index', + intent: ChangeToolIntent(index), + defaultActivator: SingleActivator(key, control: true), + ); +}); + class ChangeToolAction extends Action { - ChangeToolAction(); + final BuildContext context; + + ChangeToolAction(this.context); @override Future invoke(ChangeToolIntent intent) async { - final bloc = intent.context.read(); - intent.context.read().changeTool( + final bloc = context.read(); + context.read().changeTool( bloc, - context: intent.context, + context: context, index: intent.index, ); } diff --git a/app/lib/actions/color_palette.dart b/app/lib/actions/color_palette.dart index ebc0552e4d9a..d1b51b08ccaf 100644 --- a/app/lib/actions/color_palette.dart +++ b/app/lib/actions/color_palette.dart @@ -4,21 +4,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class ColorPaletteIntent extends Intent { - final BuildContext context; - - const ColorPaletteIntent(this.context); + const ColorPaletteIntent(); } class ColorPaletteAction extends Action { - ColorPaletteAction(); + final BuildContext context; + + ColorPaletteAction(this.context); @override Future invoke(ColorPaletteIntent intent) { return showDialog( - context: intent.context, + context: context, builder: (ctx) => ColorPalettePickerDialog( viewMode: true, - bloc: intent.context.read(), + bloc: context.read(), ), ); } diff --git a/app/lib/actions/exit.dart b/app/lib/actions/exit.dart index 7daf13001725..f08ba62aecec 100644 --- a/app/lib/actions/exit.dart +++ b/app/lib/actions/exit.dart @@ -1,20 +1,28 @@ import 'package:butterfly/bloc/document_bloc.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:keybinder/keybinder.dart'; class ExitIntent extends Intent { - final BuildContext context; - - const ExitIntent(this.context); + const ExitIntent(); } +final exitShortcut = ShortcutDefinition( + id: 'exit', + intent: const ExitIntent(), + defaultActivator: const SingleActivator(LogicalKeyboardKey.escape), +); + class ExitAction extends Action { - ExitAction(); + final BuildContext context; + + ExitAction(this.context); @override void invoke(ExitIntent intent) { - final bloc = intent.context.read(); + final bloc = context.read(); bloc.add(const PresentationModeExited()); } } diff --git a/app/lib/actions/export.dart b/app/lib/actions/export.dart index 1f791f688e61..b6a3bc9dff2f 100644 --- a/app/lib/actions/export.dart +++ b/app/lib/actions/export.dart @@ -1,24 +1,46 @@ import 'package:butterfly/api/save.dart'; import 'package:butterfly/bloc/document_bloc.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:keybinder/keybinder.dart'; class ExportIntent extends Intent { - final BuildContext context; final bool isText; - const ExportIntent(this.context, {this.isText = false}); + const ExportIntent({this.isText = false}); } +final exportShortcut = ShortcutDefinition( + id: 'export', + intent: const ExportIntent(), + defaultActivator: const SingleActivator( + LogicalKeyboardKey.keyE, + control: true, + ), +); + +final exportTextShortcut = ShortcutDefinition( + id: 'export_text', + intent: const ExportIntent(isText: true), + defaultActivator: const SingleActivator( + LogicalKeyboardKey.keyE, + control: true, + shift: true, + ), +); + class ExportAction extends Action { - ExportAction(); + final BuildContext context; + + ExportAction(this.context); @override Future invoke(ExportIntent intent) async { - final bloc = intent.context.read(); + final bloc = context.read(); final state = bloc.state; if (state is! DocumentLoaded) return; final data = (await state.saveData()); - exportData(intent.context, data, isTextBased: intent.isText); + exportData(context, data, isTextBased: intent.isText); } } diff --git a/app/lib/actions/full_screen.dart b/app/lib/actions/full_screen.dart index a2d11b0c7540..81a1b3f8864f 100644 --- a/app/lib/actions/full_screen.dart +++ b/app/lib/actions/full_screen.dart @@ -1,14 +1,22 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:keybinder/keybinder.dart'; import 'package:material_leap/material_leap.dart'; class FullScreenIntent extends Intent { - final BuildContext context; - - const FullScreenIntent(this.context); + const FullScreenIntent(); } +final fullScreenShortcut = ShortcutDefinition( + id: 'full_screen', + intent: const FullScreenIntent(), + defaultActivator: const SingleActivator(LogicalKeyboardKey.f11), +); + class FullScreenAction extends Action { - FullScreenAction(); + final BuildContext context; + + FullScreenAction(this.context); @override Future invoke(FullScreenIntent intent) async { diff --git a/app/lib/actions/hide_ui.dart b/app/lib/actions/hide_ui.dart index a25255cc442a..821b891a80b5 100644 --- a/app/lib/actions/hide_ui.dart +++ b/app/lib/actions/hide_ui.dart @@ -1,18 +1,26 @@ import 'package:butterfly/cubits/current_index.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:keybinder/keybinder.dart'; class HideUIIntent extends Intent { - final BuildContext context; - - const HideUIIntent(this.context); + const HideUIIntent(); } +final hideUIShortcut = ShortcutDefinition( + id: 'hide_ui', + intent: const HideUIIntent(), + defaultActivator: const SingleActivator(LogicalKeyboardKey.f12), +); + class HideUIAction extends Action { - HideUIAction(); + final BuildContext context; + + HideUIAction(this.context); @override void invoke(HideUIIntent intent) { - intent.context.read().toggleKeyboardHideUI(); + context.read().toggleKeyboardHideUI(); } } diff --git a/app/lib/actions/image_export.dart b/app/lib/actions/image_export.dart index 597a7d53a2f7..744f75e750bb 100644 --- a/app/lib/actions/image_export.dart +++ b/app/lib/actions/image_export.dart @@ -2,21 +2,34 @@ import 'package:butterfly/bloc/document_bloc.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/dialogs/export/general.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:keybinder/keybinder.dart'; class ImageExportIntent extends Intent { - final BuildContext context; - - const ImageExportIntent(this.context); + const ImageExportIntent(); } +final imageExportShortcut = ShortcutDefinition( + id: 'image_export', + intent: const ImageExportIntent(), + defaultActivator: const SingleActivator( + LogicalKeyboardKey.keyE, + control: true, + alt: true, + shift: true, + ), +); + class ImageExportAction extends Action { - ImageExportAction(); + final BuildContext context; + + ImageExportAction(this.context); @override Future invoke(ImageExportIntent intent) async { - var bloc = intent.context.read(); - var transform = intent.context.read().state; + var bloc = context.read(); + var transform = context.read().state; return showDialog( builder: (context) => BlocProvider.value( value: bloc, @@ -25,7 +38,7 @@ class ImageExportAction extends Action { options: getDefaultImageExportOptions(context, transform: transform), ), ), - context: intent.context, + context: context, ); } } diff --git a/app/lib/actions/new.dart b/app/lib/actions/new.dart index 55942bf2829f..654297af674c 100644 --- a/app/lib/actions/new.dart +++ b/app/lib/actions/new.dart @@ -3,24 +3,45 @@ import 'package:butterfly/bloc/document_bloc.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import 'package:keybinder/keybinder.dart'; import '../dialogs/template.dart'; class NewIntent extends Intent { - final BuildContext context; final bool fromTemplate; - const NewIntent(this.context, {this.fromTemplate = false}); + const NewIntent({this.fromTemplate = false}); } +final newShortcut = ShortcutDefinition( + id: 'new', + intent: const NewIntent(fromTemplate: false), + defaultActivator: const SingleActivator( + LogicalKeyboardKey.keyN, + control: true, + ), +); + +final newFromTemplateShortcut = ShortcutDefinition( + id: 'new_from_template', + intent: const NewIntent(fromTemplate: true), + defaultActivator: const SingleActivator( + LogicalKeyboardKey.keyN, + control: true, + shift: true, + ), +); + class NewAction extends Action { - NewAction(); + final BuildContext context; + + NewAction(this.context); @override Future invoke(NewIntent intent) async { - final context = intent.context; final bloc = context.read(); final settingsCubit = context.read(); final settings = settingsCubit.state; diff --git a/app/lib/actions/next.dart b/app/lib/actions/next.dart index 2f79e09c5a41..2637ccf8c564 100644 --- a/app/lib/actions/next.dart +++ b/app/lib/actions/next.dart @@ -1,19 +1,27 @@ import 'package:butterfly/bloc/document_bloc.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:keybinder/keybinder.dart'; class NextIntent extends Intent { - final BuildContext context; - - const NextIntent(this.context); + const NextIntent(); } +final nextShortcut = ShortcutDefinition( + id: 'next', + intent: const NextIntent(), + defaultActivator: const SingleActivator(LogicalKeyboardKey.arrowRight), +); + class NextAction extends Action { - NextAction(); + final BuildContext context; + + NextAction(this.context); @override void invoke(NextIntent intent) { - final bloc = intent.context.read(); + final bloc = context.read(); final state = bloc.state; if (state is! DocumentPresentationState) return; state.handler.play(bloc); diff --git a/app/lib/actions/packs.dart b/app/lib/actions/packs.dart index 04eb041aefe1..3b89dfd3b082 100644 --- a/app/lib/actions/packs.dart +++ b/app/lib/actions/packs.dart @@ -1,26 +1,39 @@ import 'package:butterfly/bloc/document_bloc.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:keybinder/keybinder.dart'; import '../dialogs/packs/dialog.dart'; class PacksIntent extends Intent { final bool showDocument; - final BuildContext context; - const PacksIntent(this.context, [this.showDocument = true]); + const PacksIntent({this.showDocument = true}); } +final packsShortcut = ShortcutDefinition( + id: 'packs', + intent: const PacksIntent(), + defaultActivator: const SingleActivator( + LogicalKeyboardKey.keyP, + control: true, + alt: true, + ), +); + class PacksAction extends Action { - PacksAction(); + final BuildContext context; + + PacksAction(this.context); @override Future invoke(PacksIntent intent) { - var bloc = intent.context.read(); - var transformCubit = intent.context.read(); + var bloc = context.read(); + var transformCubit = context.read(); return showDialog( - context: intent.context, + context: context, builder: (context) => MultiBlocProvider( providers: [ BlocProvider.value(value: transformCubit), diff --git a/app/lib/actions/paste.dart b/app/lib/actions/paste.dart index 985e3efc038a..8128f89dd751 100644 --- a/app/lib/actions/paste.dart +++ b/app/lib/actions/paste.dart @@ -1,24 +1,35 @@ import 'package:butterfly/bloc/document_bloc.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:keybinder/keybinder.dart'; import '../services/import.dart'; class PasteIntent extends Intent { - final BuildContext context; - - const PasteIntent(this.context); + const PasteIntent(); } +final pasteShortcut = ShortcutDefinition( + id: 'paste', + intent: const PasteIntent(), + defaultActivator: const SingleActivator( + LogicalKeyboardKey.keyV, + control: true, + ), +); + class PasteAction extends Action { - PasteAction(); + final BuildContext context; + + PasteAction(this.context); @override Future invoke(PasteIntent intent) async { - final bloc = intent.context.read(); + final bloc = context.read(); final state = bloc.state; if (state is! DocumentLoadSuccess) return; - final importService = intent.context.read(); + final importService = context.read(); try { importService.importClipboard(state.data).then((e) => e?.submit()); } catch (_) {} diff --git a/app/lib/actions/pdf_export.dart b/app/lib/actions/pdf_export.dart index d61f374dc27f..7b429f1f22e1 100644 --- a/app/lib/actions/pdf_export.dart +++ b/app/lib/actions/pdf_export.dart @@ -1,29 +1,40 @@ import 'package:butterfly/bloc/document_bloc.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:keybinder/keybinder.dart'; import '../dialogs/export/pdf.dart'; class PdfExportIntent extends Intent { - final BuildContext context; - - const PdfExportIntent(this.context); + const PdfExportIntent(); } +final pdfExportShortcut = ShortcutDefinition( + id: 'pdf_export', + intent: const PdfExportIntent(), + defaultActivator: const SingleActivator( + LogicalKeyboardKey.keyP, + control: true, + shift: true, + ), +); + class PdfExportAction extends Action { - PdfExportAction(); + final BuildContext context; + + PdfExportAction(this.context); @override Future invoke(PdfExportIntent intent) async { - final context = intent.context; final bloc = context.read(); final state = bloc.state; if (state is! DocumentLoadSuccess) return; var areas = [state.areaPreset]; if (state.info.exportPresets.isNotEmpty) { final preset = await showDialog( - context: intent.context, + context: context, builder: (context) => BlocProvider.value(value: bloc, child: const ExportPresetsDialog()), ); diff --git a/app/lib/actions/previous.dart b/app/lib/actions/previous.dart index f554759f211f..c63043eb26c4 100644 --- a/app/lib/actions/previous.dart +++ b/app/lib/actions/previous.dart @@ -1,19 +1,27 @@ import 'package:butterfly/bloc/document_bloc.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:keybinder/keybinder.dart'; class PreviousIntent extends Intent { - final BuildContext context; - - const PreviousIntent(this.context); + const PreviousIntent(); } +final previousShortcut = ShortcutDefinition( + id: 'previous', + intent: const PreviousIntent(), + defaultActivator: const SingleActivator(LogicalKeyboardKey.arrowLeft), +); + class PreviousAction extends Action { - PreviousAction(); + final BuildContext context; + + PreviousAction(this.context); @override void invoke(PreviousIntent intent) { - final bloc = intent.context.read(); + final bloc = context.read(); final state = bloc.state; if (state is! DocumentPresentationState) return; state.handler.playReverse(bloc); diff --git a/app/lib/actions/redo.dart b/app/lib/actions/redo.dart index 4ea6a541436e..8b6a855f7a24 100644 --- a/app/lib/actions/redo.dart +++ b/app/lib/actions/redo.dart @@ -1,20 +1,31 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:keybinder/keybinder.dart'; import '../bloc/document_bloc.dart'; class RedoIntent extends Intent { - final BuildContext context; - - const RedoIntent(this.context); + const RedoIntent(); } +final redoShortcut = ShortcutDefinition( + id: 'redo', + intent: const RedoIntent(), + defaultActivator: const SingleActivator( + LogicalKeyboardKey.keyY, + control: true, + ), +); + class RedoAction extends Action { - RedoAction(); + final BuildContext context; + + RedoAction(this.context); @override Future invoke(RedoIntent intent) async { - final bloc = intent.context.read(); + final bloc = context.read(); bloc.sendRedo(); bloc.reload(); } diff --git a/app/lib/actions/save.dart b/app/lib/actions/save.dart index 9922f2e4da23..aca4445c0202 100644 --- a/app/lib/actions/save.dart +++ b/app/lib/actions/save.dart @@ -1,22 +1,33 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:keybinder/keybinder.dart'; import '../bloc/document_bloc.dart'; import '../cubits/current_index.dart'; import '../embed/action.dart'; class SaveIntent extends Intent { - final BuildContext context; - - const SaveIntent(this.context); + const SaveIntent(); } +final saveShortcut = ShortcutDefinition( + id: 'save', + intent: const SaveIntent(), + defaultActivator: const SingleActivator( + LogicalKeyboardKey.keyS, + control: true, + ), +); + class SaveAction extends Action { - SaveAction(); + final BuildContext context; + + SaveAction(this.context); @override Future invoke(SaveIntent intent) async { - final bloc = intent.context.read(); + final bloc = context.read(); final state = bloc.state; if (state is! DocumentLoadSuccess) return; if (state.embedding?.save ?? false) { diff --git a/app/lib/actions/select.dart b/app/lib/actions/select.dart index 4165eb8ec8e8..b7313eb237eb 100644 --- a/app/lib/actions/select.dart +++ b/app/lib/actions/select.dart @@ -3,26 +3,37 @@ import 'package:butterfly/handlers/handler.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:butterfly_api/butterfly_models.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:keybinder/keybinder.dart'; import '../cubits/current_index.dart'; class SelectAllIntent extends Intent { - final BuildContext context; - - const SelectAllIntent(this.context); + const SelectAllIntent(); } +final selectAllShortcut = ShortcutDefinition( + id: 'select_all', + intent: const SelectAllIntent(), + defaultActivator: const SingleActivator( + LogicalKeyboardKey.keyA, + control: true, + ), +); + class SelectAllAction extends Action { - SelectAllAction(); + final BuildContext context; + + SelectAllAction(this.context); @override Future invoke(SelectAllIntent intent) async { - final cubit = intent.context.read(); + final cubit = context.read(); if (cubit.getHandler() is SelectHandler) return; - final bloc = intent.context.read(); + final bloc = context.read(); final handler = await cubit.changeTemporaryHandler( - intent.context, + context, SelectTool(), bloc: bloc, temporaryState: TemporaryState.removeAfterClick, diff --git a/app/lib/actions/settings.dart b/app/lib/actions/settings.dart index 305cf99d62b0..33892434e4df 100644 --- a/app/lib/actions/settings.dart +++ b/app/lib/actions/settings.dart @@ -2,18 +2,30 @@ import 'dart:ui'; import 'package:butterfly/settings/home.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:keybinder/keybinder.dart'; class SettingsIntent extends Intent { - final BuildContext context; - - const SettingsIntent(this.context); + const SettingsIntent(); } +final settingsShortcut = ShortcutDefinition( + id: 'settings', + intent: const SettingsIntent(), + defaultActivator: const SingleActivator( + LogicalKeyboardKey.keyS, + control: true, + alt: true, + ), +); + class SettingsAction extends Action { - SettingsAction(); + final BuildContext context; + + SettingsAction(this.context); @override - Future invoke(SettingsIntent intent) => openSettings(intent.context); + Future invoke(SettingsIntent intent) => openSettings(context); } Future openSettings(BuildContext context) => showGeneralDialog( diff --git a/app/lib/actions/shortcuts.dart b/app/lib/actions/shortcuts.dart new file mode 100644 index 000000000000..8660a6a768fa --- /dev/null +++ b/app/lib/actions/shortcuts.dart @@ -0,0 +1,78 @@ +import 'package:keybinder/keybinder.dart'; +import 'package:butterfly/actions/background.dart'; +import 'package:butterfly/actions/change_path.dart'; +import 'package:butterfly/actions/change_tool.dart'; +import 'package:butterfly/actions/exit.dart'; +import 'package:butterfly/actions/export.dart'; +import 'package:butterfly/actions/full_screen.dart'; +import 'package:butterfly/actions/hide_ui.dart'; +import 'package:butterfly/actions/image_export.dart'; +import 'package:butterfly/actions/new.dart'; +import 'package:butterfly/actions/next.dart'; +import 'package:butterfly/actions/packs.dart'; +import 'package:butterfly/actions/paste.dart'; +import 'package:butterfly/actions/pdf_export.dart'; +import 'package:butterfly/actions/previous.dart'; +import 'package:butterfly/actions/redo.dart'; +import 'package:butterfly/actions/save.dart'; +import 'package:butterfly/actions/select.dart'; +import 'package:butterfly/actions/settings.dart'; +import 'package:butterfly/actions/svg_export.dart'; +import 'package:butterfly/actions/undo.dart'; +import 'package:butterfly/actions/zoom.dart'; +import 'package:butterfly/widgets/search.dart'; +import 'package:butterfly/services/keybinder_store.dart'; + +export 'package:butterfly/actions/background.dart'; +export 'package:butterfly/actions/change_path.dart'; +export 'package:butterfly/actions/change_tool.dart'; +export 'package:butterfly/actions/exit.dart'; +export 'package:butterfly/actions/export.dart'; +export 'package:butterfly/actions/full_screen.dart'; +export 'package:butterfly/actions/hide_ui.dart'; +export 'package:butterfly/actions/image_export.dart'; +export 'package:butterfly/actions/new.dart'; +export 'package:butterfly/actions/next.dart'; +export 'package:butterfly/actions/packs.dart'; +export 'package:butterfly/actions/paste.dart'; +export 'package:butterfly/actions/pdf_export.dart'; +export 'package:butterfly/actions/previous.dart'; +export 'package:butterfly/actions/redo.dart'; +export 'package:butterfly/actions/save.dart'; +export 'package:butterfly/actions/select.dart'; +export 'package:butterfly/actions/settings.dart'; +export 'package:butterfly/actions/svg_export.dart'; +export 'package:butterfly/actions/undo.dart'; +export 'package:butterfly/actions/zoom.dart'; +export 'package:butterfly/widgets/search.dart'; + +final keybinder = Keybinder( + store: SharedPreferencesKeybinderStore(), + definitions: [ + undoShortcut, + redoShortcut, + newShortcut, + newFromTemplateShortcut, + backgroundShortcut, + exitShortcut, + fullScreenShortcut, + hideUIShortcut, + nextShortcut, + previousShortcut, + selectAllShortcut, + searchShortcut, + exportShortcut, + exportTextShortcut, + imageExportShortcut, + pdfExportShortcut, + svgExportShortcut, + settingsShortcut, + changePathShortcut, + saveShortcut, + packsShortcut, + zoomInShortcut, + zoomOutShortcut, + pasteShortcut, + ...changeToolShortcuts, + ], +); diff --git a/app/lib/actions/svg_export.dart b/app/lib/actions/svg_export.dart index 7eb7b7e81dc8..d0c1d300f959 100644 --- a/app/lib/actions/svg_export.dart +++ b/app/lib/actions/svg_export.dart @@ -1,23 +1,34 @@ import 'package:butterfly/bloc/document_bloc.dart'; +import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/dialogs/export/general.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../cubits/transform.dart'; +import 'package:keybinder/keybinder.dart'; class SvgExportIntent extends Intent { - final BuildContext context; - - const SvgExportIntent(this.context); + const SvgExportIntent(); } +final svgExportShortcut = ShortcutDefinition( + id: 'svg_export', + intent: const SvgExportIntent(), + defaultActivator: const SingleActivator( + LogicalKeyboardKey.keyE, + control: true, + alt: true, + ), +); + class SvgExportAction extends Action { - SvgExportAction(); + final BuildContext context; + + SvgExportAction(this.context); @override Future invoke(SvgExportIntent intent) async { - final bloc = intent.context.read(); - final transform = intent.context.read().state; + final bloc = context.read(); + final transform = context.read().state; showDialog( builder: (context) => BlocProvider.value( value: bloc, @@ -26,7 +37,7 @@ class SvgExportAction extends Action { options: getDefaultSvgExportOptions(context, transform: transform), ), ), - context: intent.context, + context: context, ); } } diff --git a/app/lib/actions/undo.dart b/app/lib/actions/undo.dart index 89d512b87019..e15ff4e1d13f 100644 --- a/app/lib/actions/undo.dart +++ b/app/lib/actions/undo.dart @@ -1,20 +1,31 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:keybinder/keybinder.dart'; import '../bloc/document_bloc.dart'; class UndoIntent extends Intent { - final BuildContext context; - - const UndoIntent(this.context); + const UndoIntent(); } +final undoShortcut = ShortcutDefinition( + id: 'undo', + intent: const UndoIntent(), + defaultActivator: const SingleActivator( + LogicalKeyboardKey.keyZ, + control: true, + ), +); + class UndoAction extends Action { - UndoAction(); + final BuildContext context; + + UndoAction(this.context); @override Future invoke(UndoIntent intent) async { - final bloc = intent.context.read(); + final bloc = context.read(); bloc.sendUndo(); bloc.reload(); } diff --git a/app/lib/actions/zoom.dart b/app/lib/actions/zoom.dart index c10910162289..8a5d7b40160f 100644 --- a/app/lib/actions/zoom.dart +++ b/app/lib/actions/zoom.dart @@ -1,20 +1,41 @@ import 'package:butterfly/cubits/current_index.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:keybinder/keybinder.dart'; class ZoomIntent extends Intent { - final BuildContext context; final bool reverse; - const ZoomIntent(this.context, [this.reverse = false]); + const ZoomIntent({this.reverse = false}); } +final zoomInShortcut = ShortcutDefinition( + id: 'zoom_in', + intent: const ZoomIntent(reverse: false), + defaultActivator: const SingleActivator( + LogicalKeyboardKey.add, + control: true, + ), +); + +final zoomOutShortcut = ShortcutDefinition( + id: 'zoom_out', + intent: const ZoomIntent(reverse: true), + defaultActivator: const SingleActivator( + LogicalKeyboardKey.minus, + control: true, + ), +); + class ZoomAction extends Action { - ZoomAction(); + final BuildContext context; + + ZoomAction(this.context); @override void invoke(ZoomIntent intent) { - final currentIndex = intent.context.read().state; + final currentIndex = context.read().state; final viewport = currentIndex.cameraViewport; final center = Offset( (viewport.width ?? 0) / 2, diff --git a/app/lib/handlers/handler.dart b/app/lib/handlers/handler.dart index 5f1247207668..f894ff319657 100644 --- a/app/lib/handlers/handler.dart +++ b/app/lib/handlers/handler.dart @@ -301,11 +301,10 @@ abstract class Handler { @mustCallSuper Map> getActions(BuildContext context) => { PasteTextIntent: CallbackAction( - onInvoke: (intent) => Actions.maybeInvoke(context, PasteIntent(context)), + onInvoke: (intent) => Actions.maybeInvoke(context, PasteIntent()), ), SelectAllTextIntent: CallbackAction( - onInvoke: (intent) => - Actions.maybeInvoke(context, SelectAllIntent(context)), + onInvoke: (intent) => Actions.maybeInvoke(context, SelectAllIntent()), ), }; diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 70926c2a3376..aa83afe32a58 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -1260,5 +1260,6 @@ "densityMaximize": "Maximize", "@densityMaximize": { "description": "Density value" - } + }, + "customizeShortcuts": "Customize shortcuts" } diff --git a/app/lib/main.dart b/app/lib/main.dart index e49ccb526c32..5bbb562d5edb 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -23,6 +23,7 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; import 'package:flutter_localized_locales/flutter_localized_locales.dart'; +import 'package:keybinder/keybinder.dart'; import 'cubits/settings.dart'; import 'embed/embedding.dart'; @@ -382,6 +383,7 @@ class ButterflyApp extends StatelessWidget { ...AppLocalizations.localizationsDelegates, LocaleNamesLocalizationsDelegate(), LeapLocalizations.delegate, + KeybinderLocalizations.delegate, ], builder: (context, child) { if (!state.nativeTitleBar) { diff --git a/app/lib/services/keybinder_store.dart b/app/lib/services/keybinder_store.dart new file mode 100644 index 000000000000..b4b57bee5bfa --- /dev/null +++ b/app/lib/services/keybinder_store.dart @@ -0,0 +1,18 @@ +import 'package:keybinder/keybinder.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class SharedPreferencesKeybinderStore implements KeybinderStore { + static const String _key = 'keybinder_shortcuts'; + + @override + Future load() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(_key); + } + + @override + Future save(String data) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_key, data); + } +} diff --git a/app/lib/settings/inputs/keyboard.dart b/app/lib/settings/inputs/keyboard.dart index 5c138cedbd70..f29ca3680297 100644 --- a/app/lib/settings/inputs/keyboard.dart +++ b/app/lib/settings/inputs/keyboard.dart @@ -1,3 +1,4 @@ +import 'package:butterfly/actions/shortcuts.dart'; import 'package:butterfly/api/open.dart'; import 'package:butterfly/theme.dart'; import 'package:flutter/material.dart'; @@ -5,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; import 'package:material_leap/material_leap.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; +import 'package:keybinder/keybinder.dart'; import '../../cubits/settings.dart'; @@ -13,36 +15,142 @@ class KeyboardInputSettings extends StatelessWidget { @override Widget build(BuildContext context) { + final generalShortcuts = [ + (newShortcut, AppLocalizations.of(context).newNote), + (newFromTemplateShortcut, 'New from template'), + (exportShortcut, AppLocalizations.of(context).export), + (exportTextShortcut, 'Export as text'), + (imageExportShortcut, 'Export as image'), + (pdfExportShortcut, 'Export as PDF'), + (svgExportShortcut, 'Export as SVG'), + (packsShortcut, AppLocalizations.of(context).packs), + (settingsShortcut, AppLocalizations.of(context).settings), + (exitShortcut, AppLocalizations.of(context).exit), + ]; + + final projectShortcuts = [ + (searchShortcut, AppLocalizations.of(context).search), + (undoShortcut, AppLocalizations.of(context).undo), + (redoShortcut, AppLocalizations.of(context).redo), + (backgroundShortcut, AppLocalizations.of(context).background), + (saveShortcut, AppLocalizations.of(context).save), + (changePathShortcut, 'Change path'), + (zoomInShortcut, AppLocalizations.of(context).zoomIn), + (zoomOutShortcut, AppLocalizations.of(context).zoomOut), + (fullScreenShortcut, 'Full screen'), + (hideUIShortcut, AppLocalizations.of(context).hideUI), + (nextShortcut, AppLocalizations.of(context).nextSlide), + (previousShortcut, AppLocalizations.of(context).previousSlide), + (selectAllShortcut, AppLocalizations.of(context).selectAll), + (pasteShortcut, AppLocalizations.of(context).paste), + ...changeToolShortcuts.map( + (e) => (e, 'Tool ${int.parse(e.id.split('_').last) + 1}'), + ), + ]; + return Scaffold( appBar: WindowTitleBar( title: Text(AppLocalizations.of(context).keyboard), ), - body: Align( - alignment: Alignment.center, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: LeapBreakpoints.compact), - child: BlocBuilder( - builder: (context, state) => ListView( - children: [ - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: ListTile( - title: Text(AppLocalizations.of(context).shortcuts), - leading: const PhosphorIcon(PhosphorIconsLight.keyboard), - onTap: () => openHelp(['shortcuts'], 'keyboard'), - trailing: const PhosphorIcon( - PhosphorIconsLight.arrowSquareOut, + body: SingleChildScrollView( + child: Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 1000), + child: BlocBuilder( + builder: (context, state) => ListenableBuilder( + listenable: keybinder, + builder: (context, _) => Column( + children: [ + Card( + margin: settingsCardMargin, + child: Padding( + padding: settingsCardPadding, + child: ListTile( + title: Text(AppLocalizations.of(context).shortcuts), + leading: const PhosphorIcon( + PhosphorIconsLight.keyboard, + ), + onTap: () => openHelp(['shortcuts'], 'keyboard'), + trailing: const PhosphorIcon( + PhosphorIconsLight.arrowSquareOut, + ), + ), ), ), - ), + const SizedBox(height: 16), + _buildSection( + context, + AppLocalizations.of(context).general, + generalShortcuts, + ), + const SizedBox(height: 16), + _buildSection(context, 'Project', projectShortcuts), + const SizedBox(height: 16), + ], ), - ], + ), ), ), ), ), ); } + + Widget _buildSection( + BuildContext context, + String title, + List<(ShortcutDefinition, String)> shortcuts, + ) { + return Card( + margin: settingsCardMargin, + child: Padding( + padding: settingsCardPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: settingsCardTitlePadding, + child: Text(title, style: TextTheme.of(context).headlineSmall), + ), + const SizedBox(height: 16), + LayoutBuilder( + builder: (context, constraints) { + final width = constraints.maxWidth; + final columns = width > 600 ? 2 : 1; + final itemWidth = width / columns; + + return Wrap( + children: shortcuts + .map( + (e) => SizedBox( + width: itemWidth, + child: _buildShortcutTile(context, e.$1, e.$2), + ), + ) + .toList(), + ); + }, + ), + ], + ), + ), + ); + } + + Widget _buildShortcutTile( + BuildContext context, + ShortcutDefinition def, + String title, + ) { + return KeyRecorderListTile( + title: Text(title), + currentActivator: keybinder.getActivator(def.id), + onNewKey: (newKey) => keybinder.updateBinding(def.id, newKey), + // Only set reset if it has not default + onReset: keybinder.getActivator(def.id) != def.defaultActivator + ? () => keybinder.resetBinding(def.id) + : null, + ); + } } diff --git a/app/lib/views/app_bar.dart b/app/lib/views/app_bar.dart index 9411f2db9e1b..e2920e8757e2 100644 --- a/app/lib/views/app_bar.dart +++ b/app/lib/views/app_bar.dart @@ -388,10 +388,7 @@ class _AppBarTitleState extends State<_AppBarTitle> { icon: icon, tooltip: tooltip, onPressed: () { - Actions.maybeInvoke( - context, - SaveIntent(context), - ); + Actions.maybeInvoke(context, SaveIntent()); }, ); }, @@ -420,7 +417,7 @@ class _AppBarTitleState extends State<_AppBarTitle> { onPressed: () { Actions.maybeInvoke( context, - ChangePathIntent(context), + ChangePathIntent(), ); }, tooltip: AppLocalizations.of(context).changeDocumentPath, @@ -510,7 +507,7 @@ class _MainPopupMenu extends StatelessWidget { onPressed: () { Actions.maybeInvoke( context, - BackgroundIntent(context), + BackgroundIntent(), ); }, child: Text(AppLocalizations.of(context).background), @@ -526,7 +523,7 @@ class _MainPopupMenu extends StatelessWidget { onPressed: () async { Actions.maybeInvoke( context, - ExportIntent(context), + ExportIntent(), ); }, child: Text(AppLocalizations.of(context).packagedFile), @@ -544,7 +541,7 @@ class _MainPopupMenu extends StatelessWidget { onPressed: () async { Actions.maybeInvoke( context, - ExportIntent(context, isText: true), + ExportIntent(isText: true), ); }, child: Text(AppLocalizations.of(context).rawFile), @@ -562,7 +559,7 @@ class _MainPopupMenu extends StatelessWidget { onPressed: () async { Actions.maybeInvoke( context, - SvgExportIntent(context), + SvgExportIntent(), ); }, child: Text(AppLocalizations.of(context).svg), @@ -581,7 +578,7 @@ class _MainPopupMenu extends StatelessWidget { onPressed: () { Actions.maybeInvoke( context, - ImageExportIntent(context), + ImageExportIntent(), ); }, child: Text(AppLocalizations.of(context).image), @@ -599,7 +596,7 @@ class _MainPopupMenu extends StatelessWidget { onPressed: () { Actions.maybeInvoke( context, - PdfExportIntent(context), + PdfExportIntent(), ); }, child: Text(AppLocalizations.of(context).pdf), @@ -623,10 +620,7 @@ class _MainPopupMenu extends StatelessWidget { alt: true, ), onPressed: () { - Actions.maybeInvoke( - context, - PacksIntent(context), - ); + Actions.maybeInvoke(context, PacksIntent()); }, child: Text(AppLocalizations.of(context).packs), ), @@ -641,7 +635,7 @@ class _MainPopupMenu extends StatelessWidget { control: true, ), onPressed: () { - Actions.maybeInvoke(context, NewIntent(context)); + Actions.maybeInvoke(context, NewIntent()); }, child: Text(AppLocalizations.of(context).newContent), ), @@ -658,7 +652,7 @@ class _MainPopupMenu extends StatelessWidget { onPressed: () { Actions.maybeInvoke( context, - NewIntent(context, fromTemplate: true), + NewIntent(fromTemplate: true), ); }, child: Text(AppLocalizations.of(context).templates), diff --git a/app/lib/views/main.dart b/app/lib/views/main.dart index cdf13b0ced7c..f43e0a67465b 100644 --- a/app/lib/views/main.dart +++ b/app/lib/views/main.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:ui'; +import 'package:butterfly/actions/shortcuts.dart'; import 'package:butterfly/api/close.dart'; import 'package:butterfly/api/file_system.dart'; import 'package:butterfly/bloc/document_bloc.dart'; @@ -19,11 +20,9 @@ import 'package:butterfly/views/toolbar/view.dart'; import 'package:butterfly/views/edit.dart'; import 'package:butterfly/views/error.dart'; import 'package:butterfly/views/property.dart'; -import 'package:butterfly/widgets/search.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; import 'package:go_router/go_router.dart'; @@ -31,28 +30,7 @@ import 'package:lw_file_system/lw_file_system.dart'; import 'package:material_leap/material_leap.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; -import '../actions/background.dart'; -import '../actions/change_tool.dart'; -import '../actions/change_path.dart'; import '../actions/color_palette.dart'; -import '../actions/exit.dart'; -import '../actions/export.dart'; -import '../actions/full_screen.dart'; -import '../actions/hide_ui.dart'; -import '../actions/image_export.dart'; -import '../actions/new.dart'; -import '../actions/next.dart'; -import '../actions/packs.dart'; -import '../actions/paste.dart'; -import '../actions/pdf_export.dart'; -import '../actions/previous.dart'; -import '../actions/redo.dart'; -import '../actions/save.dart'; -import '../actions/select.dart'; -import '../actions/settings.dart'; -import '../actions/svg_export.dart'; -import '../actions/undo.dart'; -import '../actions/zoom.dart'; import '../models/viewport.dart'; import '../services/asset.dart'; import '../api/changes.dart'; @@ -91,30 +69,6 @@ class _ProjectPageState extends State { final SearchController _searchController = SearchController(); late final CloseSubscription _closeSubscription; final GlobalKey _viewportKey = GlobalKey(); - final _actions = >{ - UndoIntent: UndoAction(), - RedoIntent: RedoAction(), - NewIntent: NewAction(), - SvgExportIntent: SvgExportAction(), - ImageExportIntent: ImageExportAction(), - PdfExportIntent: PdfExportAction(), - ExportIntent: ExportAction(), - SettingsIntent: SettingsAction(), - ColorPaletteIntent: ColorPaletteAction(), - BackgroundIntent: BackgroundAction(), - ChangePathIntent: ChangePathAction(), - SaveIntent: SaveAction(), - ChangeToolIntent: ChangeToolAction(), - PacksIntent: PacksAction(), - ExitIntent: ExitAction(), - FullScreenIntent: FullScreenAction(), - HideUIIntent: HideUIAction(), - NextIntent: NextAction(), - PreviousIntent: PreviousAction(), - PasteIntent: PasteAction(), - SelectAllIntent: SelectAllAction(), - ZoomIntent: ZoomAction(), - }; @override void initState() { @@ -380,213 +334,98 @@ class _ProjectPageState extends State { previous.toolbarSize != current.toolbarSize || previous.isInline != current.isInline, builder: (context, settings) { - return Actions( - actions: { - ..._actions, - SearchIntent: CallbackAction( - onInvoke: (_) { - if (_searchController.isOpen) { - _searchController.closeView(null); - return null; - } - _searchController.openView(); + final actions = >{ + UndoIntent: UndoAction(context), + RedoIntent: RedoAction(context), + NewIntent: NewAction(context), + SvgExportIntent: SvgExportAction(context), + ImageExportIntent: ImageExportAction(context), + PdfExportIntent: PdfExportAction(context), + ExportIntent: ExportAction(context), + SettingsIntent: SettingsAction(context), + ColorPaletteIntent: ColorPaletteAction(context), + BackgroundIntent: BackgroundAction(context), + ChangePathIntent: ChangePathAction(context), + SaveIntent: SaveAction(context), + ChangeToolIntent: ChangeToolAction(context), + PacksIntent: PacksAction(context), + ExitIntent: ExitAction(context), + FullScreenIntent: FullScreenAction(context), + HideUIIntent: HideUIAction(context), + NextIntent: NextAction(context), + PreviousIntent: PreviousAction(context), + PasteIntent: PasteAction(context), + SelectAllIntent: SelectAllAction(context), + ZoomIntent: ZoomAction(context), + SearchIntent: CallbackAction( + onInvoke: (_) { + if (_searchController.isOpen) { + _searchController.closeView(null); return null; - }, - ), - }, - child: Shortcuts( - shortcuts: { - LogicalKeySet( - LogicalKeyboardKey.control, - LogicalKeyboardKey.keyZ, - ): UndoIntent( - context, - ), - LogicalKeySet( - LogicalKeyboardKey.control, - LogicalKeyboardKey.keyY, - ): RedoIntent( - context, - ), - LogicalKeySet( - LogicalKeyboardKey.control, - LogicalKeyboardKey.keyN, - ): NewIntent( - context, - fromTemplate: false, - ), - LogicalKeySet( - LogicalKeyboardKey.control, - LogicalKeyboardKey.shift, - LogicalKeyboardKey.keyN, - ): NewIntent( - context, - fromTemplate: true, - ), - LogicalKeySet( - LogicalKeyboardKey.control, - LogicalKeyboardKey.keyB, - ): BackgroundIntent( - context, - ), - LogicalKeySet(LogicalKeyboardKey.escape): - ExitIntent(context), - LogicalKeySet(LogicalKeyboardKey.f11): - FullScreenIntent(context), - LogicalKeySet(LogicalKeyboardKey.f12): - HideUIIntent(context), - LogicalKeySet( - LogicalKeyboardKey.arrowRight, - ): NextIntent( - context, - ), - LogicalKeySet(LogicalKeyboardKey.arrowLeft): - PreviousIntent(context), - LogicalKeySet( - LogicalKeyboardKey.control, - LogicalKeyboardKey.keyA, - ): SelectAllIntent( - context, - ), - LogicalKeySet( - LogicalKeyboardKey.control, - LogicalKeyboardKey.keyK, - ): SearchIntent(), - if (widget.embedding == null) ...{ - LogicalKeySet( - LogicalKeyboardKey.control, - LogicalKeyboardKey.keyE, - ): ExportIntent( - context, - ), - LogicalKeySet( - LogicalKeyboardKey.control, - LogicalKeyboardKey.shift, - LogicalKeyboardKey.keyE, - ): ExportIntent( - context, - isText: true, - ), - LogicalKeySet( - LogicalKeyboardKey.control, - LogicalKeyboardKey.alt, - LogicalKeyboardKey.shift, - LogicalKeyboardKey.keyE, - ): ImageExportIntent( - context, - ), - LogicalKeySet( - LogicalKeyboardKey.control, - LogicalKeyboardKey.shift, - LogicalKeyboardKey.keyP, - ): PdfExportIntent( - context, - ), - LogicalKeySet( - LogicalKeyboardKey.control, - LogicalKeyboardKey.alt, - LogicalKeyboardKey.keyE, - ): SvgExportIntent( - context, - ), - LogicalKeySet( - LogicalKeyboardKey.control, - LogicalKeyboardKey.alt, - LogicalKeyboardKey.keyS, - ): SettingsIntent( - context, - ), - LogicalKeySet( - LogicalKeyboardKey.alt, - LogicalKeyboardKey.keyS, - ): ChangePathIntent( - context, - ), - LogicalKeySet( - LogicalKeyboardKey.control, - LogicalKeyboardKey.keyS, - ): SaveIntent( - context, - ), - LogicalKeySet( - LogicalKeyboardKey.control, - LogicalKeyboardKey.alt, - LogicalKeyboardKey.keyP, - ): PacksIntent( - context, - ), - LogicalKeySet( - LogicalKeyboardKey.control, - LogicalKeyboardKey.add, - ): ZoomIntent( - context, - ), - LogicalKeySet( - LogicalKeyboardKey.control, - LogicalKeyboardKey.minus, - ): ZoomIntent( - context, - true, - ), - ...[ - LogicalKeyboardKey.digit1, - LogicalKeyboardKey.digit2, - LogicalKeyboardKey.digit3, - LogicalKeyboardKey.digit4, - LogicalKeyboardKey.digit5, - LogicalKeyboardKey.digit6, - LogicalKeyboardKey.digit7, - LogicalKeyboardKey.digit8, - LogicalKeyboardKey.digit9, - LogicalKeyboardKey.digit0, - ].asMap().map( - (k, v) => MapEntry( - LogicalKeySet( - LogicalKeyboardKey.control, - v, - ), - ChangeToolIntent(context, k), - ), - ), - }, + } + _searchController.openView(); + return null; }, - child: ClipRect( - child: Focus( - autofocus: true, - skipTraversal: true, - onFocusChange: (_) => false, - child: Scaffold( - appBar: - state - is DocumentPresentationState || - windowState.fullScreen || - currentIndex.hideUi != - HideState.visible - ? null - : PadAppBar( - viewportKey: _viewportKey, - size: settings.toolbarSize, - searchController: - _searchController, - padding: padding, - direction: Directionality.of( - context, - ), - inView: - state - .embedding - ?.isInternal ?? - false, - showTools: - settings.isInline && - state.embedding?.editable != - false, + ), + }; + + return ListenableBuilder( + listenable: keybinder, + builder: (context, child) { + var shortcuts = keybinder.getShortcuts(); + if (widget.embedding != null) { + shortcuts = Map.from(shortcuts) + ..removeWhere((key, intent) { + return intent is ExportIntent || + intent is ImageExportIntent || + intent is PdfExportIntent || + intent is SvgExportIntent || + intent is SettingsIntent || + intent is ChangePathIntent || + intent is SaveIntent || + intent is PacksIntent || + intent is ZoomIntent || + intent is ChangeToolIntent; + }); + } + return Actions( + actions: actions, + child: Shortcuts( + shortcuts: shortcuts, + child: child!, + ), + ); + }, + child: ClipRect( + child: Focus( + autofocus: true, + skipTraversal: true, + onFocusChange: (_) => false, + child: Scaffold( + appBar: + state is DocumentPresentationState || + windowState.fullScreen || + currentIndex.hideUi != + HideState.visible + ? null + : PadAppBar( + viewportKey: _viewportKey, + size: settings.toolbarSize, + searchController: + _searchController, + padding: padding, + direction: Directionality.of( + context, ), - body: Actions( - actions: _actions, - child: const _MainBody(), - ), - ), + inView: + state.embedding?.isInternal ?? + false, + showTools: + settings.isInline && + state.embedding?.editable != + false, + ), + body: const _MainBody(), ), ), ), diff --git a/app/lib/widgets/search.dart b/app/lib/widgets/search.dart index 99b4c3eb714f..0a413743ad20 100644 --- a/app/lib/widgets/search.dart +++ b/app/lib/widgets/search.dart @@ -5,8 +5,10 @@ import 'package:butterfly/visualizer/tool.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; +import 'package:keybinder/keybinder.dart'; import 'package:material_leap/material_leap.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; @@ -14,6 +16,15 @@ class SearchIntent extends Intent { const SearchIntent(); } +final searchShortcut = ShortcutDefinition( + id: 'search', + intent: const SearchIntent(), + defaultActivator: const SingleActivator( + LogicalKeyboardKey.keyK, + control: true, + ), +); + Future> _searchIsolate( NoteData noteData, String currentPage, diff --git a/app/pubspec.lock b/app/pubspec.lock index 3c323f37c4db..dcbe544e7e46 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -701,6 +701,15 @@ packages: url: "https://pub.dev" source: hosted version: "6.11.2" + keybinder: + dependency: "direct main" + description: + path: "packages/keybinder" + ref: "13e4ec78ac3bad73c56752fdee13329317cee115" + resolved-ref: "13e4ec78ac3bad73c56752fdee13329317cee115" + url: "https://github.com/LinwoodDev/dart_pkgs.git" + source: git + version: "0.0.1" leak_tracker: dependency: transitive description: @@ -1628,5 +1637,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.9.0 <4.0.0" + dart: ">=3.9.2 <4.0.0" flutter: ">=3.35.7" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 8415dac402ac..d28f9392924d 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -94,6 +94,11 @@ dependencies: url: https://github.com/LinwoodDev/dart_pkgs.git ref: 42bd9fde81a3692711862cdb0c60f05f648241a9 path: packages/lw_file_system + keybinder: + git: + url: https://github.com/LinwoodDev/dart_pkgs.git + ref: 13e4ec78ac3bad73c56752fdee13329317cee115 + path: packages/keybinder flutter_localized_locales: ^2.0.5 cryptography_flutter_plus: ^2.3.4 dynamic_color: ^1.7.0