mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: customize command shortcuts (#2848)
This commit is contained in:
parent
5ab64f8835
commit
b1378b4545
@ -1,6 +1,7 @@
|
||||
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
@ -31,6 +32,15 @@ class AppFlowyEditorPage extends StatefulWidget {
|
||||
State<AppFlowyEditorPage> createState() => _AppFlowyEditorPageState();
|
||||
}
|
||||
|
||||
final List<CommandShortcutEvent> commandShortcutEvents = [
|
||||
...codeBlockCommands,
|
||||
...standardCommandShortcutEvents,
|
||||
];
|
||||
|
||||
final List<CommandShortcutEvent> defaultCommandShortcutEvents = [
|
||||
...commandShortcutEvents.map((e) => e.copyWith()).toList(),
|
||||
];
|
||||
|
||||
class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
late final ScrollController effectiveScrollController;
|
||||
|
||||
@ -109,10 +119,10 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_initializeShortcuts();
|
||||
indentableBlockTypes.add(ToggleListBlockKeys.type);
|
||||
convertibleBlockTypes.add(ToggleListBlockKeys.type);
|
||||
slashMenuItems = _customSlashMenuItems();
|
||||
|
||||
effectiveScrollController = widget.scrollController ?? ScrollController();
|
||||
}
|
||||
|
||||
@ -377,4 +387,16 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
}
|
||||
return const (false, null);
|
||||
}
|
||||
|
||||
Future<void> _initializeShortcuts() async {
|
||||
//TODO(Xazin): Refactor lazy initialization
|
||||
defaultCommandShortcutEvents;
|
||||
final settingsShortcutService = SettingsShortcutService();
|
||||
final customizeShortcuts =
|
||||
await settingsShortcutService.getCustomizeShortcuts();
|
||||
await settingsShortcutService.updateCommandShortcuts(
|
||||
standardCommandShortcutEvents,
|
||||
customizeShortcuts,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ enum SettingsPage {
|
||||
files,
|
||||
user,
|
||||
supabaseSetting,
|
||||
shortcuts,
|
||||
}
|
||||
|
||||
class SettingsDialogBloc
|
||||
|
@ -0,0 +1,124 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_shortcut_event.dart';
|
||||
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'settings_shortcuts_cubit.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class ShortcutsState with _$ShortcutsState {
|
||||
const factory ShortcutsState({
|
||||
@Default(<CommandShortcutEvent>[])
|
||||
List<CommandShortcutEvent> commandShortcutEvents,
|
||||
@Default(ShortcutsStatus.initial) ShortcutsStatus status,
|
||||
@Default('') String error,
|
||||
}) = _ShortcutsState;
|
||||
}
|
||||
|
||||
enum ShortcutsStatus { initial, updating, success, failure }
|
||||
|
||||
class ShortcutsCubit extends Cubit<ShortcutsState> {
|
||||
ShortcutsCubit(this.service) : super(const ShortcutsState());
|
||||
|
||||
final SettingsShortcutService service;
|
||||
|
||||
Future<void> fetchShortcuts() async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ShortcutsStatus.updating,
|
||||
error: '',
|
||||
),
|
||||
);
|
||||
try {
|
||||
final customizeShortcuts = await service.getCustomizeShortcuts();
|
||||
await service.updateCommandShortcuts(
|
||||
commandShortcutEvents,
|
||||
customizeShortcuts,
|
||||
);
|
||||
//sort the shortcuts
|
||||
commandShortcutEvents.sort((a, b) => a.key.compareTo(b.key));
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ShortcutsStatus.success,
|
||||
commandShortcutEvents: commandShortcutEvents,
|
||||
error: '',
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ShortcutsStatus.failure,
|
||||
error: LocaleKeys.settings_shortcuts_couldNotLoadErrorMsg.tr(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateAllShortcuts() async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ShortcutsStatus.updating,
|
||||
error: '',
|
||||
),
|
||||
);
|
||||
try {
|
||||
await service.saveAllShortcuts(state.commandShortcutEvents);
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ShortcutsStatus.success,
|
||||
error: '',
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ShortcutsStatus.failure,
|
||||
error: LocaleKeys.settings_shortcuts_couldNotSaveErrorMsg.tr(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> resetToDefault() async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ShortcutsStatus.updating,
|
||||
error: '',
|
||||
),
|
||||
);
|
||||
try {
|
||||
await service.saveAllShortcuts(defaultCommandShortcutEvents);
|
||||
await fetchShortcuts();
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ShortcutsStatus.failure,
|
||||
error: LocaleKeys.settings_shortcuts_couldNotSaveErrorMsg.tr(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///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) {
|
||||
//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 '';
|
||||
}
|
||||
}
|
||||
|
||||
extension on CommandShortcutEvent {
|
||||
bool get isCodeBlockCommand => codeBlockCommands.contains(this);
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'shortcuts_model.dart';
|
||||
|
||||
class SettingsShortcutService {
|
||||
/// If file is non null then the SettingsShortcutService uses that
|
||||
/// file to store all the shortcuts, otherwise uses the default
|
||||
/// Document Directory.
|
||||
/// Typically we only intend to pass a file during testing.
|
||||
SettingsShortcutService({
|
||||
File? file,
|
||||
}) {
|
||||
_initializeService(file);
|
||||
}
|
||||
|
||||
late final File _file;
|
||||
final _initCompleter = Completer<void>();
|
||||
|
||||
/// Takes in commandShortcuts as an input and saves them to the shortcuts.JSON file.
|
||||
Future<void> saveAllShortcuts(
|
||||
List<CommandShortcutEvent> commandShortcuts,
|
||||
) async {
|
||||
final shortcuts = EditorShortcuts(
|
||||
commandShortcuts: commandShortcuts.toCommandShortcutModelList(),
|
||||
);
|
||||
|
||||
await _file.writeAsString(
|
||||
jsonEncode(shortcuts.toJson()),
|
||||
flush: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// Checks the file for saved shortcuts. If shortcuts do NOT exist then returns
|
||||
/// an empty list. If shortcuts exist
|
||||
/// then calls an utility method i.e getShortcutsFromJson which returns the saved shortcuts.
|
||||
Future<List<CommandShortcutModel>> getCustomizeShortcuts() async {
|
||||
await _initCompleter.future;
|
||||
final shortcutsInJson = await _file.readAsString();
|
||||
|
||||
if (shortcutsInJson.isEmpty) {
|
||||
return [];
|
||||
} else {
|
||||
return getShortcutsFromJson(shortcutsInJson);
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts shortcuts from the saved json file. The shortcuts in the saved file consist of [List<CommandShortcutModel>].
|
||||
/// This list needs to be converted to List<CommandShortcutEvent\>. This function is intended to facilitate the same.
|
||||
List<CommandShortcutModel> getShortcutsFromJson(String savedJson) {
|
||||
final shortcuts = EditorShortcuts.fromJson(jsonDecode(savedJson));
|
||||
return shortcuts.commandShortcuts;
|
||||
}
|
||||
|
||||
Future<void> updateCommandShortcuts(
|
||||
List<CommandShortcutEvent> commandShortcuts,
|
||||
List<CommandShortcutModel> customizeShortcuts,
|
||||
) async {
|
||||
for (final shortcut in customizeShortcuts) {
|
||||
final shortcutEvent = commandShortcuts.firstWhereOrNull(
|
||||
(s) => (s.key == shortcut.key && s.command != shortcut.command),
|
||||
);
|
||||
shortcutEvent?.updateCommand(command: shortcut.command);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> resetToDefaultShortcuts() async {
|
||||
await _initCompleter.future;
|
||||
await saveAllShortcuts(defaultCommandShortcutEvents);
|
||||
}
|
||||
|
||||
// Accesses the shortcuts.json file within the default AppFlowy Document Directory or creates a new file if it already doesn't exist.
|
||||
Future<void> _initializeService(File? file) async {
|
||||
_file = file ?? await _defaultShortcutFile();
|
||||
_initCompleter.complete();
|
||||
}
|
||||
|
||||
//returns the default file for storing shortcuts
|
||||
Future<File> _defaultShortcutFile() async {
|
||||
final path = await getIt<ApplicationDataStorage>().getPath();
|
||||
return File(
|
||||
p.join(path, 'shortcuts', 'shortcuts.json'),
|
||||
)..createSync(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
extension on List<CommandShortcutEvent> {
|
||||
/// Utility method for converting a CommandShortcutEvent List to a
|
||||
/// CommandShortcutModal List. This is necessary for creating shortcuts
|
||||
/// object, which is used for saving the shortcuts list.
|
||||
List<CommandShortcutModel> toCommandShortcutModelList() =>
|
||||
map((e) => CommandShortcutModel.fromCommandEvent(e)).toList();
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
class EditorShortcuts {
|
||||
EditorShortcuts({
|
||||
required this.commandShortcuts,
|
||||
});
|
||||
|
||||
final List<CommandShortcutModel> commandShortcuts;
|
||||
|
||||
factory EditorShortcuts.fromJson(Map<String, dynamic> json) =>
|
||||
EditorShortcuts(
|
||||
commandShortcuts: List<CommandShortcutModel>.from(
|
||||
json["commandShortcuts"].map(
|
||||
(x) => CommandShortcutModel.fromJson(x),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"commandShortcuts":
|
||||
List<dynamic>.from(commandShortcuts.map((x) => x.toJson())),
|
||||
};
|
||||
}
|
||||
|
||||
class CommandShortcutModel {
|
||||
const CommandShortcutModel({
|
||||
required this.key,
|
||||
required this.command,
|
||||
});
|
||||
|
||||
final String key;
|
||||
final String command;
|
||||
|
||||
factory CommandShortcutModel.fromJson(Map<String, dynamic> json) =>
|
||||
CommandShortcutModel(
|
||||
key: json["key"],
|
||||
command: (json["command"] ?? ''),
|
||||
);
|
||||
|
||||
factory CommandShortcutModel.fromCommandEvent(
|
||||
CommandShortcutEvent commandShortcutEvent,
|
||||
) =>
|
||||
CommandShortcutModel(
|
||||
key: commandShortcutEvent.key,
|
||||
command: commandShortcutEvent.command,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"key": key,
|
||||
"command": command,
|
||||
};
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is CommandShortcutModel &&
|
||||
key == other.key &&
|
||||
command == other.command;
|
||||
|
||||
@override
|
||||
int get hashCode => key.hashCode ^ command.hashCode;
|
||||
}
|
@ -2,6 +2,7 @@ import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_system_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
@ -88,6 +89,8 @@ class SettingsDialog extends StatelessWidget {
|
||||
return SettingsUserView(user);
|
||||
case SettingsPage.supabaseSetting:
|
||||
return const SupabaseSettingView();
|
||||
case SettingsPage.shortcuts:
|
||||
return const SettingsCustomizeShortcutsWrapper();
|
||||
default:
|
||||
return Container();
|
||||
}
|
||||
|
@ -0,0 +1,257 @@
|
||||
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_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.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';
|
||||
|
||||
class SettingsCustomizeShortcutsWrapper extends StatelessWidget {
|
||||
const SettingsCustomizeShortcutsWrapper({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<ShortcutsCubit>(
|
||||
create: (_) =>
|
||||
ShortcutsCubit(SettingsShortcutService())..fetchShortcuts(),
|
||||
child: const SettingsCustomizeShortcutsView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsCustomizeShortcutsView extends StatelessWidget {
|
||||
const SettingsCustomizeShortcutsView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ShortcutsCubit, ShortcutsState>(
|
||||
builder: (context, state) {
|
||||
switch (state.status) {
|
||||
case ShortcutsStatus.initial:
|
||||
case ShortcutsStatus.updating:
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
case ShortcutsStatus.success:
|
||||
return ShortcutsListView(shortcuts: state.commandShortcutEvents);
|
||||
case ShortcutsStatus.failure:
|
||||
return 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),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: shortcuts.length,
|
||||
itemBuilder: (context, index) => ShortcutsListTile(
|
||||
shortcutEvent: shortcuts[index],
|
||||
),
|
||||
),
|
||||
),
|
||||
const VSpace(10),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
const Spacer(),
|
||||
FlowyTextButton(
|
||||
LocaleKeys.settings_shortcuts_resetToDefault.tr(),
|
||||
onPressed: () {
|
||||
context.read<ShortcutsCubit>().resetToDefault();
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShortcutsListTile extends StatelessWidget {
|
||||
const ShortcutsListTile({
|
||||
super.key,
|
||||
required this.shortcutEvent,
|
||||
});
|
||||
|
||||
final CommandShortcutEvent shortcutEvent;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText.medium(
|
||||
key: Key(shortcutEvent.key),
|
||||
shortcutEvent.key.capitalize(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
FlowyTextButton(
|
||||
shortcutEvent.command,
|
||||
fillColor: Colors.transparent,
|
||||
onPressed: () {
|
||||
showKeyListenerDialog(context);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
Divider(
|
||||
color: Theme.of(context).dividerColor,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void showKeyListenerDialog(BuildContext widgetContext) {
|
||||
showDialog(
|
||||
context: widgetContext,
|
||||
builder: (builderContext) {
|
||||
final controller = TextEditingController(text: shortcutEvent.command);
|
||||
final formKey = GlobalKey<FormState>();
|
||||
return AlertDialog(
|
||||
title: Text(LocaleKeys.settings_shortcuts_updateShortcutStep.tr()),
|
||||
content: RawKeyboardListener(
|
||||
focusNode: FocusNode(),
|
||||
onKey: (key) {
|
||||
if (key is! RawKeyDownEvent) return;
|
||||
if (key.logicalKey == LogicalKeyboardKey.enter &&
|
||||
!key.isShiftPressed) {
|
||||
if (controller.text == 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 rawkeyevent.
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_validateForConflicts(BuildContext context, String command) {
|
||||
final conflict = BlocProvider.of<ShortcutsCubit>(context).getConflict(
|
||||
shortcutEvent,
|
||||
command,
|
||||
);
|
||||
if (conflict.isEmpty) return null;
|
||||
|
||||
return LocaleKeys.settings_shortcuts_shortcutIsAlreadyUsed.tr(
|
||||
namedArgs: {'conflict': conflict},
|
||||
);
|
||||
}
|
||||
|
||||
_updateKey(BuildContext context, String command) {
|
||||
shortcutEvent.updateCommand(command: command);
|
||||
BlocProvider.of<ShortcutsCubit>(context).updateAllShortcuts();
|
||||
}
|
||||
|
||||
_dismiss(BuildContext context) => Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
extension on RawKeyEvent {
|
||||
String get convertToCommand {
|
||||
String command = '';
|
||||
if (isAltPressed) {
|
||||
command += 'alt+';
|
||||
}
|
||||
if (isControlPressed) {
|
||||
command += 'ctrl+';
|
||||
}
|
||||
if (isShiftPressed) {
|
||||
command += 'shift+';
|
||||
}
|
||||
if (isMetaPressed) {
|
||||
command += 'meta+';
|
||||
}
|
||||
|
||||
final keyPressed = keyToCodeMapping.keys.firstWhere(
|
||||
(k) => keyToCodeMapping[k] == logicalKey.keyId,
|
||||
orElse: () => '',
|
||||
);
|
||||
|
||||
return command += keyPressed;
|
||||
}
|
||||
}
|
||||
|
||||
class ShortcutsErrorView extends StatelessWidget {
|
||||
final String errorMessage;
|
||||
const ShortcutsErrorView({super.key, required this.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: () {
|
||||
BlocProvider.of<ShortcutsCubit>(context).fetchShortcuts();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -70,6 +70,16 @@ class SettingsMenu extends StatelessWidget {
|
||||
icon: Icons.sync,
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.shortcuts,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(),
|
||||
icon: Icons.cut,
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,164 @@
|
||||
import 'dart:ffi';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.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:bloc_test/bloc_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockSettingsShortcutService extends Mock
|
||||
implements SettingsShortcutService {}
|
||||
|
||||
void main() {
|
||||
group("ShortcutsCubit", () {
|
||||
late SettingsShortcutService service;
|
||||
late ShortcutsCubit shortcutsCubit;
|
||||
|
||||
setUp(() async {
|
||||
service = MockSettingsShortcutService();
|
||||
when(
|
||||
() => service.saveAllShortcuts(any()),
|
||||
).thenAnswer((_) async => true);
|
||||
when(
|
||||
() => service.getCustomizeShortcuts(),
|
||||
).thenAnswer((_) async => []);
|
||||
when(
|
||||
() => service.updateCommandShortcuts(any(), any()),
|
||||
).thenAnswer((_) async => Void);
|
||||
|
||||
shortcutsCubit = ShortcutsCubit(service);
|
||||
});
|
||||
|
||||
test('initial state is correct', () {
|
||||
final shortcutsCubit = ShortcutsCubit(service);
|
||||
expect(shortcutsCubit.state, const ShortcutsState());
|
||||
});
|
||||
|
||||
group('fetchShortcuts', () {
|
||||
blocTest<ShortcutsCubit, ShortcutsState>(
|
||||
'calls getCustomizeShortcuts() once',
|
||||
build: () => shortcutsCubit,
|
||||
act: (cubit) => cubit.fetchShortcuts(),
|
||||
verify: (_) {
|
||||
verify(() => service.getCustomizeShortcuts()).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
blocTest<ShortcutsCubit, ShortcutsState>(
|
||||
'emits [updating, failure] when getCustomizeShortcuts() throws',
|
||||
setUp: () {
|
||||
when(
|
||||
() => service.getCustomizeShortcuts(),
|
||||
).thenThrow(Exception('oops'));
|
||||
},
|
||||
build: () => shortcutsCubit,
|
||||
act: (cubit) => cubit.fetchShortcuts(),
|
||||
expect: () => <dynamic>[
|
||||
const ShortcutsState(status: ShortcutsStatus.updating),
|
||||
isA<ShortcutsState>()
|
||||
.having((w) => w.status, 'status', ShortcutsStatus.failure)
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<ShortcutsCubit, ShortcutsState>(
|
||||
'emits [updating, success] when getCustomizeShortcuts() returns shortcuts',
|
||||
build: () => shortcutsCubit,
|
||||
act: (cubit) => cubit.fetchShortcuts(),
|
||||
expect: () => <dynamic>[
|
||||
const ShortcutsState(status: ShortcutsStatus.updating),
|
||||
isA<ShortcutsState>()
|
||||
.having((w) => w.status, 'status', ShortcutsStatus.success)
|
||||
.having(
|
||||
(w) => w.commandShortcutEvents,
|
||||
'shortcuts',
|
||||
commandShortcutEvents,
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
group('updateShortcut', () {
|
||||
blocTest<ShortcutsCubit, ShortcutsState>(
|
||||
'calls saveAllShortcuts() once',
|
||||
build: () => shortcutsCubit,
|
||||
act: (cubit) => cubit.updateAllShortcuts(),
|
||||
verify: (_) {
|
||||
verify(() => service.saveAllShortcuts(any())).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
blocTest<ShortcutsCubit, ShortcutsState>(
|
||||
'emits [updating, failure] when saveAllShortcuts() throws',
|
||||
setUp: () {
|
||||
when(
|
||||
() => service.saveAllShortcuts(any()),
|
||||
).thenThrow(Exception('oops'));
|
||||
},
|
||||
build: () => shortcutsCubit,
|
||||
act: (cubit) => cubit.updateAllShortcuts(),
|
||||
expect: () => <dynamic>[
|
||||
const ShortcutsState(status: ShortcutsStatus.updating),
|
||||
isA<ShortcutsState>()
|
||||
.having((w) => w.status, 'status', ShortcutsStatus.failure)
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<ShortcutsCubit, ShortcutsState>(
|
||||
'emits [updating, success] when saveAllShortcuts() is successful',
|
||||
build: () => shortcutsCubit,
|
||||
act: (cubit) => cubit.updateAllShortcuts(),
|
||||
expect: () => <dynamic>[
|
||||
const ShortcutsState(status: ShortcutsStatus.updating),
|
||||
isA<ShortcutsState>()
|
||||
.having((w) => w.status, 'status', ShortcutsStatus.success)
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
group('resetToDefault', () {
|
||||
blocTest<ShortcutsCubit, ShortcutsState>(
|
||||
'calls saveAllShortcuts() once',
|
||||
build: () => shortcutsCubit,
|
||||
act: (cubit) => cubit.resetToDefault(),
|
||||
verify: (_) {
|
||||
verify(() => service.saveAllShortcuts(any())).called(1);
|
||||
verify(() => service.getCustomizeShortcuts()).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
blocTest<ShortcutsCubit, ShortcutsState>(
|
||||
'emits [updating, failure] when saveAllShortcuts() throws',
|
||||
setUp: () {
|
||||
when(
|
||||
() => service.saveAllShortcuts(any()),
|
||||
).thenThrow(Exception('oops'));
|
||||
},
|
||||
build: () => shortcutsCubit,
|
||||
act: (cubit) => cubit.resetToDefault(),
|
||||
expect: () => <dynamic>[
|
||||
const ShortcutsState(status: ShortcutsStatus.updating),
|
||||
isA<ShortcutsState>()
|
||||
.having((w) => w.status, 'status', ShortcutsStatus.failure)
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<ShortcutsCubit, ShortcutsState>(
|
||||
'emits [updating, success] when getCustomizeShortcuts() returns shortcuts',
|
||||
build: () => shortcutsCubit,
|
||||
act: (cubit) => cubit.resetToDefault(),
|
||||
expect: () => <dynamic>[
|
||||
const ShortcutsState(status: ShortcutsStatus.updating),
|
||||
isA<ShortcutsState>()
|
||||
.having((w) => w.status, 'status', ShortcutsStatus.success)
|
||||
.having(
|
||||
(w) => w.commandShortcutEvents,
|
||||
'shortcuts',
|
||||
commandShortcutEvents,
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,169 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io' show File;
|
||||
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/shortcuts/shortcuts_model.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:file/memory.dart';
|
||||
|
||||
void main() {
|
||||
late SettingsShortcutService service;
|
||||
late File mockFile;
|
||||
String shortcutsJson = '';
|
||||
|
||||
setUp(() async {
|
||||
final MemoryFileSystem fileSystem = MemoryFileSystem.test();
|
||||
mockFile = await fileSystem.file("shortcuts.json").create(recursive: true);
|
||||
service = SettingsShortcutService(file: mockFile);
|
||||
shortcutsJson = """{
|
||||
"commandShortcuts":[
|
||||
{
|
||||
"key":"move the cursor upward",
|
||||
"command":"alt+arrow up"
|
||||
},
|
||||
{
|
||||
"key":"move the cursor forward one character",
|
||||
"command":"alt+arrow left"
|
||||
},
|
||||
{
|
||||
"key":"move the cursor downward",
|
||||
"command":"alt+arrow down"
|
||||
}
|
||||
]
|
||||
}""";
|
||||
});
|
||||
|
||||
group("Settings Shortcut Service", () {
|
||||
test(
|
||||
"returns default standard shortcuts if file is empty",
|
||||
() async {
|
||||
expect(await service.getCustomizeShortcuts(), []);
|
||||
},
|
||||
);
|
||||
|
||||
test('returns updated shortcut event list from json', () {
|
||||
final commandShortcuts = service.getShortcutsFromJson(shortcutsJson);
|
||||
|
||||
final cursorUpShortcut = commandShortcuts
|
||||
.firstWhere((el) => el.key == "move the cursor upward");
|
||||
|
||||
final cursorDownShortcut = commandShortcuts
|
||||
.firstWhere((el) => el.key == "move the cursor downward");
|
||||
|
||||
expect(
|
||||
commandShortcuts.length,
|
||||
3,
|
||||
);
|
||||
expect(cursorUpShortcut.command, "alt+arrow up");
|
||||
expect(cursorDownShortcut.command, "alt+arrow down");
|
||||
});
|
||||
|
||||
test(
|
||||
"saveAllShortcuts saves shortcuts",
|
||||
() async {
|
||||
//updating one of standard command shortcut events.
|
||||
final currentCommandShortcuts = standardCommandShortcutEvents;
|
||||
const kKey = "scroll one page down";
|
||||
const oldCommand = "page down";
|
||||
const newCommand = "alt+page down";
|
||||
final commandShortcutEvent = currentCommandShortcuts
|
||||
.firstWhere((element) => element.key == kKey);
|
||||
|
||||
expect(commandShortcutEvent.command, oldCommand);
|
||||
|
||||
//updating the command.
|
||||
commandShortcutEvent.updateCommand(
|
||||
command: newCommand,
|
||||
);
|
||||
|
||||
//saving the updated shortcuts
|
||||
await service.saveAllShortcuts(currentCommandShortcuts);
|
||||
|
||||
//reading from the mock file the saved shortcut list.
|
||||
final savedDataInFile = await mockFile.readAsString();
|
||||
|
||||
//Check if the lists where properly converted to JSON and saved.
|
||||
final shortcuts = EditorShortcuts(
|
||||
commandShortcuts:
|
||||
currentCommandShortcuts.toCommandShortcutModelList(),
|
||||
);
|
||||
|
||||
expect(jsonEncode(shortcuts.toJson()), savedDataInFile);
|
||||
|
||||
//now checking if the modified command of "move the cursor upward" is "arrow up"
|
||||
final newCommandShortcuts =
|
||||
service.getShortcutsFromJson(savedDataInFile);
|
||||
|
||||
final updatedCommandEvent =
|
||||
newCommandShortcuts.firstWhere((el) => el.key == kKey);
|
||||
|
||||
expect(updatedCommandEvent.command, newCommand);
|
||||
},
|
||||
);
|
||||
|
||||
test('load shortcuts from file', () async {
|
||||
//updating one of standard command shortcut event.
|
||||
const kKey = "scroll one page up";
|
||||
const oldCommand = "page up";
|
||||
const newCommand = "alt+page up";
|
||||
final currentCommandShortcuts = standardCommandShortcutEvents;
|
||||
final commandShortcutEvent =
|
||||
currentCommandShortcuts.firstWhere((element) => element.key == kKey);
|
||||
|
||||
expect(commandShortcutEvent.command, oldCommand);
|
||||
|
||||
//updating the command.
|
||||
commandShortcutEvent.updateCommand(command: newCommand);
|
||||
|
||||
//saving the updated shortcuts
|
||||
service.saveAllShortcuts(currentCommandShortcuts);
|
||||
|
||||
//now directly fetching the shortcuts from loadShortcuts
|
||||
final commandShortcuts = await service.getCustomizeShortcuts();
|
||||
expect(
|
||||
commandShortcuts,
|
||||
currentCommandShortcuts.toCommandShortcutModelList(),
|
||||
);
|
||||
|
||||
final updatedCommandEvent =
|
||||
commandShortcuts.firstWhere((el) => el.key == kKey);
|
||||
|
||||
expect(updatedCommandEvent.command, newCommand);
|
||||
});
|
||||
|
||||
test('updateCommandShortcuts works properly', () async {
|
||||
//updating one of standard command shortcut event.
|
||||
const kKey = "move the cursor forward one character";
|
||||
const oldCommand = "arrow left";
|
||||
const newCommand = "alt+arrow left";
|
||||
final currentCommandShortcuts = standardCommandShortcutEvents;
|
||||
|
||||
//check if the current shortcut event's key is set to old command.
|
||||
final currentCommandEvent =
|
||||
currentCommandShortcuts.firstWhere((el) => el.key == kKey);
|
||||
|
||||
expect(currentCommandEvent.command, oldCommand);
|
||||
|
||||
final commandShortcutModelList =
|
||||
EditorShortcuts.fromJson(jsonDecode(shortcutsJson)).commandShortcuts;
|
||||
|
||||
//now calling the updateCommandShortcuts method
|
||||
await service.updateCommandShortcuts(
|
||||
currentCommandShortcuts,
|
||||
commandShortcutModelList,
|
||||
);
|
||||
|
||||
//check if the shortcut event's key is updated.
|
||||
final updatedCommandEvent =
|
||||
currentCommandShortcuts.firstWhere((el) => el.key == kKey);
|
||||
|
||||
expect(updatedCommandEvent.command, newCommand);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
extension on List<CommandShortcutEvent> {
|
||||
List<CommandShortcutModel> toCommandShortcutModelList() =>
|
||||
map((e) => CommandShortcutModel.fromCommandEvent(e)).toList();
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockShortcutsCubit extends MockCubit<ShortcutsState>
|
||||
implements ShortcutsCubit {}
|
||||
|
||||
void main() {
|
||||
group(
|
||||
"CustomizeShortcutsView",
|
||||
() {
|
||||
group(
|
||||
"should be displayed in ViewState",
|
||||
() {
|
||||
late ShortcutsCubit mockShortcutsCubit;
|
||||
|
||||
setUp(() {
|
||||
mockShortcutsCubit = MockShortcutsCubit();
|
||||
});
|
||||
|
||||
testWidgets('Initial when cubit emits [ShortcutsStatus.Initial]',
|
||||
(widgetTester) async {
|
||||
when(() => mockShortcutsCubit.state)
|
||||
.thenReturn(const ShortcutsState());
|
||||
|
||||
await widgetTester.pumpWidget(
|
||||
BlocProvider.value(
|
||||
value: mockShortcutsCubit,
|
||||
child:
|
||||
const MaterialApp(home: SettingsCustomizeShortcutsView()),
|
||||
),
|
||||
);
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Updating when cubit emits [ShortcutsStatus.updating]',
|
||||
(widgetTester) async {
|
||||
when(() => mockShortcutsCubit.state).thenReturn(
|
||||
const ShortcutsState(status: ShortcutsStatus.updating),
|
||||
);
|
||||
|
||||
await widgetTester.pumpWidget(
|
||||
BlocProvider.value(
|
||||
value: mockShortcutsCubit,
|
||||
child:
|
||||
const MaterialApp(home: SettingsCustomizeShortcutsView()),
|
||||
),
|
||||
);
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Shows ShortcutsList when cubit emits [ShortcutsStatus.success]',
|
||||
(widgetTester) async {
|
||||
KeyEventResult dummyHandler(EditorState e) =>
|
||||
KeyEventResult.handled;
|
||||
|
||||
final dummyShortcuts = <CommandShortcutEvent>[
|
||||
CommandShortcutEvent(
|
||||
key: 'Copy',
|
||||
command: 'ctrl+c',
|
||||
handler: dummyHandler,
|
||||
),
|
||||
CommandShortcutEvent(
|
||||
key: 'Paste',
|
||||
command: 'ctrl+v',
|
||||
handler: dummyHandler,
|
||||
),
|
||||
CommandShortcutEvent(
|
||||
key: 'Undo',
|
||||
command: 'ctrl+z',
|
||||
handler: dummyHandler,
|
||||
),
|
||||
CommandShortcutEvent(
|
||||
key: 'Redo',
|
||||
command: 'ctrl+y',
|
||||
handler: dummyHandler,
|
||||
),
|
||||
];
|
||||
|
||||
when(() => mockShortcutsCubit.state).thenReturn(
|
||||
ShortcutsState(
|
||||
status: ShortcutsStatus.success,
|
||||
commandShortcutEvents: dummyShortcuts,
|
||||
),
|
||||
);
|
||||
await widgetTester.pumpWidget(
|
||||
BlocProvider.value(
|
||||
value: mockShortcutsCubit,
|
||||
child:
|
||||
const MaterialApp(home: SettingsCustomizeShortcutsView()),
|
||||
),
|
||||
);
|
||||
|
||||
await widgetTester.pump();
|
||||
|
||||
final listViewFinder = find.byType(ShortcutsListView);
|
||||
final foundShortcuts = widgetTester
|
||||
.widget<ShortcutsListView>(listViewFinder)
|
||||
.shortcuts;
|
||||
|
||||
expect(listViewFinder, findsOneWidget);
|
||||
expect(foundShortcuts, dummyShortcuts);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('Shows Error when cubit emits [ShortcutsStatus.failure]',
|
||||
(tester) async {
|
||||
when(() => mockShortcutsCubit.state).thenReturn(
|
||||
const ShortcutsState(
|
||||
status: ShortcutsStatus.failure,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
BlocProvider.value(
|
||||
value: mockShortcutsCubit,
|
||||
child:
|
||||
const MaterialApp(home: SettingsCustomizeShortcutsView()),
|
||||
),
|
||||
);
|
||||
expect(find.byType(ShortcutsErrorView), findsOneWidget);
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group("ShortcutsErrorView", () {
|
||||
testWidgets("displays correctly", (widgetTester) async {
|
||||
await widgetTester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: ShortcutsErrorView(
|
||||
errorMessage: 'Error occured',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(FlowyText), findsOneWidget);
|
||||
expect(find.byType(FlowyIconButton), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
KeyEventResult dummyHandler(EditorState e) => KeyEventResult.handled;
|
||||
|
||||
final shortcut = CommandShortcutEvent(
|
||||
key: 'Copy',
|
||||
command: 'ctrl+c',
|
||||
handler: dummyHandler,
|
||||
);
|
||||
|
||||
group("ShortcutsListTile", () {
|
||||
group(
|
||||
"should be displayed correctly",
|
||||
() {
|
||||
testWidgets('with key and command', (widgetTester) async {
|
||||
final sKey = Key(shortcut.key);
|
||||
|
||||
await widgetTester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ShortcutsListTile(shortcutEvent: shortcut),
|
||||
),
|
||||
);
|
||||
|
||||
final commandTextFinder = find.byKey(sKey);
|
||||
final foundCommand =
|
||||
widgetTester.widget<FlowyText>(commandTextFinder).text;
|
||||
|
||||
expect(commandTextFinder, findsOneWidget);
|
||||
expect(foundCommand, shortcut.key);
|
||||
|
||||
final btnFinder = find.byType(FlowyTextButton);
|
||||
final foundBtnText =
|
||||
widgetTester.widget<FlowyTextButton>(btnFinder).text;
|
||||
|
||||
expect(btnFinder, findsOneWidget);
|
||||
expect(foundBtnText, shortcut.command);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
group(
|
||||
"taps the button",
|
||||
() {
|
||||
testWidgets("opens AlertDialog correctly", (widgetTester) async {
|
||||
await widgetTester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ShortcutsListTile(shortcutEvent: shortcut),
|
||||
),
|
||||
);
|
||||
|
||||
final btnFinder = find.byType(FlowyTextButton);
|
||||
final foundBtnText =
|
||||
widgetTester.widget<FlowyTextButton>(btnFinder).text;
|
||||
|
||||
expect(btnFinder, findsOneWidget);
|
||||
expect(foundBtnText, shortcut.command);
|
||||
|
||||
await widgetTester.tap(btnFinder);
|
||||
await widgetTester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(AlertDialog), findsOneWidget);
|
||||
expect(find.byType(RawKeyboardListener), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("updates the text with new key event",
|
||||
(widgetTester) async {
|
||||
await widgetTester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ShortcutsListTile(shortcutEvent: shortcut),
|
||||
),
|
||||
);
|
||||
|
||||
final btnFinder = find.byType(FlowyTextButton);
|
||||
|
||||
await widgetTester.tap(btnFinder);
|
||||
await widgetTester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(AlertDialog), findsOneWidget);
|
||||
expect(find.byType(RawKeyboardListener), findsOneWidget);
|
||||
|
||||
await widgetTester.sendKeyEvent(LogicalKeyboardKey.keyC);
|
||||
|
||||
expect(find.text('c'), findsOneWidget);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
KeyEventResult dummyHandler(EditorState e) => KeyEventResult.handled;
|
||||
|
||||
final dummyShortcuts = [
|
||||
CommandShortcutEvent(
|
||||
key: 'Copy',
|
||||
command: 'ctrl+c',
|
||||
handler: dummyHandler,
|
||||
),
|
||||
CommandShortcutEvent(
|
||||
key: 'Paste',
|
||||
command: 'ctrl+v',
|
||||
handler: dummyHandler,
|
||||
),
|
||||
CommandShortcutEvent(
|
||||
key: 'Undo',
|
||||
command: 'ctrl+z',
|
||||
handler: dummyHandler,
|
||||
),
|
||||
CommandShortcutEvent(
|
||||
key: 'Redo',
|
||||
command: 'ctrl+y',
|
||||
handler: dummyHandler,
|
||||
),
|
||||
];
|
||||
|
||||
group("ShortcutsListView", () {
|
||||
group("should be displayed correctly", () {
|
||||
testWidgets("with empty shortcut list", (widgetTester) async {
|
||||
await widgetTester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: ShortcutsListView(shortcuts: []),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(FlowyText), findsNWidgets(3));
|
||||
//we expect three text widgets which are keybinding, command, and reset
|
||||
expect(find.byType(ListView), findsOneWidget);
|
||||
expect(find.byType(ShortcutsListTile), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets("with 1 item in shortcut list", (widgetTester) async {
|
||||
await widgetTester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ShortcutsListView(shortcuts: [dummyShortcuts[0]]),
|
||||
),
|
||||
);
|
||||
|
||||
await widgetTester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(FlowyText), findsAtLeastNWidgets(3));
|
||||
expect(find.byType(ListView), findsOneWidget);
|
||||
expect(find.byType(ShortcutsListTile), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("with populated shortcut list", (widgetTester) async {
|
||||
await widgetTester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ShortcutsListView(shortcuts: dummyShortcuts),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(FlowyText), findsAtLeastNWidgets(3));
|
||||
expect(find.byType(ListView), findsOneWidget);
|
||||
expect(
|
||||
find.byType(ShortcutsListTile),
|
||||
findsNWidgets(dummyShortcuts.length),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -277,6 +277,17 @@
|
||||
"icon": "Icon",
|
||||
"selectAnIcon": "Select an icon",
|
||||
"pleaseInputYourOpenAIKey": "please input your OpenAI key"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
|
Loading…
Reference in New Issue
Block a user