Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: reset individual shortcut #5756

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

Expand Down Expand Up @@ -86,5 +87,80 @@ void main() {
'',
);
});

testWidgets('can reset an individual shortcut', (tester) async {
// In order to reset a shortcut, we must first override it.

await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();

await tester.openSettings();
await tester.openSettingsPage(SettingsPage.shortcuts);
await tester.pumpAndSettle();

final pageUpCmdText =
LocaleKeys.settings_shortcutsPage_keybindings_pageUp.tr();
final defaultPageUpCmd = pageUpCommand.command;

// Input "Page Up text" into the search field
// This test works because we only have one input field on the shortcuts page.
await tester.enterText(find.byType(TextField), pageUpCmdText);
await tester.pumpAndSettle();

await tester.hoverOnWidget(
find.descendant(
of: find.byType(ShortcutSettingTile),
matching: find.text(pageUpCmdText),
),
onHover: () async {
// changing the shortcut
await tester.tap(find.byFlowySvg(FlowySvgs.edit_s));
await tester.pumpAndSettle();

await FlowyTestKeyboard.simulateKeyDownEvent(
[
LogicalKeyboardKey.backquote,
LogicalKeyboardKey.enter,
],
tester: tester,
);
await tester.pumpAndSettle();
},
);

// We expect the first ShortcutSettingTile to have one
// [KeyBadge] with `backquote` label
// which will confirm that we have changed the command
final theOnlyTile = tester.widget(find.byType(ShortcutSettingTile).first)
as ShortcutSettingTile;
expect(
theOnlyTile.command.command,
'backquote',
);

// hover on the ShortcutSettingTile and click the restore button
await tester.hoverOnWidget(
find.descendant(
of: find.byType(ShortcutSettingTile),
matching: find.text(pageUpCmdText),
),
onHover: () async {
await tester.tap(
find.descendant(
of: find.byType(ShortcutSettingTile).first,
matching: find.byFlowySvg(FlowySvgs.restore_s),
),
);
await tester.pumpAndSettle();
},
);

// We expect the first ShortcutSettingTile to have one
// [KeyBadge] with `page up` label
expect(
theOnlyTile.command.command,
defaultPageUpCmd,
);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ class ShortcutsCubit extends Cubit<ShortcutsState> {

final SettingsShortcutService service;

/// Fetches and updates shortcut data.
///
/// This method retrieves customizable shortcuts data from ShortcutService instance
/// and updates the command shortcuts based on the provided data and current state.
///
Future<void> fetchShortcuts() async {
emit(
state.copyWith(
Expand Down Expand Up @@ -82,6 +87,7 @@ class ShortcutsCubit extends Cubit<ShortcutsState> {
}
}

/// Saves all updated shortcuts to the Shortcut Service instance we are using.
Future<void> updateAllShortcuts() async {
emit(state.copyWith(status: ShortcutsStatus.updating, error: ''));

Expand All @@ -98,6 +104,7 @@ class ShortcutsCubit extends Cubit<ShortcutsState> {
}
}

/// This method resets all shortcuts to their default commands.
Future<void> resetToDefault() async {
emit(state.copyWith(status: ShortcutsStatus.updating, error: ''));

Expand All @@ -108,7 +115,39 @@ class ShortcutsCubit extends Cubit<ShortcutsState> {
emit(
state.copyWith(
status: ShortcutsStatus.failure,
error: LocaleKeys.settings_shortcutsPage_couldNotSaveErrorMsg.tr(),
error: LocaleKeys
.settings_shortcutsPage_couldNotResetShortcutsErrorMsg
.tr(),
),
);
}
}

/// Resets an individual shortcut to its default shortcut command.
/// Takes in the shortcut to reset.
Future<void> resetIndividualShortcut(CommandShortcutEvent shortcut) async {
emit(state.copyWith(status: ShortcutsStatus.updating, error: ''));

try {
// If no shortcut is found in the `defaultCommandShortcutEvents` then
// it will throw an error which will be handled by our catch block.
final defaultShortcut = defaultCommandShortcutEvents.firstWhere(
(el) => el.key == shortcut.key && el.handler == shortcut.handler,
);

// only update the shortcut if it is overidden
if (defaultShortcut.command != shortcut.command) {
shortcut.updateCommand(command: defaultShortcut.command);
await service.saveAllShortcuts(state.commandShortcutEvents);
}

emit(state.copyWith(status: ShortcutsStatus.success, error: ''));
} catch (e) {
emit(
state.copyWith(
status: ShortcutsStatus.failure,
error: LocaleKeys.settings_shortcutsPage_couldNotResetSingleErrorMsg
.tr(),
),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ class SettingsShortcutService {
late final File _file;
final _initCompleter = Completer<void>();

/// Takes in commandShortcuts as an input and saves them to the shortcuts.JSON file.
/// Takes in commandShortcuts as an input and saves them to the
/// `shortcuts.JSON` file.
Future<void> saveAllShortcuts(
List<CommandShortcutEvent> commandShortcuts,
) async {
Expand All @@ -39,9 +40,11 @@ class SettingsShortcutService {
);
}

/// 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.
/// Checks the file for saved shortcuts.
/// If shortcuts do NOT exist then returns an empty list.
/// If shortcuts exists, then calls an utility method:
///
/// `getShortcutsFromJson` - which returns the saved shortcuts.
Future<List<CommandShortcutModel>> getCustomizeShortcuts() async {
await _initCompleter.future;
final shortcutsInJson = await _file.readAsString();
Expand All @@ -53,8 +56,10 @@ class SettingsShortcutService {
}
}

/// 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.
/// 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;
Expand All @@ -77,13 +82,14 @@ class SettingsShortcutService {
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.
/// 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
/// returns the default file for storing shortcuts.
Future<File> _defaultShortcutFile() async {
final path = await getIt<ApplicationDataStorage>().getPath();
return File(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
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/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';
Expand Down Expand Up @@ -190,6 +191,7 @@ class _ResetButton extends StatelessWidget {
horizontal: 6,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const FlowySvg(
FlowySvgs.restore_s,
Expand Down Expand Up @@ -330,6 +332,18 @@ class _ShortcutSettingTileState extends State<ShortcutSettingTile> {
_finishEditing();
}

void _resetIndividualCommand(CommandShortcutEvent shortcut) {
context.read<ShortcutsCubit>().resetIndividualShortcut(shortcut);
}

bool canResetCommand(CommandShortcutEvent shortcut) {
final defaultShortcut = defaultCommandShortcutEvents.firstWhere(
(el) => el.key == shortcut.key && el.handler == shortcut.handler,
);

return defaultShortcut.command != shortcut.command;
}

@override
void dispose() {
focusNode.dispose();
Expand Down Expand Up @@ -382,40 +396,43 @@ class _ShortcutSettingTileState extends State<ShortcutSettingTile> {
);
}

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 _renderKeybindings(bool isHovering) {
final canReset = canResetCommand(widget.command);

return 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)
Row(
children: [
_EditShortcutBtn(
onEdit: () {
if (widget.canStartEditing()) {
setState(() {
widget.onStartEditing();
isEditing = true;
});
}
},
),
const HSpace(16),
_ResetShortcutBtn(
onReset: () => _resetIndividualCommand(widget.command),
canReset: canReset,
),
],
),
const HSpace(8),
],
);
}

Widget _renderKeybindEditor() => TapRegion(
onTapOutside: canClickOutside ? null : (_) => _finishEditing(),
Expand Down Expand Up @@ -459,6 +476,64 @@ class _ShortcutSettingTileState extends State<ShortcutSettingTile> {
}
}

class _EditShortcutBtn extends StatelessWidget {
const _EditShortcutBtn({super.key, required this.onEdit});

final VoidCallback onEdit;

@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onEdit,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: FlowyTooltip(
message: LocaleKeys.settings_shortcutsPage_editTooltip.tr(),
child: const FlowySvg(
FlowySvgs.edit_s,
size: Size.square(16),
),
),
),
);
}
}

class _ResetShortcutBtn extends StatelessWidget {
const _ResetShortcutBtn({
super.key,
required this.onReset,
required this.canReset,
});

final bool canReset;
final VoidCallback onReset;

@override
Widget build(BuildContext context) {
return Opacity(
opacity: canReset ? 1 : 0.5,
child: GestureDetector(
onTap: canReset ? onReset : null,
child: MouseRegion(
cursor: canReset ? SystemMouseCursors.click : MouseCursor.defer,
child: FlowyTooltip(
message: canReset
? LocaleKeys.settings_shortcutsPage_resetSingleTooltip.tr()
: LocaleKeys
.settings_shortcutsPage_unavailableResetSingleTooltip
.tr(),
child: const FlowySvg(
FlowySvgs.restore_s,
size: Size.square(16),
),
),
),
),
);
}
}

@visibleForTesting
class KeyBadge extends StatelessWidget {
const KeyBadge({super.key, required this.keyLabel});
Expand Down
Loading
Loading