From fa8648045837f707b5cb016522948d7b98cff315 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Wed, 19 Jun 2024 10:24:34 +0200 Subject: [PATCH] feat: shortcuts page remake (#5567) * feat: settings shortcuts page remake * test: add shortcut test * fix: physics on listview * fix: menu icon --- .../desktop/settings/settings_runner.dart | 2 + .../settings/shortcuts_settings_test.dart | 90 +++ .../shared/common_operations.dart | 22 +- .../integration_test/shared/settings.dart | 9 + .../document/presentation/editor_page.dart | 17 +- .../custom_text_align_command.dart | 9 +- .../shortcuts/settings_shortcuts_cubit.dart | 61 +- .../pages/settings_shortcuts_view.dart | 751 ++++++++++++++++++ .../settings/settings_dialog.dart | 2 +- .../settings/shared/settings_body.dart | 1 + .../settings_customize_shortcuts_view.dart | 261 ------ .../settings/widgets/settings_menu.dart | 4 +- .../flowy_icons/16x/keyboard_arrow_down.svg | 8 + .../flowy_icons/16x/keyboard_arrow_left.svg | 8 + .../flowy_icons/16x/keyboard_arrow_right.svg | 8 + .../flowy_icons/16x/keyboard_arrow_up.svg | 8 + .../flowy_icons/16x/keyboard_meta.svg | 8 + .../flowy_icons/16x/keyboard_option.svg | 8 + .../flowy_icons/16x/keyboard_return.svg | 8 + .../flowy_icons/16x/keyboard_shift.svg | 8 + .../flowy_icons/16x/keyboard_tab.svg | 8 + frontend/resources/translations/en.json | 131 ++- 22 files changed, 1105 insertions(+), 327 deletions(-) create mode 100644 frontend/appflowy_flutter/integration_test/desktop/settings/shortcuts_settings_test.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart create mode 100644 frontend/resources/flowy_icons/16x/keyboard_arrow_down.svg create mode 100644 frontend/resources/flowy_icons/16x/keyboard_arrow_left.svg create mode 100644 frontend/resources/flowy_icons/16x/keyboard_arrow_right.svg create mode 100644 frontend/resources/flowy_icons/16x/keyboard_arrow_up.svg create mode 100644 frontend/resources/flowy_icons/16x/keyboard_meta.svg create mode 100644 frontend/resources/flowy_icons/16x/keyboard_option.svg create mode 100644 frontend/resources/flowy_icons/16x/keyboard_return.svg create mode 100644 frontend/resources/flowy_icons/16x/keyboard_shift.svg create mode 100644 frontend/resources/flowy_icons/16x/keyboard_tab.svg diff --git a/frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart index fa22f1c740..34fe511e9e 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart @@ -2,10 +2,12 @@ import 'package:integration_test/integration_test.dart'; import 'notifications_settings_test.dart' as notifications_settings_test; import 'settings_billing_test.dart' as settings_billing_test; +import 'shortcuts_settings_test.dart' as shortcuts_settings_test; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); notifications_settings_test.main(); settings_billing_test.main(); + shortcuts_settings_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/settings/shortcuts_settings_test.dart b/frontend/appflowy_flutter/integration_test/desktop/settings/shortcuts_settings_test.dart new file mode 100644 index 0000000000..fb3383a218 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/settings/shortcuts_settings_test.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/settings_shortcuts_view.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../../shared/keyboard.dart'; +import '../../shared/util.dart'; +import '../board/board_hide_groups_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('shortcuts test', () { + testWidgets('can change and overwrite shortcut', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + + await tester.openSettings(); + await tester.openSettingsPage(SettingsPage.shortcuts); + await tester.pumpAndSettle(); + + final backspaceCmd = + LocaleKeys.settings_shortcutsPage_keybindings_backspace.tr(); + + // Input "Delete" into the search field + await tester.enterText(find.byType(TextField), backspaceCmd); + await tester.pumpAndSettle(); + + await tester.hoverOnWidget( + find.descendant( + of: find.byType(ShortcutSettingTile), + matching: find.text(backspaceCmd), + ), + onHover: () async { + await tester.tap(find.byFlowySvg(FlowySvgs.edit_s)); + await tester.pumpAndSettle(); + + await FlowyTestKeyboard.simulateKeyDownEvent( + [ + LogicalKeyboardKey.delete, + LogicalKeyboardKey.enter, + ], + tester: tester, + ); + await tester.pumpAndSettle(); + }, + ); + + // We expect to see conflict dialog + expect( + find.text( + LocaleKeys.settings_shortcutsPage_conflictDialog_confirmLabel.tr(), + ), + findsOneWidget, + ); + + // Press on confirm label + await tester.tap( + find.text( + LocaleKeys.settings_shortcutsPage_conflictDialog_confirmLabel.tr(), + ), + ); + await tester.pumpAndSettle(); + + // We expect the first ShortcutSettingTile to have one + // [KeyBadge] with `delete` label + final first = tester.widget(find.byType(ShortcutSettingTile).first) + as ShortcutSettingTile; + expect( + first.command.command, + 'delete', + ); + + // And the second one which is `Delete left character` to have none + // as it will have been overwritten + final second = tester.widget(find.byType(ShortcutSettingTile).at(1)) + as ShortcutSettingTile; + expect( + second.command.command, + '', + ); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart index 8bc7236994..6d7cf5ab2f 100644 --- a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart @@ -1,5 +1,10 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -22,6 +27,7 @@ import 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab. import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart'; import 'package:appflowy/workspace/presentation/widgets/more_view_actions/more_view_actions.dart'; import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart'; import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; @@ -29,10 +35,6 @@ import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'emoji.dart'; @@ -619,6 +621,18 @@ extension SettingsFinder on CommonFinders { matching: find.byType(Scrollable), ) .first; + + Finder findSettingsMenuScrollable() => find + .descendant( + of: find + .descendant( + of: find.byType(SettingsMenu), + matching: find.byType(SingleChildScrollView), + ) + .first, + matching: find.byType(Scrollable), + ) + .first; } extension ViewLayoutPBTest on ViewLayoutPB { diff --git a/frontend/appflowy_flutter/integration_test/shared/settings.dart b/frontend/appflowy_flutter/integration_test/shared/settings.dart index 3b25c32111..34684aab1a 100644 --- a/frontend/appflowy_flutter/integration_test/shared/settings.dart +++ b/frontend/appflowy_flutter/integration_test/shared/settings.dart @@ -12,6 +12,7 @@ import 'package:flowy_infra_ui/style_widget/text_field.dart'; import 'package:flutter_test/flutter_test.dart'; import '../desktop/board/board_hide_groups_test.dart'; + import 'base.dart'; import 'common_operations.dart'; @@ -31,6 +32,14 @@ extension AppFlowySettings on WidgetTester { final button = find.byWidgetPredicate( (widget) => widget is SettingsMenuElement && widget.page == page, ); + + await scrollUntilVisible( + button, + 0, + scrollable: find.findSettingsMenuScrollable(), + ); + await pump(); + expect(button, findsOneWidget); await tapButton(button); return; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index f58dd6f452..cf90431c8e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -1,5 +1,8 @@ import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_configuration.dart'; @@ -28,23 +31,21 @@ import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; final codeBlockLocalization = CodeBlockLocalizations( codeBlockNewParagraph: - LocaleKeys.settings_shortcuts_commands_codeBlockNewParagraph.tr(), + LocaleKeys.settings_shortcutsPage_commands_codeBlockNewParagraph.tr(), codeBlockIndentLines: - LocaleKeys.settings_shortcuts_commands_codeBlockIndentLines.tr(), + LocaleKeys.settings_shortcutsPage_commands_codeBlockIndentLines.tr(), codeBlockOutdentLines: - LocaleKeys.settings_shortcuts_commands_codeBlockOutdentLines.tr(), + LocaleKeys.settings_shortcutsPage_commands_codeBlockOutdentLines.tr(), codeBlockSelectAll: - LocaleKeys.settings_shortcuts_commands_codeBlockSelectAll.tr(), + LocaleKeys.settings_shortcutsPage_commands_codeBlockSelectAll.tr(), codeBlockPasteText: - LocaleKeys.settings_shortcuts_commands_codeBlockPasteText.tr(), + LocaleKeys.settings_shortcutsPage_commands_codeBlockPasteText.tr(), codeBlockAddTwoSpaces: - LocaleKeys.settings_shortcuts_commands_codeBlockAddTwoSpaces.tr(), + LocaleKeys.settings_shortcutsPage_commands_codeBlockAddTwoSpaces.tr(), ); final localizedCodeBlockCommands = diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart index e11c42ae99..9fe78591c3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart @@ -1,8 +1,9 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; final List customTextAlignCommands = [ customTextLeftAlignCommand, @@ -21,7 +22,7 @@ final List customTextAlignCommands = [ final CommandShortcutEvent customTextLeftAlignCommand = CommandShortcutEvent( key: 'Align text to the left', command: 'ctrl+shift+l', - getDescription: LocaleKeys.settings_shortcuts_commands_textAlignLeft.tr, + getDescription: LocaleKeys.settings_shortcutsPage_commands_textAlignLeft.tr, handler: (editorState) => _textAlignHandler(editorState, leftAlignmentKey), ); @@ -36,7 +37,7 @@ final CommandShortcutEvent customTextLeftAlignCommand = CommandShortcutEvent( final CommandShortcutEvent customTextCenterAlignCommand = CommandShortcutEvent( key: 'Align text to the center', command: 'ctrl+shift+e', - getDescription: LocaleKeys.settings_shortcuts_commands_textAlignCenter.tr, + getDescription: LocaleKeys.settings_shortcutsPage_commands_textAlignCenter.tr, handler: (editorState) => _textAlignHandler(editorState, centerAlignmentKey), ); @@ -51,7 +52,7 @@ final CommandShortcutEvent customTextCenterAlignCommand = CommandShortcutEvent( final CommandShortcutEvent customTextRightAlignCommand = CommandShortcutEvent( key: 'Align text to the right', command: 'ctrl+shift+r', - getDescription: LocaleKeys.settings_shortcuts_commands_textAlignRight.tr, + getDescription: LocaleKeys.settings_shortcutsPage_commands_textAlignRight.tr, handler: (editorState) => _textAlignHandler(editorState, rightAlignmentKey), ); diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart index 0fdaa49128..07794e05ac 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart @@ -18,7 +18,27 @@ class ShortcutsState with _$ShortcutsState { }) = _ShortcutsState; } -enum ShortcutsStatus { initial, updating, success, failure } +enum ShortcutsStatus { + initial, + updating, + success, + failure; + + /// Helper getter for when the [ShortcutsStatus] signifies + /// that the shortcuts have not been loaded yet. + /// + bool get isLoading => [initial, updating].contains(this); + + /// Helper getter for when the [ShortcutsStatus] signifies + /// a failure by itself being [ShortcutsStatus.failure] + /// + bool get isFailure => this == ShortcutsStatus.failure; + + /// Helper getter for when the [ShortcutsStatus] signifies + /// a success by itself being [ShortcutsStatus.success] + /// + bool get isSuccess => this == ShortcutsStatus.success; +} class ShortcutsCubit extends Cubit { ShortcutsCubit(this.service) : super(const ShortcutsState()); @@ -56,44 +76,31 @@ class ShortcutsCubit extends Cubit { emit( state.copyWith( status: ShortcutsStatus.failure, - error: LocaleKeys.settings_shortcuts_couldNotLoadErrorMsg.tr(), + error: LocaleKeys.settings_shortcutsPage_couldNotLoadErrorMsg.tr(), ), ); } } Future updateAllShortcuts() async { - emit( - state.copyWith( - status: ShortcutsStatus.updating, - error: '', - ), - ); + emit(state.copyWith(status: ShortcutsStatus.updating, error: '')); + try { await service.saveAllShortcuts(state.commandShortcutEvents); - emit( - state.copyWith( - status: ShortcutsStatus.success, - error: '', - ), - ); + emit(state.copyWith(status: ShortcutsStatus.success, error: '')); } catch (e) { emit( state.copyWith( status: ShortcutsStatus.failure, - error: LocaleKeys.settings_shortcuts_couldNotSaveErrorMsg.tr(), + error: LocaleKeys.settings_shortcutsPage_couldNotSaveErrorMsg.tr(), ), ); } } Future resetToDefault() async { - emit( - state.copyWith( - status: ShortcutsStatus.updating, - error: '', - ), - ); + emit(state.copyWith(status: ShortcutsStatus.updating, error: '')); + try { await service.saveAllShortcuts(defaultCommandShortcutEvents); await fetchShortcuts(); @@ -101,7 +108,7 @@ class ShortcutsCubit extends Cubit { emit( state.copyWith( status: ShortcutsStatus.failure, - error: LocaleKeys.settings_shortcuts_couldNotSaveErrorMsg.tr(), + error: LocaleKeys.settings_shortcutsPage_couldNotSaveErrorMsg.tr(), ), ); } @@ -110,16 +117,20 @@ class ShortcutsCubit extends Cubit { /// Checks if the new command is conflicting with other shortcut /// We also check using the key, whether this command is a codeblock /// shortcut, if so we only check a conflict with other codeblock shortcut. - String getConflict(CommandShortcutEvent currentShortcut, String command) { + CommandShortcutEvent? getConflict( + CommandShortcutEvent currentShortcut, + String command, + ) { // check if currentShortcut is a codeblock shortcut. final isCodeBlockCommand = currentShortcut.isCodeBlockCommand; for (final e in state.commandShortcutEvents) { if (e.command == command && e.isCodeBlockCommand == isCodeBlockCommand) { - return e.key; + return e; } } - return ''; + + return null; } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart new file mode 100644 index 0000000000..27b0596d3a --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart @@ -0,0 +1,751 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/custom_cut_command.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcut_event.dart'; +import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart'; +import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SettingsShortcutsView extends StatefulWidget { + const SettingsShortcutsView({super.key}); + + @override + State createState() => _SettingsShortcutsViewState(); +} + +class _SettingsShortcutsViewState extends State { + String _query = ''; + bool _isEditing = false; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => + ShortcutsCubit(SettingsShortcutService())..fetchShortcuts(), + child: Builder( + builder: (context) => SettingsBody( + title: LocaleKeys.settings_shortcutsPage_title.tr(), + autoSeparate: false, + children: [ + Row( + children: [ + Flexible( + child: _SearchBar( + onSearchChanged: (v) => setState(() => _query = v), + ), + ), + const HSpace(10), + _ResetButton( + onReset: () => SettingsAlertDialog( + isDangerous: true, + title: LocaleKeys.settings_shortcutsPage_resetDialog_title + .tr(), + subtitle: LocaleKeys + .settings_shortcutsPage_resetDialog_description + .tr(), + confirmLabel: LocaleKeys + .settings_shortcutsPage_resetDialog_buttonLabel + .tr(), + confirm: () { + Navigator.of(context).pop(); + context.read().resetToDefault(); + }, + ).show(context), + ), + ], + ), + BlocBuilder( + builder: (context, state) { + final filtered = state.commandShortcutEvents + .where( + (e) => e.afLabel + .toLowerCase() + .contains(_query.toLowerCase()), + ) + .toList(); + + return Column( + children: [ + const VSpace(16), + if (state.status.isLoading) ...[ + const CircularProgressIndicator(), + ] else if (state.status.isFailure) ...[ + FlowyErrorPage.message( + LocaleKeys.settings_shortcutsPage_errorPage_message + .tr(args: [state.error]), + howToFix: LocaleKeys + .settings_shortcutsPage_errorPage_howToFix + .tr(), + ), + ] else ...[ + ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: filtered.length, + itemBuilder: (context, index) => ShortcutSettingTile( + command: filtered[index], + canStartEditing: () => !_isEditing, + onStartEditing: () => + setState(() => _isEditing = true), + onFinishEditing: () => + setState(() => _isEditing = false), + ), + ), + ], + ], + ); + }, + ), + ], + ), + ), + ); + } +} + +class _SearchBar extends StatelessWidget { + const _SearchBar({this.onSearchChanged}); + + final void Function(String)? onSearchChanged; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 36, + child: FlowyTextField( + onChanged: onSearchChanged, + textStyle: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + ), + decoration: InputDecoration( + hintText: LocaleKeys.settings_shortcutsPage_searchHint.tr(), + counterText: '', + contentPadding: const EdgeInsets.symmetric( + vertical: 9, + horizontal: 16, + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.outline, + ), + borderRadius: Corners.s12Border, + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + ), + borderRadius: Corners.s12Border, + ), + errorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.error, + ), + borderRadius: Corners.s12Border, + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.error, + ), + borderRadius: Corners.s12Border, + ), + ), + ), + ); + } +} + +class _ResetButton extends StatelessWidget { + const _ResetButton({this.onReset}); + + final void Function()? onReset; + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: onReset, + child: FlowyHover( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 6, + ), + child: Row( + children: [ + const FlowySvg( + FlowySvgs.restore_s, + size: Size.square(20), + ), + const HSpace(6), + SizedBox( + height: 16, + child: FlowyText.regular( + LocaleKeys.settings_shortcutsPage_actions_resetDefault.tr(), + color: AFThemeExtension.of(context).strongText, + ), + ), + ], + ), + ), + ), + ); + } +} + +class ShortcutSettingTile extends StatefulWidget { + const ShortcutSettingTile({ + super.key, + required this.command, + required this.onStartEditing, + required this.onFinishEditing, + required this.canStartEditing, + }); + + final CommandShortcutEvent command; + final VoidCallback onStartEditing; + final VoidCallback onFinishEditing; + final bool Function() canStartEditing; + + @override + State createState() => _ShortcutSettingTileState(); +} + +class _ShortcutSettingTileState extends State { + final keybindController = TextEditingController(); + + late final FocusNode focusNode; + + bool isHovering = false; + bool isEditing = false; + bool canClickOutside = false; + + @override + void initState() { + super.initState(); + focusNode = FocusNode( + onKeyEvent: (focusNode, key) { + if (key is! KeyDownEvent && key is! KeyRepeatEvent) { + return KeyEventResult.ignored; + } + + if (key.logicalKey == LogicalKeyboardKey.enter && + !HardwareKeyboard.instance.isShiftPressed) { + if (keybindController.text == widget.command.command) { + _finishEditing(); + return KeyEventResult.handled; + } + + final conflict = context.read().getConflict( + widget.command, + keybindController.text, + ); + + if (conflict != null) { + canClickOutside = true; + SettingsAlertDialog( + title: LocaleKeys.settings_shortcutsPage_conflictDialog_title + .tr(args: [keybindController.text]), + confirm: () { + conflict.clearCommand(); + _updateCommand(); + Navigator.of(context).pop(); + }, + confirmLabel: LocaleKeys + .settings_shortcutsPage_conflictDialog_confirmLabel + .tr(), + children: [ + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontSize: 16, + fontWeight: FontWeight.normal, + ), + children: [ + TextSpan( + text: LocaleKeys + .settings_shortcutsPage_conflictDialog_descriptionPrefix + .tr(), + ), + TextSpan( + text: conflict.afLabel, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: LocaleKeys + .settings_shortcutsPage_conflictDialog_descriptionSuffix + .tr(args: [keybindController.text]), + ), + ], + ), + ), + ], + ).show(context).then((_) => canClickOutside = false); + } else { + _updateCommand(); + } + } else if (key.logicalKey == LogicalKeyboardKey.escape) { + _finishEditing(); + } else { + // Extract complete keybinding + setState(() => keybindController.text = key.toCommand); + } + + return KeyEventResult.handled; + }, + ); + } + + void _finishEditing() => setState(() { + isEditing = false; + keybindController.clear(); + widget.onFinishEditing(); + }); + + void _updateCommand() { + widget.command.updateCommand(command: keybindController.text); + context.read().updateAllShortcuts(); + _finishEditing(); + } + + @override + void dispose() { + focusNode.dispose(); + keybindController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 4), + decoration: BoxDecoration( + border: Border( + top: BorderSide(color: Theme.of(context).dividerColor), + ), + ), + child: FlowyHover( + cursor: MouseCursor.defer, + style: HoverStyle( + hoverColor: Theme.of(context).colorScheme.secondaryContainer, + borderRadius: BorderRadius.zero, + ), + resetHoverOnRebuild: false, + builder: (context, isHovering) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + const HSpace(8), + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 10), + child: FlowyText.regular( + widget.command.afLabel, + fontSize: 14, + lineHeight: 1, + maxLines: 2, + color: AFThemeExtension.of(context).strongText, + ), + ), + ), + Expanded( + child: isEditing + ? _renderKeybindEditor() + : _renderKeybindings(isHovering), + ), + ], + ), + ), + ), + ); + } + + Widget _renderKeybindings(bool isHovering) => Row( + children: [ + if (widget.command.keybindings.isNotEmpty) ...[ + ..._toParts(widget.command.keybindings.first).map( + (key) => KeyBadge(keyLabel: key), + ), + ] else ...[ + const SizedBox(height: 24), + ], + const Spacer(), + if (isHovering) + GestureDetector( + onTap: () { + if (widget.canStartEditing()) { + setState(() { + widget.onStartEditing(); + isEditing = true; + }); + } + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: FlowyTooltip( + message: LocaleKeys.settings_shortcutsPage_editTooltip.tr(), + child: const FlowySvg( + FlowySvgs.edit_s, + size: Size.square(16), + ), + ), + ), + ), + const HSpace(8), + ], + ); + + Widget _renderKeybindEditor() => TapRegion( + onTapOutside: canClickOutside ? null : (_) => _finishEditing(), + child: FlowyTextField( + focusNode: focusNode, + controller: keybindController, + hintText: LocaleKeys.settings_shortcutsPage_editBindingHint.tr(), + onChanged: (_) => setState(() {}), + suffixIcon: keybindController.text.isNotEmpty + ? MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => setState(() => keybindController.clear()), + child: const FlowySvg( + FlowySvgs.close_s, + size: Size.square(10), + ), + ), + ) + : null, + ), + ); + + List _toParts(Keybinding binding) { + final List keys = []; + + if (binding.isControlPressed) { + keys.add('ctrl'); + } + if (binding.isMetaPressed) { + keys.add('meta'); + } + if (binding.isShiftPressed) { + keys.add('shift'); + } + if (binding.isAltPressed) { + keys.add('alt'); + } + + return keys..add(binding.keyLabel); + } +} + +@visibleForTesting +class KeyBadge extends StatelessWidget { + const KeyBadge({super.key, required this.keyLabel}); + + final String keyLabel; + + @override + Widget build(BuildContext context) { + if (iconData == null && keyLabel.isEmpty) { + return const SizedBox.shrink(); + } + + return Container( + height: 24, + margin: const EdgeInsets.only(right: 4), + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: AFThemeExtension.of(context).greySelect, + borderRadius: Corners.s4Border, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.25), + blurRadius: 1, + offset: const Offset(0, 1), + ), + ], + ), + child: Center( + child: iconData != null + ? FlowySvg( + iconData!, + color: AFThemeExtension.of(context).strongText, + ) + : FlowyText.medium( + keyLabel.toLowerCase(), + fontSize: 12, + color: AFThemeExtension.of(context).strongText, + ), + ), + ); + } + + FlowySvgData? get iconData => switch (keyLabel) { + 'meta' => FlowySvgs.keyboard_meta_s, + 'arrow left' => FlowySvgs.keyboard_arrow_left_s, + 'arrow right' => FlowySvgs.keyboard_arrow_right_s, + 'arrow up' => FlowySvgs.keyboard_arrow_up_s, + 'arrow down' => FlowySvgs.keyboard_arrow_down_s, + 'shift' => FlowySvgs.keyboard_shift_s, + 'tab' => FlowySvgs.keyboard_tab_s, + 'enter' || 'return' => FlowySvgs.keyboard_return_s, + 'opt' || 'option' => FlowySvgs.keyboard_option_s, + _ => null, + }; +} + +extension ToCommand on KeyEvent { + String get toCommand { + String command = ''; + if (HardwareKeyboard.instance.isControlPressed) { + command += 'ctrl+'; + } + if (HardwareKeyboard.instance.isMetaPressed) { + command += 'meta+'; + } + if (HardwareKeyboard.instance.isShiftPressed) { + command += 'shift+'; + } + if (HardwareKeyboard.instance.isAltPressed) { + command += 'alt+'; + } + + if ([ + LogicalKeyboardKey.control, + LogicalKeyboardKey.controlLeft, + LogicalKeyboardKey.controlRight, + LogicalKeyboardKey.meta, + LogicalKeyboardKey.metaLeft, + LogicalKeyboardKey.metaRight, + LogicalKeyboardKey.alt, + LogicalKeyboardKey.altLeft, + LogicalKeyboardKey.altRight, + LogicalKeyboardKey.shift, + LogicalKeyboardKey.shiftLeft, + LogicalKeyboardKey.shiftRight, + ].contains(logicalKey)) { + return command; + } + + final keyPressed = keyToCodeMapping.keys.firstWhere( + (k) => keyToCodeMapping[k] == logicalKey.keyId, + orElse: () => '', + ); + + return command += keyPressed; + } +} + +extension CommandLabel on CommandShortcutEvent { + String get afLabel { + String? label; + + if (key == toggleToggleListCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_toggleToDoList.tr(); + } else if (key == insertNewParagraphNextToCodeBlockCommand('').key) { + label = LocaleKeys + .settings_shortcutsPage_keybindings_insertNewParagraphInCodeblock + .tr(); + } else if (key == pasteInCodeblock('').key) { + label = + LocaleKeys.settings_shortcutsPage_keybindings_pasteInCodeblock.tr(); + } else if (key == selectAllInCodeBlockCommand('').key) { + label = + LocaleKeys.settings_shortcutsPage_keybindings_selectAllCodeblock.tr(); + } else if (key == tabToInsertSpacesInCodeBlockCommand('').key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_indentLineCodeblock + .tr(); + } else if (key == tabToDeleteSpacesInCodeBlockCommand('').key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_outdentLineCodeblock + .tr(); + } else if (key == tabSpacesAtCurosrInCodeBlockCommand('').key) { + label = LocaleKeys + .settings_shortcutsPage_keybindings_twoSpacesCursorCodeblock + .tr(); + } else if (key == customCopyCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_copy.tr(); + } else if (key == customPasteCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_paste.tr(); + } else if (key == customCutCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_cut.tr(); + } else if (key == customTextLeftAlignCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_alignLeft.tr(); + } else if (key == customTextCenterAlignCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_alignCenter.tr(); + } else if (key == customTextRightAlignCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_alignRight.tr(); + } else if (key == undoCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_undo.tr(); + } else if (key == redoCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_redo.tr(); + } else if (key == convertToParagraphCommand.key) { + label = + LocaleKeys.settings_shortcutsPage_keybindings_convertToParagraph.tr(); + } else if (key == backspaceCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_backspace.tr(); + } else if (key == deleteLeftWordCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_deleteLeftWord.tr(); + } else if (key == deleteLeftSentenceCommand.key) { + label = + LocaleKeys.settings_shortcutsPage_keybindings_deleteLeftSentence.tr(); + } else if (key == deleteCommand.key) { + label = PlatformExtension.isMacOS + ? LocaleKeys.settings_shortcutsPage_keybindings_deleteMacOS.tr() + : LocaleKeys.settings_shortcutsPage_keybindings_delete.tr(); + } else if (key == deleteRightWordCommand.key) { + label = + LocaleKeys.settings_shortcutsPage_keybindings_deleteRightWord.tr(); + } else if (key == moveCursorLeftCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorLeft.tr(); + } else if (key == moveCursorToBeginCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorBeginning + .tr(); + } else if (key == moveCursorToLeftWordCommand.key) { + label = + LocaleKeys.settings_shortcutsPage_keybindings_moveCursorLeftWord.tr(); + } else if (key == moveCursorLeftSelectCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorLeftSelect + .tr(); + } else if (key == moveCursorBeginSelectCommand.key) { + label = LocaleKeys + .settings_shortcutsPage_keybindings_moveCursorBeginSelect + .tr(); + } else if (key == moveCursorLeftWordSelectCommand.key) { + label = LocaleKeys + .settings_shortcutsPage_keybindings_moveCursorLeftWordSelect + .tr(); + } else if (key == moveCursorRightCommand.key) { + label = + LocaleKeys.settings_shortcutsPage_keybindings_moveCursorRight.tr(); + } else if (key == moveCursorToEndCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorEnd.tr(); + } else if (key == moveCursorToRightWordCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorRightWord + .tr(); + } else if (key == moveCursorRightSelectCommand.key) { + label = LocaleKeys + .settings_shortcutsPage_keybindings_moveCursorRightSelect + .tr(); + } else if (key == moveCursorEndSelectCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorEndSelect + .tr(); + } else if (key == moveCursorRightWordSelectCommand.key) { + label = LocaleKeys + .settings_shortcutsPage_keybindings_moveCursorRightWordSelect + .tr(); + } else if (key == moveCursorUpCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorUp.tr(); + } else if (key == moveCursorTopSelectCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorTopSelect + .tr(); + } else if (key == moveCursorTopCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorTop.tr(); + } else if (key == moveCursorUpSelectCommand.key) { + label = + LocaleKeys.settings_shortcutsPage_keybindings_moveCursorUpSelect.tr(); + } else if (key == moveCursorDownCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorDown.tr(); + } else if (key == moveCursorBottomSelectCommand.key) { + label = LocaleKeys + .settings_shortcutsPage_keybindings_moveCursorBottomSelect + .tr(); + } else if (key == moveCursorBottomCommand.key) { + label = + LocaleKeys.settings_shortcutsPage_keybindings_moveCursorBottom.tr(); + } else if (key == moveCursorDownSelectCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorDownSelect + .tr(); + } else if (key == homeCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_home.tr(); + } else if (key == endCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_end.tr(); + } else if (key == toggleBoldCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_toggleBold.tr(); + } else if (key == toggleItalicCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_toggleItalic.tr(); + } else if (key == toggleUnderlineCommand.key) { + label = + LocaleKeys.settings_shortcutsPage_keybindings_toggleUnderline.tr(); + } else if (key == toggleStrikethroughCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_toggleStrikethrough + .tr(); + } else if (key == toggleCodeCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_toggleCode.tr(); + } else if (key == toggleHighlightCommand.key) { + label = + LocaleKeys.settings_shortcutsPage_keybindings_toggleHighlight.tr(); + } else if (key == showLinkMenuCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_showLinkMenu.tr(); + } else if (key == openInlineLinkCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_openInlineLink.tr(); + } else if (key == openLinksCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_openLinks.tr(); + } else if (key == indentCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_indent.tr(); + } else if (key == outdentCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_outdent.tr(); + } else if (key == exitEditingCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_exit.tr(); + } else if (key == pageUpCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_pageUp.tr(); + } else if (key == pageDownCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_pageDown.tr(); + } else if (key == selectAllCommand.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_selectAll.tr(); + } else if (key == pasteTextWithoutFormattingCommand.key) { + label = LocaleKeys + .settings_shortcutsPage_keybindings_pasteWithoutFormatting + .tr(); + } else if (key == emojiShortcutEvent.key) { + label = + LocaleKeys.settings_shortcutsPage_keybindings_showEmojiPicker.tr(); + } else if (key == enterInTableCell.key) { + label = + LocaleKeys.settings_shortcutsPage_keybindings_enterInTableCell.tr(); + } else if (key == leftInTableCell.key) { + label = + LocaleKeys.settings_shortcutsPage_keybindings_leftInTableCell.tr(); + } else if (key == rightInTableCell.key) { + label = + LocaleKeys.settings_shortcutsPage_keybindings_rightInTableCell.tr(); + } else if (key == upInTableCell.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_upInTableCell.tr(); + } else if (key == downInTableCell.key) { + label = + LocaleKeys.settings_shortcutsPage_keybindings_downInTableCell.tr(); + } else if (key == tabInTableCell.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_tabInTableCell.tr(); + } else if (key == shiftTabInTableCell.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_shiftTabInTableCell + .tr(); + } else if (key == backSpaceInTableCell.key) { + label = LocaleKeys.settings_shortcutsPage_keybindings_backSpaceInTableCell + .tr(); + } + + return label ?? description?.capitalize() ?? ''; + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index 09aeeacba5..cbe92f1b16 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -7,10 +7,10 @@ import 'package:appflowy/workspace/presentation/settings/pages/settings_account_ import 'package:appflowy/workspace/presentation/settings/pages/settings_billing_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_manage_data_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_plan_view.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/settings_shortcuts_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_page.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart index 3c838bd6fa..041822947f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart @@ -30,6 +30,7 @@ class SettingsBody extends StatelessWidget { SettingsHeader(title: title, description: description), Flexible( child: SeparatedColumn( + mainAxisSize: MainAxisSize.min, separatorBuilder: () => autoSeparate ? const SettingsCategorySpacer() : const VSpace(16), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart deleted file mode 100644 index 8066d9b65e..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart +++ /dev/null @@ -1,261 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart'; -import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart'; -import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SettingsShortcutsView extends StatelessWidget { - const SettingsShortcutsView({super.key}); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => - ShortcutsCubit(SettingsShortcutService())..fetchShortcuts(), - child: SettingsBody( - title: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(), - children: [ - BlocBuilder( - builder: (_, state) => switch (state.status) { - ShortcutsStatus.initial || - ShortcutsStatus.updating => - const Center(child: CircularProgressIndicator()), - ShortcutsStatus.success => - ShortcutsListView(shortcuts: state.commandShortcutEvents), - ShortcutsStatus.failure => - ShortcutsErrorView(errorMessage: state.error), - }, - ), - ], - ), - ); - } -} - -class ShortcutsListView extends StatelessWidget { - const ShortcutsListView({super.key, required this.shortcuts}); - - final List shortcuts; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: FlowyText.semibold( - LocaleKeys.settings_shortcuts_command.tr(), - overflow: TextOverflow.ellipsis, - ), - ), - FlowyText.semibold( - LocaleKeys.settings_shortcuts_keyBinding.tr(), - overflow: TextOverflow.ellipsis, - ), - ], - ), - const VSpace(10), - ...shortcuts.map((e) => ShortcutsListTile(shortcutEvent: e)), - const VSpace(10), - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - const Spacer(), - FlowyTextButton( - LocaleKeys.settings_shortcuts_resetToDefault.tr(), - fontColor: AFThemeExtension.of(context).textColor, - onPressed: () => context.read().resetToDefault(), - ), - ], - ), - const VSpace(10), - ], - ); - } -} - -class ShortcutsListTile extends StatefulWidget { - const ShortcutsListTile({ - super.key, - required this.shortcutEvent, - }); - - final CommandShortcutEvent shortcutEvent; - - @override - State createState() => _ShortcutsListTileState(); -} - -class _ShortcutsListTileState extends State { - late final TextEditingController controller; - - @override - void initState() { - controller = TextEditingController(text: widget.shortcutEvent.command); - super.initState(); - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Row( - children: [ - Expanded( - child: FlowyText.medium( - key: Key(widget.shortcutEvent.key), - widget.shortcutEvent.description!.capitalize(), - overflow: TextOverflow.ellipsis, - ), - ), - FlowyTextButton( - widget.shortcutEvent.command, - fontColor: AFThemeExtension.of(context).textColor, - fillColor: Colors.transparent, - onPressed: () => showKeyListenerDialog(context, controller), - ), - ], - ), - Divider( - color: Theme.of(context).dividerColor, - ), - ], - ); - } - - void showKeyListenerDialog( - BuildContext widgetContext, - TextEditingController controller, - ) { - showDialog( - context: widgetContext, - builder: (builderContext) { - final formKey = GlobalKey(); - return AlertDialog( - title: Text(LocaleKeys.settings_shortcuts_updateShortcutStep.tr()), - content: KeyboardListener( - focusNode: FocusNode(), - onKeyEvent: (key) { - if (key is! KeyDownEvent) return; - if (key.logicalKey == LogicalKeyboardKey.enter && - !HardwareKeyboard.instance.isShiftPressed) { - if (controller.text == widget.shortcutEvent.command) { - _dismiss(builderContext); - } - if (formKey.currentState!.validate()) { - _updateKey(widgetContext, controller.text); - _dismiss(builderContext); - } - } else if (key.logicalKey == LogicalKeyboardKey.escape) { - _dismiss(builderContext); - } else { - //extract the keybinding command from the key event. - controller.text = key.convertToCommand; - } - }, - child: Form( - key: formKey, - child: TextFormField( - autofocus: true, - controller: controller, - readOnly: true, - maxLines: null, - decoration: const InputDecoration( - border: OutlineInputBorder(), - ), - validator: (_) => _validateForConflicts( - widgetContext, - controller.text, - ), - ), - ), - ), - ); - }, - ); - } - - String? _validateForConflicts(BuildContext context, String command) { - final conflict = BlocProvider.of(context).getConflict( - widget.shortcutEvent, - command, - ); - if (conflict.isEmpty) return null; - - return LocaleKeys.settings_shortcuts_shortcutIsAlreadyUsed.tr( - namedArgs: {'conflict': conflict}, - ); - } - - void _updateKey(BuildContext context, String command) { - widget.shortcutEvent.updateCommand(command: command); - BlocProvider.of(context).updateAllShortcuts(); - } - - void _dismiss(BuildContext context) => Navigator.of(context).pop(); -} - -extension on KeyEvent { - String get convertToCommand { - String command = ''; - if (HardwareKeyboard.instance.isAltPressed) { - command += 'alt+'; - } - if (HardwareKeyboard.instance.isControlPressed) { - command += 'ctrl+'; - } - if (HardwareKeyboard.instance.isShiftPressed) { - command += 'shift+'; - } - if (HardwareKeyboard.instance.isMetaPressed) { - command += 'meta+'; - } - - final keyPressed = keyToCodeMapping.keys.firstWhere( - (k) => keyToCodeMapping[k] == logicalKey.keyId, - orElse: () => '', - ); - - return command += keyPressed; - } -} - -class ShortcutsErrorView extends StatelessWidget { - const ShortcutsErrorView({super.key, required this.errorMessage}); - - final String errorMessage; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: FlowyText.medium( - errorMessage, - overflow: TextOverflow.ellipsis, - ), - ), - FlowyIconButton( - icon: const Icon(Icons.replay_outlined), - onPressed: () => context.read().fetchShortcuts(), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index 393fd92f0a..0e89ba8cf7 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -98,8 +98,8 @@ class SettingsMenu extends StatelessWidget { SettingsMenuElement( page: SettingsPage.shortcuts, selectedPage: currentPage, - label: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(), - icon: const Icon(Icons.cut), + label: LocaleKeys.settings_shortcutsPage_menuLabel.tr(), + icon: const FlowySvg(FlowySvgs.settings_shortcuts_m), changeSelectedPage: changeSelectedPage, ), if (FeatureFlag.planBilling.isOn && diff --git a/frontend/resources/flowy_icons/16x/keyboard_arrow_down.svg b/frontend/resources/flowy_icons/16x/keyboard_arrow_down.svg new file mode 100644 index 0000000000..e97f68009b --- /dev/null +++ b/frontend/resources/flowy_icons/16x/keyboard_arrow_down.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/keyboard_arrow_left.svg b/frontend/resources/flowy_icons/16x/keyboard_arrow_left.svg new file mode 100644 index 0000000000..9d235a7170 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/keyboard_arrow_left.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/keyboard_arrow_right.svg b/frontend/resources/flowy_icons/16x/keyboard_arrow_right.svg new file mode 100644 index 0000000000..1be939c778 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/keyboard_arrow_right.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/keyboard_arrow_up.svg b/frontend/resources/flowy_icons/16x/keyboard_arrow_up.svg new file mode 100644 index 0000000000..b67b2ea940 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/keyboard_arrow_up.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/keyboard_meta.svg b/frontend/resources/flowy_icons/16x/keyboard_meta.svg new file mode 100644 index 0000000000..73f4eb9d2a --- /dev/null +++ b/frontend/resources/flowy_icons/16x/keyboard_meta.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/keyboard_option.svg b/frontend/resources/flowy_icons/16x/keyboard_option.svg new file mode 100644 index 0000000000..46d3907e69 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/keyboard_option.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/keyboard_return.svg b/frontend/resources/flowy_icons/16x/keyboard_return.svg new file mode 100644 index 0000000000..1e4106a450 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/keyboard_return.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/keyboard_shift.svg b/frontend/resources/flowy_icons/16x/keyboard_shift.svg new file mode 100644 index 0000000000..b546f0a7f0 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/keyboard_shift.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/16x/keyboard_tab.svg b/frontend/resources/flowy_icons/16x/keyboard_tab.svg new file mode 100644 index 0000000000..5cef294a9b --- /dev/null +++ b/frontend/resources/flowy_icons/16x/keyboard_tab.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 363ed6d141..50067fbf6c 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -502,6 +502,115 @@ } } }, + "shortcutsPage": { + "menuLabel": "Shortcuts", + "title": "Shortcuts", + "editBindingHint": "Input new binding", + "searchHint": "Search", + "actions": { + "resetDefault": "Reset default" + }, + "errorPage": { + "message": "Failed to load shortcuts: {}", + "howToFix": "Please try again, if the issue persists please reach out on GitHub." + }, + "resetDialog": { + "title": "Reset shortcuts", + "description": "This will reset all of your keybindings to the default, you cannot undo this later, are you sure you want to proceed?", + "buttonLabel": "Reset" + }, + "conflictDialog": { + "title": "{} is currently in use", + "descriptionPrefix": "This keybinding is currently being used by ", + "descriptionSuffix": ". If you replace this keybinding, it will be removed from {}.", + "confirmLabel": "Continue" + }, + "editTooltip": "Press to start editing the keybinding", + "keybindings": { + "toggleToDoList": "Toggle to do list", + "insertNewParagraphInCodeblock": "Insert new paragraph", + "pasteInCodeblock": "Paste in codeblock", + "selectAllCodeblock": "Select all", + "indentLineCodeblock": "Insert two spaces at line start", + "outdentLineCodeblock": "Delete two spaces at line start", + "twoSpacesCursorCodeblock": "Insert two spaces at cursor", + "copy": "Copy selection", + "paste": "Paste in content", + "cut": "Cut selection", + "alignLeft": "Align text left", + "alignCenter": "Align text center", + "alignRight": "Align text right", + "undo": "Undo", + "redo": "Redo", + "convertToParagraph": "Convert block to paragraph", + "backspace": "Delete", + "deleteLeftWord": "Delete left word", + "deleteLeftSentence": "Delete left sentence", + "delete": "Delete right character", + "deleteMacOS": "Delete left character", + "deleteRightWord": "Delete right word", + "moveCursorLeft": "Move cursor left", + "moveCursorBeginning": "Move cursor to the beginning", + "moveCursorLeftWord": "Move cursor left one word", + "moveCursorLeftSelect": "Select and move cursor left", + "moveCursorBeginSelect": "Select and move cursor to the beginning", + "moveCursorLeftWordSelect": "Select and move cursor left one word", + "moveCursorRight": "Move cursor right", + "moveCursorEnd": "Move cursor to the end", + "moveCursorRightWord": "Move cursor right one word", + "moveCursorRightSelect": "Select and move cursor right one", + "moveCursorEndSelect": "Select and move cursor to the end", + "moveCursorRightWordSelect": "Select and move cursor to the right one word", + "moveCursorUp": "Move cursor up", + "moveCursorTopSelect": "Select and move cursor to the top", + "moveCursorTop": "Move cursor to the top", + "moveCursorUpSelect": "Select and move cursor up", + "moveCursorBottomSelect": "Select and move cursor to the bottom", + "moveCursorBottom": "Move cursor to the bottom", + "moveCursorDown": "Move cursor down", + "moveCursorDownSelect": "Select and move cursor down", + "home": "Scroll to the top", + "end": "Scroll to the bottom", + "toggleBold": "Toggle bold", + "toggleItalic": "Toggle italic", + "toggleUnderline": "Toggle underline", + "toggleStrikethrough": "Toggle strikethrough", + "toggleCode": "Toggle in-line code", + "toggleHighlight": "Toggle highlight", + "showLinkMenu": "Show link menu", + "openInlineLink": "Open in-line link", + "openLinks": "Open all selected links", + "indent": "Indent", + "outdent": "Outdent", + "exit": "Exit editing", + "pageUp": "Scroll on page up", + "pageDown": "Scroll one page down", + "selectAll": "Select all", + "pasteWithoutFormatting": "Paste content without formatting", + "showEmojiPicker": "Show emoji picker", + "enterInTableCell": "Add linebreak in table", + "leftInTableCell": "Move left one cell in table", + "rightInTableCell": "Move right one cell in table", + "upInTableCell": "Move up one cell in table", + "downInTableCell": "Move down one cell in table", + "tabInTableCell": "Go to next available cell in table", + "shiftTabInTableCell": "Go to previously available cell in table", + "backSpaceInTableCell": "Stop at the beginning of the cell" + }, + "commands": { + "codeBlockNewParagraph": "Insert a new paragraph next to the code block", + "codeBlockIndentLines": "Insert two spaces at the line start in code block", + "codeBlockOutdentLines": "Delete two spaces at the line start in code block", + "codeBlockAddTwoSpaces": "Insert two spaces at the cursor position in code block", + "codeBlockSelectAll": "Select all content inside a code block", + "codeBlockPasteText": "Paste text in codeblock", + "textAlignLeft": "Align text to the left", + "textAlignCenter": "Align text to the center", + "textAlignRight": "Align text to the right" + }, + "couldNotLoadErrorMsg": "Could not load shortcuts, Try again", + "couldNotSaveErrorMsg": "Could not save shortcuts, Try again" + }, "planPage": { "menuLabel": "Plan", "title": "Pricing plan", @@ -858,28 +967,6 @@ "pleaseInputYourStabilityAIKey": "please input your Stability AI key", "clickToLogout": "Click to logout the current user" }, - "shortcuts": { - "shortcutsLabel": "Shortcuts", - "command": "Command", - "keyBinding": "Keybinding", - "addNewCommand": "Add New Command", - "updateShortcutStep": "Press desired key combination and press ENTER", - "shortcutIsAlreadyUsed": "This shortcut is already used for: {conflict}", - "resetToDefault": "Reset to default keybindings", - "couldNotLoadErrorMsg": "Could not load shortcuts, Try again", - "couldNotSaveErrorMsg": "Could not save shortcuts, Try again", - "commands": { - "codeBlockNewParagraph": "Insert a new paragraph next to the code block", - "codeBlockIndentLines": "Insert two spaces at the line start in code block", - "codeBlockOutdentLines": "Delete two spaces at the line start in code block", - "codeBlockAddTwoSpaces": "Insert two spaces at the cursor position in code block", - "codeBlockSelectAll": "Select all content inside a code block", - "codeBlockPasteText": "Paste text in codeblock", - "textAlignLeft": "Align text to the left", - "textAlignCenter": "Align text to the center", - "textAlignRight": "Align text to the right" - } - }, "mobile": { "personalInfo": "Personal Information", "username": "User Name",