Skip to content

Commit

Permalink
feat: support hardware keyboard event
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasXu0 committed Apr 24, 2023
1 parent cd2af20 commit b02c3ed
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -1,32 +1,21 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character_shortcut_event.dart';
import 'package:flutter/services.dart';

Future<bool> executeCharacterShortcutEvent(
EditorState editorState,
String character,
) async {
final shortcutEvents = editorState.characterShortcutEvents;
for (final shortcutEvent in shortcutEvents) {
if (shortcutEvent.character == character &&
await shortcutEvent.handler(editorState)) {
return true;
}
}
return false;
}

Future<void> onInsert(
TextEditingDeltaInsertion insertion,
EditorState editorState,
List<CharacterShortcutEvent> characterShortcutEvents,
) async {
Log.input.debug('onInsert: $insertion');

// character events
final character = insertion.textInserted;
if (character.length == 1) {
final execution = await executeCharacterShortcutEvent(
final execution = await _executeCharacterShortcutEvent(
editorState,
character,
characterShortcutEvents,
);
if (execution) {
return;
Expand Down Expand Up @@ -55,3 +44,17 @@ Future<void> onInsert(
throw UnimplementedError();
}
}

Future<bool> _executeCharacterShortcutEvent(
EditorState editorState,
String character,
List<CharacterShortcutEvent> characterShortcutEvents,
) async {
for (final shortcutEvent in characterShortcutEvents) {
if (shortcutEvent.character == character &&
await shortcutEvent.handler(editorState)) {
return true;
}
}
return false;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';

Future<void> onReplace(TextEditingDeltaReplacement replacement) async {
Future<void> onReplace(
TextEditingDeltaReplacement replacement,
EditorState editorState,
) async {
Log.input.debug('onReplace: $replacement');
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,20 @@ class DeltaTextInputService extends TextInputService with DeltaTextInputClient {
required super.onPerformAction,
});

TextInputConnection? textInputConnection;

@override
TextRange? composingTextRange;

@override
bool get attached => textInputConnection?.attached ?? false;
bool get attached => _textInputConnection?.attached ?? false;

@override
AutofillScope? get currentAutofillScope => throw UnimplementedError();

@override
TextEditingValue? get currentTextEditingValue => throw UnimplementedError();

TextInputConnection? _textInputConnection;

@override
Future<void> apply(List<TextEditingDelta> deltas) async {
final formattedDeltas = deltas.map((e) => e.format()).toList();
Expand All @@ -83,8 +83,9 @@ class DeltaTextInputService extends TextInputService with DeltaTextInputClient {

@override
void attach(TextEditingValue textEditingValue) {
if (textInputConnection == null || textInputConnection!.attached == false) {
textInputConnection = TextInput.attach(
if (_textInputConnection == null ||
_textInputConnection!.attached == false) {
_textInputConnection = TextInput.attach(
this,
const TextInputConfiguration(
enableDeltaModel: true,
Expand All @@ -96,7 +97,7 @@ class DeltaTextInputService extends TextInputService with DeltaTextInputClient {
}

final formattedValue = textEditingValue.format();
textInputConnection!
_textInputConnection!
..setEditingState(formattedValue)
..show();

Expand All @@ -107,8 +108,8 @@ class DeltaTextInputService extends TextInputService with DeltaTextInputClient {

@override
void close() {
textInputConnection?.close();
textInputConnection = null;
_textInputConnection?.close();
_textInputConnection = null;
}

@override
Expand All @@ -123,7 +124,7 @@ class DeltaTextInputService extends TextInputService with DeltaTextInputClient {
// Only support macOS now.
@override
void updateCaretPosition(Size size, Matrix4 transform, Rect rect) {
textInputConnection
_textInputConnection
?..setEditableSizeAndTransform(size, transform)
..setCaretRect(rect);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character_shortcut_event.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/command_shortcut_event.dart';
import 'package:appflowy_editor/src/editor/util/debounce.dart';
import 'package:appflowy_editor/src/editor/util/util.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'ime/delta_input_impl.dart';

// handle software keyboard and hardware keyboard
class KeyboardServiceWidget extends StatefulWidget {
const KeyboardServiceWidget({
super.key,
this.commandShortcutEvents = const [],
this.characterShortcutEvents = const [],
this.focusNode,
required this.child,
});

final FocusNode? focusNode;
final List<CommandShortcutEvent> commandShortcutEvents;
final List<CharacterShortcutEvent> characterShortcutEvents;
final Widget child;

@override
State<KeyboardServiceWidget> createState() => _KeyboardServiceWidgetState();
}

class _KeyboardServiceWidgetState extends State<KeyboardServiceWidget> {
late final TextInputService textInputService;
late final EditorState editorState;
late final TextInputService textInputService;
late final FocusNode focusNode;

@override
void initState() {
Expand All @@ -29,31 +40,83 @@ class _KeyboardServiceWidgetState extends State<KeyboardServiceWidget> {
editorState.selectionNotifier.addListener(_onSelectionChanged);

textInputService = DeltaTextInputService(
onInsert: (insertion) => onInsert(insertion, editorState),
onDelete: (deletion) => onDelete(deletion, editorState),
onReplace: onReplace,
onInsert: (insertion) async => await onInsert(
insertion,
editorState,
widget.characterShortcutEvents,
),
onDelete: (deletion) async => await onDelete(
deletion,
editorState,
),
onReplace: (replacement) async => await onReplace(
replacement,
editorState,
),
onNonTextUpdate: onNonTextUpdate,
onPerformAction: (action) => onPerformAction(action, editorState),
onPerformAction: (action) async => await onPerformAction(
action,
editorState,
),
);

focusNode = widget.focusNode ?? FocusNode(debugLabel: 'keyboard service');
}

@override
void dispose() {
editorState.selectionNotifier.removeListener(_onSelectionChanged);
if (widget.focusNode == null) {
focusNode.dispose();
}
super.dispose();
}

@override
Widget build(BuildContext context) {
if (widget.commandShortcutEvents.isNotEmpty) {
// the Focus widget is used to handle hardware keyboard.
return Focus(
focusNode: focusNode,
onKey: _onKey,
child: widget.child,
);
}
// if there is no command shortcut event, we don't need to handle hardware keyboard.
// like in read-only mode.
return widget.child;
}

/// handle hardware keyboard
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
if (event is! RawKeyDownEvent) {
return KeyEventResult.ignored;
}

for (final shortcutEvent in widget.commandShortcutEvents) {
// check if the shortcut event can respond to the raw key event
if (shortcutEvent.canRespondToRawKeyEvent(event)) {
final result = shortcutEvent.handler(editorState);
if (result == KeyEventResult.handled) {
return KeyEventResult.handled;
} else if (result == KeyEventResult.skipRemainingHandlers) {
return KeyEventResult.skipRemainingHandlers;
}
continue;
}
}

return KeyEventResult.ignored;
}

void _onSelectionChanged() {
// attach the delta text input service if needed
final selection = editorState.selection;
if (selection == null) {
textInputService.close();
} else {
// debounce the attachTextInputService function to avoid
// the text input service being attached too frequently.
Debounce.debounce(
'attachTextInputService',
const Duration(milliseconds: 200),
Expand All @@ -69,19 +132,26 @@ class _KeyboardServiceWidgetState extends State<KeyboardServiceWidget> {
}
}

// This function is used to get the current text editing value of the editor
// based on the given selection.
TextEditingValue? _getCurrentTextEditingValue(Selection selection) {
final editableNodes =
editorState.selectionService.currentSelectedNodes.where(
(element) => element.delta != null,
);
final selection = editorState.selection;
// Get all the editable nodes in the selection.
final editableNodes = editorState
.getNodesInSelection(selection)
.where((element) => element.delta != null);

// Get the composing text range.
final composingTextRange = textInputService.composingTextRange;
if (editableNodes.isNotEmpty && selection != null) {
if (editableNodes.isNotEmpty) {
// Get the text by concatenating all the editable nodes in the selection.
var text = editableNodes.fold<String>(
'',
(sum, editableNode) => '$sum${editableNode.delta!.toPlainText()}\n',
);

// Remove the last '\n'.
text = text.substring(0, text.length - 1);

return TextEditingValue(
text: text,
selection: TextSelection(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import 'dart:io';

import 'package:appflowy_editor/src/service/shortcut_event/keybinding.dart';
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

typedef CommandShortcutEventHandler = KeyEventResult Function(
EditorState editorState,
);

/// Defines the implementation of shortcut event based on command.
class CommandShortcutEvent {
Expand Down Expand Up @@ -45,7 +49,7 @@ class CommandShortcutEvent {
///
String command;

final ShortcutEventHandler handler;
final CommandShortcutEventHandler handler;

List<Keybinding> get keybindings => _keybindings;
List<Keybinding> _keybindings = [];
Expand Down Expand Up @@ -98,10 +102,14 @@ class CommandShortcutEvent {
}
}

bool canRespondToRawKeyEvent(RawKeyEvent event) {
return keybindings.containsKeyEvent(event);
}

CommandShortcutEvent copyWith({
String? key,
String? command,
ShortcutEventHandler? handler,
CommandShortcutEventHandler? handler,
}) {
return CommandShortcutEvent(
key: key ?? this.key,
Expand Down
7 changes: 7 additions & 0 deletions lib/src/editor/util/platform_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import 'dart:io';
import 'package:flutter/foundation.dart';

extension PlatformExtension on Platform {
static bool get isDesktopOrWeb {
if (kIsWeb) {
return true;
}
return isDesktop;
}

static bool get isDesktop {
if (kIsWeb) {
return false;
Expand Down

0 comments on commit b02c3ed

Please sign in to comment.