feat: shortcuts page remake (#5567)

* feat: settings shortcuts page remake

* test: add shortcut test

* fix: physics on listview

* fix: menu icon
This commit is contained in:
Mathias Mogensen 2024-06-19 10:24:34 +02:00 committed by GitHub
parent d75eb05707
commit fa86480458
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1105 additions and 327 deletions

View File

@ -2,10 +2,12 @@ import 'package:integration_test/integration_test.dart';
import 'notifications_settings_test.dart' as notifications_settings_test; import 'notifications_settings_test.dart' as notifications_settings_test;
import 'settings_billing_test.dart' as settings_billing_test; import 'settings_billing_test.dart' as settings_billing_test;
import 'shortcuts_settings_test.dart' as shortcuts_settings_test;
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
notifications_settings_test.main(); notifications_settings_test.main();
settings_billing_test.main(); settings_billing_test.main();
shortcuts_settings_test.main();
} }

View File

@ -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,
'',
);
});
});
}

View File

@ -1,5 +1,10 @@
import 'dart:io'; 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.dart';
import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/flowy_svgs.g.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_button.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.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/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/more_view_actions.dart';
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.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'; 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:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/buttons/primary_button.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 'package:flutter_test/flutter_test.dart';
import 'emoji.dart'; import 'emoji.dart';
@ -619,6 +621,18 @@ extension SettingsFinder on CommonFinders {
matching: find.byType(Scrollable), matching: find.byType(Scrollable),
) )
.first; .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 { extension ViewLayoutPBTest on ViewLayoutPB {

View File

@ -12,6 +12,7 @@ import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../desktop/board/board_hide_groups_test.dart'; import '../desktop/board/board_hide_groups_test.dart';
import 'base.dart'; import 'base.dart';
import 'common_operations.dart'; import 'common_operations.dart';
@ -31,6 +32,14 @@ extension AppFlowySettings on WidgetTester {
final button = find.byWidgetPredicate( final button = find.byWidgetPredicate(
(widget) => widget is SettingsMenuElement && widget.page == page, (widget) => widget is SettingsMenuElement && widget.page == page,
); );
await scrollUntilVisible(
button,
0,
scrollable: find.findSettingsMenuScrollable(),
);
await pump();
expect(button, findsOneWidget); expect(button, findsOneWidget);
await tapButton(button); await tapButton(button);
return; return;

View File

@ -1,5 +1,8 @@
import 'dart:ui' as ui; 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/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_configuration.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:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.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'; import 'package:flutter_bloc/flutter_bloc.dart';
final codeBlockLocalization = CodeBlockLocalizations( final codeBlockLocalization = CodeBlockLocalizations(
codeBlockNewParagraph: codeBlockNewParagraph:
LocaleKeys.settings_shortcuts_commands_codeBlockNewParagraph.tr(), LocaleKeys.settings_shortcutsPage_commands_codeBlockNewParagraph.tr(),
codeBlockIndentLines: codeBlockIndentLines:
LocaleKeys.settings_shortcuts_commands_codeBlockIndentLines.tr(), LocaleKeys.settings_shortcutsPage_commands_codeBlockIndentLines.tr(),
codeBlockOutdentLines: codeBlockOutdentLines:
LocaleKeys.settings_shortcuts_commands_codeBlockOutdentLines.tr(), LocaleKeys.settings_shortcutsPage_commands_codeBlockOutdentLines.tr(),
codeBlockSelectAll: codeBlockSelectAll:
LocaleKeys.settings_shortcuts_commands_codeBlockSelectAll.tr(), LocaleKeys.settings_shortcutsPage_commands_codeBlockSelectAll.tr(),
codeBlockPasteText: codeBlockPasteText:
LocaleKeys.settings_shortcuts_commands_codeBlockPasteText.tr(), LocaleKeys.settings_shortcutsPage_commands_codeBlockPasteText.tr(),
codeBlockAddTwoSpaces: codeBlockAddTwoSpaces:
LocaleKeys.settings_shortcuts_commands_codeBlockAddTwoSpaces.tr(), LocaleKeys.settings_shortcutsPage_commands_codeBlockAddTwoSpaces.tr(),
); );
final localizedCodeBlockCommands = final localizedCodeBlockCommands =

View File

@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
final List<CommandShortcutEvent> customTextAlignCommands = [ final List<CommandShortcutEvent> customTextAlignCommands = [
customTextLeftAlignCommand, customTextLeftAlignCommand,
@ -21,7 +22,7 @@ final List<CommandShortcutEvent> customTextAlignCommands = [
final CommandShortcutEvent customTextLeftAlignCommand = CommandShortcutEvent( final CommandShortcutEvent customTextLeftAlignCommand = CommandShortcutEvent(
key: 'Align text to the left', key: 'Align text to the left',
command: 'ctrl+shift+l', command: 'ctrl+shift+l',
getDescription: LocaleKeys.settings_shortcuts_commands_textAlignLeft.tr, getDescription: LocaleKeys.settings_shortcutsPage_commands_textAlignLeft.tr,
handler: (editorState) => _textAlignHandler(editorState, leftAlignmentKey), handler: (editorState) => _textAlignHandler(editorState, leftAlignmentKey),
); );
@ -36,7 +37,7 @@ final CommandShortcutEvent customTextLeftAlignCommand = CommandShortcutEvent(
final CommandShortcutEvent customTextCenterAlignCommand = CommandShortcutEvent( final CommandShortcutEvent customTextCenterAlignCommand = CommandShortcutEvent(
key: 'Align text to the center', key: 'Align text to the center',
command: 'ctrl+shift+e', command: 'ctrl+shift+e',
getDescription: LocaleKeys.settings_shortcuts_commands_textAlignCenter.tr, getDescription: LocaleKeys.settings_shortcutsPage_commands_textAlignCenter.tr,
handler: (editorState) => _textAlignHandler(editorState, centerAlignmentKey), handler: (editorState) => _textAlignHandler(editorState, centerAlignmentKey),
); );
@ -51,7 +52,7 @@ final CommandShortcutEvent customTextCenterAlignCommand = CommandShortcutEvent(
final CommandShortcutEvent customTextRightAlignCommand = CommandShortcutEvent( final CommandShortcutEvent customTextRightAlignCommand = CommandShortcutEvent(
key: 'Align text to the right', key: 'Align text to the right',
command: 'ctrl+shift+r', command: 'ctrl+shift+r',
getDescription: LocaleKeys.settings_shortcuts_commands_textAlignRight.tr, getDescription: LocaleKeys.settings_shortcutsPage_commands_textAlignRight.tr,
handler: (editorState) => _textAlignHandler(editorState, rightAlignmentKey), handler: (editorState) => _textAlignHandler(editorState, rightAlignmentKey),
); );

View File

@ -18,7 +18,27 @@ class ShortcutsState with _$ShortcutsState {
}) = _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<ShortcutsState> { class ShortcutsCubit extends Cubit<ShortcutsState> {
ShortcutsCubit(this.service) : super(const ShortcutsState()); ShortcutsCubit(this.service) : super(const ShortcutsState());
@ -56,44 +76,31 @@ class ShortcutsCubit extends Cubit<ShortcutsState> {
emit( emit(
state.copyWith( state.copyWith(
status: ShortcutsStatus.failure, status: ShortcutsStatus.failure,
error: LocaleKeys.settings_shortcuts_couldNotLoadErrorMsg.tr(), error: LocaleKeys.settings_shortcutsPage_couldNotLoadErrorMsg.tr(),
), ),
); );
} }
} }
Future<void> updateAllShortcuts() async { Future<void> updateAllShortcuts() async {
emit( emit(state.copyWith(status: ShortcutsStatus.updating, error: ''));
state.copyWith(
status: ShortcutsStatus.updating,
error: '',
),
);
try { try {
await service.saveAllShortcuts(state.commandShortcutEvents); await service.saveAllShortcuts(state.commandShortcutEvents);
emit( emit(state.copyWith(status: ShortcutsStatus.success, error: ''));
state.copyWith(
status: ShortcutsStatus.success,
error: '',
),
);
} catch (e) { } catch (e) {
emit( emit(
state.copyWith( state.copyWith(
status: ShortcutsStatus.failure, status: ShortcutsStatus.failure,
error: LocaleKeys.settings_shortcuts_couldNotSaveErrorMsg.tr(), error: LocaleKeys.settings_shortcutsPage_couldNotSaveErrorMsg.tr(),
), ),
); );
} }
} }
Future<void> resetToDefault() async { Future<void> resetToDefault() async {
emit( emit(state.copyWith(status: ShortcutsStatus.updating, error: ''));
state.copyWith(
status: ShortcutsStatus.updating,
error: '',
),
);
try { try {
await service.saveAllShortcuts(defaultCommandShortcutEvents); await service.saveAllShortcuts(defaultCommandShortcutEvents);
await fetchShortcuts(); await fetchShortcuts();
@ -101,7 +108,7 @@ class ShortcutsCubit extends Cubit<ShortcutsState> {
emit( emit(
state.copyWith( state.copyWith(
status: ShortcutsStatus.failure, status: ShortcutsStatus.failure,
error: LocaleKeys.settings_shortcuts_couldNotSaveErrorMsg.tr(), error: LocaleKeys.settings_shortcutsPage_couldNotSaveErrorMsg.tr(),
), ),
); );
} }
@ -110,16 +117,20 @@ class ShortcutsCubit extends Cubit<ShortcutsState> {
/// Checks if the new command is conflicting with other shortcut /// Checks if the new command is conflicting with other shortcut
/// We also check using the key, whether this command is a codeblock /// We also check using the key, whether this command is a codeblock
/// shortcut, if so we only check a conflict with other codeblock shortcut. /// 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. // check if currentShortcut is a codeblock shortcut.
final isCodeBlockCommand = currentShortcut.isCodeBlockCommand; final isCodeBlockCommand = currentShortcut.isCodeBlockCommand;
for (final e in state.commandShortcutEvents) { for (final e in state.commandShortcutEvents) {
if (e.command == command && e.isCodeBlockCommand == isCodeBlockCommand) { if (e.command == command && e.isCodeBlockCommand == isCodeBlockCommand) {
return e.key; return e;
} }
} }
return '';
return null;
} }
} }

View File

@ -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<SettingsShortcutsView> createState() => _SettingsShortcutsViewState();
}
class _SettingsShortcutsViewState extends State<SettingsShortcutsView> {
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<ShortcutsCubit>().resetToDefault();
},
).show(context),
),
],
),
BlocBuilder<ShortcutsCubit, ShortcutsState>(
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<ShortcutSettingTile> createState() => _ShortcutSettingTileState();
}
class _ShortcutSettingTileState extends State<ShortcutSettingTile> {
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<ShortcutsCubit>().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<ShortcutsCubit>().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<String> _toParts(Keybinding binding) {
final List<String> 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() ?? '';
}
}

View File

@ -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_billing_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_manage_data_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_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/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/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/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_menu.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';

View File

@ -30,6 +30,7 @@ class SettingsBody extends StatelessWidget {
SettingsHeader(title: title, description: description), SettingsHeader(title: title, description: description),
Flexible( Flexible(
child: SeparatedColumn( child: SeparatedColumn(
mainAxisSize: MainAxisSize.min,
separatorBuilder: () => autoSeparate separatorBuilder: () => autoSeparate
? const SettingsCategorySpacer() ? const SettingsCategorySpacer()
: const VSpace(16), : const VSpace(16),

View File

@ -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<ShortcutsCubit>(
create: (_) =>
ShortcutsCubit(SettingsShortcutService())..fetchShortcuts(),
child: SettingsBody(
title: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(),
children: [
BlocBuilder<ShortcutsCubit, ShortcutsState>(
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<CommandShortcutEvent> 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<ShortcutsCubit>().resetToDefault(),
),
],
),
const VSpace(10),
],
);
}
}
class ShortcutsListTile extends StatefulWidget {
const ShortcutsListTile({
super.key,
required this.shortcutEvent,
});
final CommandShortcutEvent shortcutEvent;
@override
State<ShortcutsListTile> createState() => _ShortcutsListTileState();
}
class _ShortcutsListTileState extends State<ShortcutsListTile> {
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<FormState>();
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<ShortcutsCubit>(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<ShortcutsCubit>(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<ShortcutsCubit>().fetchShortcuts(),
),
],
);
}
}

View File

@ -98,8 +98,8 @@ class SettingsMenu extends StatelessWidget {
SettingsMenuElement( SettingsMenuElement(
page: SettingsPage.shortcuts, page: SettingsPage.shortcuts,
selectedPage: currentPage, selectedPage: currentPage,
label: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(), label: LocaleKeys.settings_shortcutsPage_menuLabel.tr(),
icon: const Icon(Icons.cut), icon: const FlowySvg(FlowySvgs.settings_shortcuts_m),
changeSelectedPage: changeSelectedPage, changeSelectedPage: changeSelectedPage,
), ),
if (FeatureFlag.planBilling.isOn && if (FeatureFlag.planBilling.isOn &&

View File

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_536_10567" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<rect width="16" height="16" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_536_10567)">
<path d="M8 11.9997L4 7.99967L4.93333 7.06634L7.33333 9.46634V3.33301H8.66667V9.46634L11.0667 7.06634L12 7.99967L8 11.9997Z" fill="#1C1B1F"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 451 B

View File

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_536_10470" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<rect width="16" height="16" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_536_10470)">
<path d="M6 12L2 8L6 4L6.93333 4.93333L4.53333 7.33333H14V8.66667H4.53333L6.93333 11.0667L6 12Z" fill="#1C1B1F"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 423 B

View File

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_536_10550" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<rect width="16" height="16" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_536_10550)">
<path d="M9.33335 12L8.40002 11.0333L10.7667 8.66667H2.66669V7.33333H10.7667L8.40002 4.96667L9.33335 4L13.3334 8L9.33335 12Z" fill="#1C1B1F"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 452 B

View File

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_536_10529" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<rect width="16" height="16" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_536_10529)">
<path d="M7.33333 11.9997V5.86634L4.93333 8.26634L4 7.33301L8 3.33301L12 7.33301L11.0667 8.26634L8.66667 5.86634V11.9997H7.33333Z" fill="#1C1B1F"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 457 B

View File

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_536_9311" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<rect width="16" height="16" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_536_9311)">
<path d="M4.33333 14C3.68889 14 3.13889 13.7722 2.68333 13.3167C2.22778 12.8611 2 12.3111 2 11.6667C2 11.0222 2.22778 10.4722 2.68333 10.0167C3.13889 9.56111 3.68889 9.33333 4.33333 9.33333H5.33333V6.66667H4.33333C3.68889 6.66667 3.13889 6.43889 2.68333 5.98333C2.22778 5.52778 2 4.97778 2 4.33333C2 3.68889 2.22778 3.13889 2.68333 2.68333C3.13889 2.22778 3.68889 2 4.33333 2C4.97778 2 5.52778 2.22778 5.98333 2.68333C6.43889 3.13889 6.66667 3.68889 6.66667 4.33333V5.33333H9.33333V4.33333C9.33333 3.68889 9.56111 3.13889 10.0167 2.68333C10.4722 2.22778 11.0222 2 11.6667 2C12.3111 2 12.8611 2.22778 13.3167 2.68333C13.7722 3.13889 14 3.68889 14 4.33333C14 4.97778 13.7722 5.52778 13.3167 5.98333C12.8611 6.43889 12.3111 6.66667 11.6667 6.66667H10.6667V9.33333H11.6667C12.3111 9.33333 12.8611 9.56111 13.3167 10.0167C13.7722 10.4722 14 11.0222 14 11.6667C14 12.3111 13.7722 12.8611 13.3167 13.3167C12.8611 13.7722 12.3111 14 11.6667 14C11.0222 14 10.4722 13.7722 10.0167 13.3167C9.56111 12.8611 9.33333 12.3111 9.33333 11.6667V10.6667H6.66667V11.6667C6.66667 12.3111 6.43889 12.8611 5.98333 13.3167C5.52778 13.7722 4.97778 14 4.33333 14ZM4.33333 12.6667C4.61111 12.6667 4.84722 12.5694 5.04167 12.375C5.23611 12.1806 5.33333 11.9444 5.33333 11.6667V10.6667H4.33333C4.05556 10.6667 3.81944 10.7639 3.625 10.9583C3.43056 11.1528 3.33333 11.3889 3.33333 11.6667C3.33333 11.9444 3.43056 12.1806 3.625 12.375C3.81944 12.5694 4.05556 12.6667 4.33333 12.6667ZM11.6667 12.6667C11.9444 12.6667 12.1806 12.5694 12.375 12.375C12.5694 12.1806 12.6667 11.9444 12.6667 11.6667C12.6667 11.3889 12.5694 11.1528 12.375 10.9583C12.1806 10.7639 11.9444 10.6667 11.6667 10.6667H10.6667V11.6667C10.6667 11.9444 10.7639 12.1806 10.9583 12.375C11.1528 12.5694 11.3889 12.6667 11.6667 12.6667ZM6.66667 9.33333H9.33333V6.66667H6.66667V9.33333ZM4.33333 5.33333H5.33333V4.33333C5.33333 4.05556 5.23611 3.81944 5.04167 3.625C4.84722 3.43056 4.61111 3.33333 4.33333 3.33333C4.05556 3.33333 3.81944 3.43056 3.625 3.625C3.43056 3.81944 3.33333 4.05556 3.33333 4.33333C3.33333 4.61111 3.43056 4.84722 3.625 5.04167C3.81944 5.23611 4.05556 5.33333 4.33333 5.33333ZM10.6667 5.33333H11.6667C11.9444 5.33333 12.1806 5.23611 12.375 5.04167C12.5694 4.84722 12.6667 4.61111 12.6667 4.33333C12.6667 4.05556 12.5694 3.81944 12.375 3.625C12.1806 3.43056 11.9444 3.33333 11.6667 3.33333C11.3889 3.33333 11.1528 3.43056 10.9583 3.625C10.7639 3.81944 10.6667 4.05556 10.6667 4.33333V5.33333Z" fill="#1C1B1F"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_536_9739" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<rect width="16" height="16" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_536_9739)">
<path d="M9.85 12.6663L5.23333 4.66634H2V3.33301H6L10.6167 11.333H14V12.6663H9.85ZM10 4.66634V3.33301H14V4.66634H10Z" fill="#1C1B1F"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_536_9302" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<rect width="16" height="16" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_536_9302)">
<path d="M6 12L2 8L6 4L6.93333 4.93333L4.53333 7.33333H12.6667V4.66667H14V8.66667H4.53333L6.93333 11.0667L6 12Z" fill="#1C1B1F"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 437 B

View File

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_536_9779" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<rect width="16" height="16" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_536_9779)">
<path d="M5.33333 13.9997V8.66634H2L8 1.33301L14 8.66634H10.6667V13.9997H5.33333ZM6.66667 12.6663H9.33333V7.33301H11.1833L8 3.43301L4.81667 7.33301H6.66667V12.6663Z" fill="#1C1B1F"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 490 B

View File

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_536_11836" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="17">
<rect y="0.00195312" width="16" height="16" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_536_11836)">
<path d="M13.3333 12.002V4.00195H14.6666V12.002H13.3333ZM7.99998 12.002L7.04998 11.0686L9.44998 8.66862H1.33331V7.33529H9.44998L7.06665 4.93529L7.99998 4.00195L12 8.00195L7.99998 12.002Z" fill="#1C1B1F"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 529 B

View File

@ -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": { "planPage": {
"menuLabel": "Plan", "menuLabel": "Plan",
"title": "Pricing plan", "title": "Pricing plan",
@ -858,28 +967,6 @@
"pleaseInputYourStabilityAIKey": "please input your Stability AI key", "pleaseInputYourStabilityAIKey": "please input your Stability AI key",
"clickToLogout": "Click to logout the current user" "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": { "mobile": {
"personalInfo": "Personal Information", "personalInfo": "Personal Information",
"username": "User Name", "username": "User Name",