diff --git a/super_editor/example/lib/demos/example_editor/example_editor.dart b/super_editor/example/lib/demos/example_editor/example_editor.dart index 687dfa3b38..4e1fa81839 100644 --- a/super_editor/example/lib/demos/example_editor/example_editor.dart +++ b/super_editor/example/lib/demos/example_editor/example_editor.dart @@ -1,3 +1,4 @@ +import 'package:example/demos/example_editor/text_input_visualizer.dart'; import 'package:example/logging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -43,6 +44,9 @@ class _ExampleEditorState extends State { final _overlayController = MagnifierAndToolbarController() // ..screenPadding = const EdgeInsets.all(20.0); + final TextInputDebugger _textInputDebugger = TextInputDebugger(); + bool _showImeDebugger = false; + @override void initState() { super.initState(); @@ -319,20 +323,33 @@ class _ExampleEditorState extends State { child: Builder( builder: (themedContext) { // This builder captures the new theme - return Stack( + return Row( children: [ - Column( - children: [ - Expanded( - child: _buildEditor(themedContext), - ), - if (_isMobile) _buildMountedToolbar(), - ], - ), - Align( - alignment: Alignment.bottomRight, - child: _buildCornerFabs(), + Expanded( + child: Stack( + children: [ + Column( + children: [ + Expanded( + child: _buildEditor(themedContext), + ), + if (_isMobile) _buildMountedToolbar(), + ], + ), + Align( + alignment: Alignment.bottomRight, + child: _buildCornerFabs(), + ), + ], + ), ), + if (_showImeDebugger) + SizedBox( + width: 400, + child: SuperEditorImeDebugger( + debugger: _textInputDebugger, + ), + ), ], ); }, @@ -349,6 +366,8 @@ class _ExampleEditorState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ + _buildImeDebuggerToggle(), + const SizedBox(height: 16), _buildDebugVisualsToggle(), const SizedBox(height: 16), _buildLightAndDarkModeToggle(), @@ -357,6 +376,28 @@ class _ExampleEditorState extends State { ); } + Widget _buildImeDebuggerToggle() { + return FloatingActionButton( + backgroundColor: _brightness.value == Brightness.light ? _darkBackground : _lightBackground, + foregroundColor: _brightness.value == Brightness.light ? _lightBackground : _darkBackground, + elevation: 5, + onPressed: () { + setState(() { + _showImeDebugger = !_showImeDebugger; + + if (_showImeDebugger) { + _textInputDebugger.enable(); + } else { + _textInputDebugger.disable(); + } + }); + }, + child: const Icon( + Icons.translate, + ), + ); + } + Widget _buildDebugVisualsToggle() { return FloatingActionButton( backgroundColor: _brightness.value == Brightness.light ? _darkBackground : _lightBackground, @@ -410,6 +451,7 @@ class _ExampleEditorState extends State { focusNode: _editorFocusNode, scrollController: _scrollController, documentLayoutKey: _docLayoutKey, + textInputDebugger: _textInputDebugger, documentOverlayBuilders: [ DefaultCaretOverlayBuilder( caretStyle: const CaretStyle().copyWith(color: isLight ? Colors.black : Colors.redAccent), diff --git a/super_editor/example/lib/demos/example_editor/text_input_visualizer.dart b/super_editor/example/lib/demos/example_editor/text_input_visualizer.dart new file mode 100644 index 0000000000..691827498f --- /dev/null +++ b/super_editor/example/lib/demos/example_editor/text_input_visualizer.dart @@ -0,0 +1,614 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:super_editor/src/infrastructure/flutter/flutter_pipeline.dart'; +import 'package:super_editor/super_editor.dart'; +import 'package:super_text_layout/super_text_layout.dart'; + +/// Displays debug information related to text input. +/// +/// Shows a list of events, which could be generated by the OS +/// or the editor. +class SuperEditorImeDebugger extends StatefulWidget { + const SuperEditorImeDebugger({ + Key? key, + required this.debugger, + this.componentBuilders = defaultTextInputDebugComponentBuilders, + }) : super(key: key); + + /// The object that collects and holds the debug events. + final TextInputDebugger debugger; + + /// Priority list of widget factories that create the visual representation + /// of the events. + final List componentBuilders; + + @override + State createState() => _SuperEditorImeDebuggerState(); +} + +class _SuperEditorImeDebuggerState extends State { + final ScrollController _scrollController = ScrollController(); + + bool _isExpanded = false; + + @override + void initState() { + super.initState(); + widget.debugger.addListener(_scrollToTheEndOnNextFrame); + _scrollToTheEndOnNextFrame(); + } + + @override + void didUpdateWidget(covariant SuperEditorImeDebugger oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.debugger != widget.debugger) { + oldWidget.debugger.removeListener(_scrollToTheEndOnNextFrame); + widget.debugger.addListener(_scrollToTheEndOnNextFrame); + _scrollToTheEndOnNextFrame(); + } + } + + @override + void dispose() { + widget.debugger.removeListener(_scrollToTheEndOnNextFrame); + _scrollController.dispose(); + super.dispose(); + } + + void _scrollToTheEndOnNextFrame() { + onNextFrame((_) => _scrollController.jumpTo(_scrollController.position.maxScrollExtent)); + } + + @override + Widget build(BuildContext context) { + return ColoredBox( + color: const Color(0xFF222222), + child: ListenableBuilder( + listenable: widget.debugger, + builder: (context, child) { + return Column( + children: [ + _buildToolbar(), + Expanded( + child: ListView.separated( + controller: _scrollController, + itemCount: widget.debugger.events.length, + itemBuilder: (context, index) => _buildEvent(context, widget.debugger.events[index]), + separatorBuilder: (context, index) => SizedBox(height: 20), + ), + ), + ], + ); + }, + ), + ); + } + + Widget _buildToolbar() { + return Material( + elevation: 5, + color: Colors.black, + child: SizedBox( + height: 40, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + children: [ + IconButton( + onPressed: widget.debugger.clear, + hoverColor: const Color(0xFF222222), + icon: Icon( + Icons.clear, + color: Colors.white, + ), + ), + IconButton( + onPressed: () { + setState(() { + _isExpanded = !_isExpanded; + }); + }, + hoverColor: const Color(0xFF222222), + icon: Icon( + _isExpanded ? Icons.close_fullscreen : Icons.open_in_full, + color: Colors.white, + ), + ), + Spacer(), + CircleAvatar( + maxRadius: 16, + backgroundColor: const Color(0xFF222222), + child: Text( + widget.debugger.events.length.toString(), + style: TextStyle( + fontSize: 16, + color: Colors.white, + ), + ), + ), + ], + ), + ), + ), + ); + } + + /// Builds the visual representation of the [event]. + /// + /// Throws if there isn't a component builder for this event. + Widget _buildEvent(BuildContext context, TextInputDebugEvent event) { + Widget? content; + for (final builder in widget.componentBuilders) { + content = builder(context, event, _isExpanded); + if (content != null) { + break; + } + } + + if (content == null) { + throw Exception('No component for $event'); + } + + return content; + } +} + +typedef TextInputDebugComponentBuilder = Widget? Function( + BuildContext context, TextInputDebugEvent event, bool detailed); + +/// Component builders that generate a visual representation for the events. +const defaultTextInputDebugComponentBuilders = [ + _buildSetEditingStateEvent, + _buildTextDeltaEvents, + _buildKeyEvent, + _buildGenericEvent, +]; + +/// Build the widget for a `setEditingState` call. +/// +/// When [detailed] is `false`, only the minimum relevant information should be shown. +/// For example, when using an ExpansionTile, it should be expanded or collapsed based on [detailed]. +Widget? _buildSetEditingStateEvent(BuildContext context, TextInputDebugEvent event, bool detailed) { + final textEditingValue = event.data; + if (textEditingValue is! TextEditingValue) { + return null; + } + + return _ExpandableTile( + expanded: detailed, + leading: Icon( + Icons.arrow_upward, + color: Colors.green, + ), + title: Text( + event.method, + style: TextStyle( + fontWeight: FontWeight.bold, + color: _fontColor, + ), + ), + children: [ + _createLabelAndValue( + label: 'Text', + text: textEditingValue.text, + selection: textEditingValue.selection, + ), + SizedBox(height: 5), + _createLabelAndValue( + label: 'Selection', + text: textEditingValue.selection.toString(), + ), + SizedBox(height: 5), + _createLabelAndValue( + label: 'Composing region', + text: textEditingValue.composing.toString(), + ), + ], + ); +} + +/// Build the widget for an `updateEditingStateWithDeltas` call. +Widget? _buildTextDeltaEvents(BuildContext context, TextInputDebugEvent event, bool detailed) { + final params = event.data; + if (params is! List) { + return null; + } + + final deltaComponents = params.map( + (delta) { + if (delta is TextEditingDeltaInsertion) { + return _buildInsertionDeltaEvent(context, delta); + } + + if (delta is TextEditingDeltaReplacement) { + return _buildReplacementDeltaEvent(context, delta); + } + + if (delta is TextEditingDeltaDeletion) { + return _buildDeletionDeltaEvent(context, delta); + } + + if (delta is TextEditingDeltaNonTextUpdate) { + return _buildNonTextDeltaEvent(context, delta); + } + + return SizedBox(); + }, + ).toList(); + return _ExpandableTile( + expanded: detailed, + leading: Icon( + Icons.arrow_downward, + color: Colors.blue, + ), + title: Text( + event.method, + style: TextStyle( + fontWeight: FontWeight.bold, + color: _fontColor, + ), + ), + children: deltaComponents, + ); +} + +/// Build the widget for a `TextEditingDeltaInsertion`. +Widget _buildInsertionDeltaEvent(BuildContext context, TextEditingDeltaInsertion delta) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _createLabelAndValue( + label: 'Delta kind', + text: delta.runtimeType.toString(), + ), + SizedBox(height: 5), + _createLabelAndValue( + label: 'Old Text', + text: delta.oldText, + ), + SizedBox(height: 5), + _createLabelAndValue( + label: 'Inserted Text', + text: delta.textInserted, + ), + SizedBox(height: 5), + _createLabelAndValue( + label: 'Insertion Offset', + text: delta.insertionOffset.toString(), + ), + SizedBox(height: 5), + _createLabelAndValue( + label: 'IME new text', + text: delta.apply(TextEditingValue(text: delta.oldText)).text, + attribution: _insertedTextAttribution, + range: TextRange( + start: delta.insertionOffset, + end: delta.insertionOffset + delta.textInserted.length, + ), + selection: delta.selection, + ), + _createLabelAndValue( + label: 'Selection', + text: delta.selection.toString(), + ), + SizedBox(height: 5), + _createLabelAndValue( + label: 'Composing region', + text: delta.composing.toString(), + ), + ], + ); +} + +/// Build the widget for a `TextEditingDeltaReplacement`. +Widget _buildReplacementDeltaEvent(BuildContext context, TextEditingDeltaReplacement delta) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _createLabelAndValue( + label: 'Delta kind', + text: delta.runtimeType.toString(), + ), + SizedBox(height: 5), + _createLabelAndValue( + label: 'Old Text', + text: delta.oldText, + attribution: _textRemovedAttribution, + range: delta.replacedRange, + ), + SizedBox(height: 5), + _createLabelAndValue( + label: 'Range', + text: delta.replacedRange.toString(), + ), + SizedBox(height: 5), + _createLabelAndValue( + label: 'IME new text', + text: delta.apply(TextEditingValue(text: delta.oldText)).text, + attribution: _insertedTextAttribution, + range: TextRange( + start: delta.replacedRange.start, + end: delta.replacedRange.start + delta.replacementText.length, + ), + selection: delta.selection, + ), + _createLabelAndValue( + label: 'Selection', + text: delta.selection.toString(), + ), + SizedBox(height: 5), + _createLabelAndValue( + label: 'Composing region', + text: delta.composing.toString(), + ), + ], + ); +} + +/// Build the widget for a `TextEditingDeltaDeletion`. +Widget _buildDeletionDeltaEvent(BuildContext context, TextEditingDeltaDeletion delta) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _createLabelAndValue( + label: 'Delta kind', + text: delta.runtimeType.toString(), + ), + SizedBox(height: 5), + _createLabelAndValue( + label: 'OldText', + text: delta.oldText, + attribution: _textRemovedAttribution, + range: delta.deletedRange, + ), + SizedBox(height: 5), + _createLabelAndValue( + label: 'Range', + text: delta.deletedRange.toString(), + ), + SizedBox(height: 5), + _createLabelAndValue( + label: 'IME new text', + text: delta.apply(TextEditingValue(text: delta.oldText)).text, + selection: delta.selection, + ), + _createLabelAndValue( + label: 'Selection', + text: delta.selection.toString(), + ), + SizedBox(height: 5), + _createLabelAndValue( + label: 'Composing region', + text: delta.composing.toString(), + ), + ], + ); +} + +/// Build the widget for a `TextEditingDeltaNonTextUpdate`. +Widget _buildNonTextDeltaEvent(BuildContext context, TextEditingDeltaNonTextUpdate delta) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _createLabelAndValue( + label: 'Delta kind', + text: delta.runtimeType.toString(), + ), + SizedBox(height: 5), + _createLabelAndValue( + label: 'IME Text', + text: delta.apply(TextEditingValue(text: delta.oldText)).text, + selection: delta.selection, + ), + _createLabelAndValue( + label: 'Selection', + text: delta.selection.toString(), + ), + SizedBox(height: 5), + _createLabelAndValue( + label: 'Composing region', + text: delta.composing.toString(), + ), + ], + ); +} + +/// Build the widget for an `onKey` call. +Widget? _buildKeyEvent(BuildContext context, TextInputDebugEvent event, bool detailed) { + final keyEvent = event.data; + if (keyEvent is! RawKeyDownEvent) { + return null; + } + + return _ExpandableTile( + expanded: detailed, + leading: Icon( + Icons.arrow_downward, + color: Colors.blue, + ), + title: Text( + event.method, + style: TextStyle( + fontWeight: FontWeight.bold, + color: _fontColor, + ), + ), + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _createLabelAndValue( + label: 'Key', + text: keyEvent.data.physicalKey.debugName ?? keyEvent.data.keyLabel, + ), + ], + ), + ], + ); +} + +/// Build the widget for any given [event]. +Widget _buildGenericEvent(BuildContext context, TextInputDebugEvent event, bool detailed) { + return _ExpandableTile( + expanded: detailed, + leading: Icon( + Icons.info, + color: Colors.blue, + ), + title: Text( + event.method, + style: TextStyle( + fontWeight: FontWeight.bold, + color: _fontColor, + ), + ), + children: [ + Text(event.data.toString()), + ], + ); +} + +/// Applies styles to highlight text insertions and deletions. +TextStyle _deltaTextStyler(Set attributions) { + TextStyle newStyle = TextStyle( + color: Color(0xFFCCCCCC), + fontSize: 14, + ); + + if (attributions.contains(boldAttribution)) { + newStyle = newStyle.copyWith(fontWeight: FontWeight.bold); + } + + if (attributions.contains(_insertedTextAttribution)) { + newStyle = newStyle.copyWith( + decoration: TextDecoration.underline, + color: Colors.blue, + fontWeight: FontWeight.bold, + ); + } + + if (attributions.contains(_textRemovedAttribution)) { + newStyle = newStyle.copyWith( + decoration: TextDecoration.underline, + color: Colors.red, + fontWeight: FontWeight.bold, + ); + } + + return newStyle; +} + +/// If [selection] is given, the selection +Widget _createLabelAndValue({ + required String label, + required String text, + Attribution? attribution, + TextRange? range, + TextSelection? selection, +}) { + // Adjust the selection to account for the label. + final transposedSelection = selection != null // + ? selection.copyWith( + baseOffset: selection.baseOffset + label.length + 2, + extentOffset: selection.extentOffset + label.length + 2, + ) + : null; + + final spans = AttributedSpans() + ..addAttribution( + newAttribution: boldAttribution, + start: 0, + end: label.length, + ); + + if (attribution != null && range != null) { + spans.addAttribution( + newAttribution: attribution, + start: range.start + label.length + 2, + end: range.end + label.length + 1, + ); + } + + return SuperTextWithSelection.single( + richText: AttributedText( + text: '$label: $text', + spans: spans, + ).computeTextSpan(_deltaTextStyler), + userSelection: selection != null // + ? UserSelection( + selection: transposedSelection!, + blinkCaret: false, + ) + : null, + ); +} + +/// [Attribution] which marks a text range as inserted. +const _insertedTextAttribution = const NamedAttribution('textInsertedAttribution'); + +/// [Attribution] which marks a range as removed. +const _textRemovedAttribution = const NamedAttribution('textRemovedAttribution'); + +const _fontColor = Color(0xFFCCCCCC); + +/// An [ExpansionTile] which expands and collapses based on [expanded]. +class _ExpandableTile extends StatefulWidget { + const _ExpandableTile({ + required this.title, + required this.leading, + required this.children, + required this.expanded, + }); + + /// The primary content of the tile. + final Widget title; + + /// A widget to display before the [title]. + final Widget leading; + + /// Controls if the tile is expanded or collapsed. + final bool expanded; + + /// The widgets that are displayed when the tile is expanded. + final List children; + + @override + State<_ExpandableTile> createState() => _ExpandableTileState(); +} + +class _ExpandableTileState extends State<_ExpandableTile> { + final ExpansionTileController _controller = ExpansionTileController(); + + @override + void didUpdateWidget(covariant _ExpandableTile oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.expanded != widget.expanded) { + if (widget.expanded) { + _controller.expand(); + } else { + _controller.collapse(); + } + } + } + + @override + Widget build(BuildContext context) { + return ExpansionTile( + controller: _controller, + expandedCrossAxisAlignment: CrossAxisAlignment.start, + expandedAlignment: Alignment.topLeft, + collapsedIconColor: Colors.white, + iconColor: Colors.white, + childrenPadding: EdgeInsets.all(8.0), + leading: widget.leading, + title: widget.title, + initiallyExpanded: widget.expanded, + children: widget.children, + ); + } +} diff --git a/super_editor/lib/src/default_editor/document_hardware_keyboard/document_physical_keyboard.dart b/super_editor/lib/src/default_editor/document_hardware_keyboard/document_physical_keyboard.dart index 827f6b542e..f9839302be 100644 --- a/super_editor/lib/src/default_editor/document_hardware_keyboard/document_physical_keyboard.dart +++ b/super_editor/lib/src/default_editor/document_hardware_keyboard/document_physical_keyboard.dart @@ -1,6 +1,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:super_editor/src/core/edit_context.dart'; +import 'package:super_editor/src/default_editor/document_ime/document_ime_communication.dart'; import 'package:super_editor/src/infrastructure/_logging.dart'; import 'package:super_editor/src/infrastructure/keyboard.dart'; @@ -22,6 +23,7 @@ class SuperEditorHardwareKeyHandler extends StatefulWidget { required this.editContext, this.keyboardActions = const [], this.autofocus = false, + this.textInputDebugger, required this.child, }) : super(key: key); @@ -43,6 +45,9 @@ class SuperEditorHardwareKeyHandler extends StatefulWidget { /// Whether or not the [SuperEditorHardwareKeyHandler] should autofocus final bool autofocus; + /// Event collector for debugging purposes. + final TextInputDebugger? textInputDebugger; + /// The [child] widget, which is expected to include the document UI /// somewhere in the sub-tree. final Widget child; @@ -69,6 +74,15 @@ class _SuperEditorHardwareKeyHandlerState extends State textEditingDeltas) { + debugger?.add( + TextInputDebugEvent( + method: 'updateEditingValueWithDeltas', + data: textEditingDeltas, + ), + ); + if (textEditingDeltas.isEmpty) { return; } @@ -246,6 +265,12 @@ class DocumentImeInputClient extends TextInputConnectionDecorator with TextInput @override void performAction(TextInputAction action) { + debugger?.add( + TextInputDebugEvent( + method: 'performAction', + data: action, + ), + ); editorImeLog.fine("IME says to perform action: $action"); if (action == TextInputAction.newline) { textDeltasDocumentEditor.insertNewline(); @@ -281,3 +306,88 @@ class DocumentImeInputClient extends TextInputConnectionDecorator with TextInput editorImeLog.info("IME connection was closed"); } } + +/// Collects events related to text input for debugging purposes. +/// +/// These events might be key events, deltas received from the platform or editing +/// state sent to the platform. +/// +/// Call `enable` to start collecting. +/// +/// Call `disable` to stop collecting. +class TextInputDebugger with ChangeNotifier { + /// Whether or not event should be collected. + bool get enabled => _enabled; + bool _enabled = false; + + /// All events collected since the debugger was enabled. + List get events => UnmodifiableListView(_events); + final List _events = []; + + /// Add the [event] to the list. + /// + /// Does nothing if not [enabled]. + int add(TextInputDebugEvent event) { + if (!_enabled) { + return -1; + } + + _events.add(event); + notifyListeners(); + return _events.length - 1; + } + + /// Removs the event at [index] from the list. + /// + /// Does nothing if not [enabled]. + void removeAt(int index) { + if (!_enabled) { + return; + } + + _events.removeAt(index); + } + + /// Remove all collected events. + void clear() { + if (!_enabled) { + return; + } + + _events.clear(); + notifyListeners(); + } + + /// Start collecting events. + void enable() { + _enabled = true; + notifyListeners(); + } + + /// Stop collecting events. + /// + /// After calling this method, calling [add], [removeAt] or [clear] has no effect. + void disable() { + _enabled = false; + notifyListeners(); + } +} + +/// An event related to text input. +class TextInputDebugEvent { + TextInputDebugEvent({ + required this.method, + required this.data, + }); + + /// The method that generated the event. + /// + /// For example, `updateEditingValueWithDeltas`. + final String method; + + /// The event data. + /// + /// For example, for `updateEditingValueWithDeltas`, + /// [data] is the delta list received. + final dynamic data; +} diff --git a/super_editor/lib/src/default_editor/document_ime/supereditor_ime_interactor.dart b/super_editor/lib/src/default_editor/document_ime/supereditor_ime_interactor.dart index f874d813ee..75d8f302d8 100644 --- a/super_editor/lib/src/default_editor/document_ime/supereditor_ime_interactor.dart +++ b/super_editor/lib/src/default_editor/document_ime/supereditor_ime_interactor.dart @@ -34,6 +34,7 @@ class SuperEditorImeInteractor extends StatefulWidget { this.imePolicies = const SuperEditorImePolicies(), this.imeConfiguration = const SuperEditorImeConfiguration(), this.imeOverrides, + this.textInputDebugger, this.hardwareKeyboardActions = const [], this.floatingCursorController, required this.child, @@ -116,6 +117,9 @@ class SuperEditorImeInteractor extends StatefulWidget { /// a property on this IME interactor. final FloatingCursorController? floatingCursorController; + /// Event collector for debugging purposes. + final TextInputDebugger? textInputDebugger; + final Widget child; @override @@ -163,6 +167,7 @@ class SuperEditorImeInteractorState extends State impl textDeltasDocumentEditor: _textDeltasDocumentEditor, imeConnection: _imeConnection, floatingCursorController: widget.floatingCursorController, + debugger: widget.textInputDebugger, ); _imeClient = DeltaTextInputClientDecorator(); @@ -296,6 +301,7 @@ class SuperEditorImeInteractorState extends State impl editContext: widget.editContext, keyboardActions: widget.hardwareKeyboardActions, autofocus: widget.autofocus, + textInputDebugger: widget.textInputDebugger, child: DocumentSelectionOpenAndCloseImePolicy( focusNode: _focusNode, editor: widget.editContext.editor, diff --git a/super_editor/lib/src/default_editor/super_editor.dart b/super_editor/lib/src/default_editor/super_editor.dart index 723857bab0..ca8f4ef76a 100644 --- a/super_editor/lib/src/default_editor/super_editor.dart +++ b/super_editor/lib/src/default_editor/super_editor.dart @@ -119,6 +119,7 @@ class SuperEditor extends StatefulWidget { this.overlayController, this.plugins = const {}, this.debugPaint = const DebugPaintConfig(), + this.textInputDebugger, }) : stylesheet = stylesheet ?? defaultStylesheet, selectionStyles = selectionStyle ?? defaultSelectionStyle, keyboardActions = keyboardActions ?? @@ -272,6 +273,9 @@ class SuperEditor extends StatefulWidget { /// Plugins that add sets of behaviors to the editing experience. final Set plugins; + /// Event collector for debugging purposes. + final TextInputDebugger? textInputDebugger; + /// Paints some extra visual ornamentation to help with /// debugging. final DebugPaintConfig debugPaint; @@ -548,6 +552,7 @@ class SuperEditorState extends State { ...plugin.keyboardActions, ...widget.keyboardActions, ], + textInputDebugger: widget.textInputDebugger, child: child, ); case TextInputSource.ime: @@ -567,6 +572,7 @@ class SuperEditorState extends State { ...widget.keyboardActions, ], floatingCursorController: _floatingCursorController, + textInputDebugger: widget.textInputDebugger, child: child, ); }