From b4418af74780e1a73fe8e7527609f094c837bec1 Mon Sep 17 00:00:00 2001 From: Angelo Silvestre Date: Mon, 7 Aug 2023 22:58:49 -0300 Subject: [PATCH 1/6] [SuperEditor] Create IME visualizer (Resolves #1120) --- .../demos/example_editor/example_editor.dart | 66 +++++++++-- .../document_physical_keyboard.dart | 15 +++ .../document_ime_communication.dart | 109 ++++++++++++++++++ .../supereditor_ime_interactor.dart | 5 + .../lib/src/default_editor/super_editor.dart | 6 + 5 files changed, 189 insertions(+), 12 deletions(-) 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..fb3c54ba6d 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( + watcher: _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/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..a5a5584f54 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,8 @@ class SuperEditorHardwareKeyHandler extends StatefulWidget { /// Whether or not the [SuperEditorHardwareKeyHandler] should autofocus final bool autofocus; + final TextInputDebugger? textInputDebugger; + /// The [child] widget, which is expected to include the document UI /// somewhere in the sub-tree. final Widget child; @@ -69,6 +73,13 @@ class _SuperEditorHardwareKeyHandlerState extends State textEditingDeltas) { + debugger?.add( + TextInputDebugEvent( + method: 'updateEditingValueWithDeltas', + data: textEditingDeltas, + ), + ); + if (textEditingDeltas.isEmpty) { return; } @@ -246,6 +264,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 +305,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..393f51b9c9 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,8 @@ class SuperEditorImeInteractor extends StatefulWidget { /// a property on this IME interactor. final FloatingCursorController? floatingCursorController; + final TextInputDebugger? textInputDebugger; + final Widget child; @override @@ -163,6 +166,7 @@ class SuperEditorImeInteractorState extends State impl textDeltasDocumentEditor: _textDeltasDocumentEditor, imeConnection: _imeConnection, floatingCursorController: widget.floatingCursorController, + debugger: widget.textInputDebugger, ); _imeClient = DeltaTextInputClientDecorator(); @@ -296,6 +300,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..6ff6787528 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,8 @@ class SuperEditor extends StatefulWidget { /// Plugins that add sets of behaviors to the editing experience. final Set plugins; + final TextInputDebugger? textInputDebugger; + /// Paints some extra visual ornamentation to help with /// debugging. final DebugPaintConfig debugPaint; @@ -548,6 +551,8 @@ class SuperEditorState extends State { ...plugin.keyboardActions, ...widget.keyboardActions, ], + 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, ); } From ee7de54fe5d1dd0269805938b6e0c8b7b66e5f3b Mon Sep 17 00:00:00 2001 From: Angelo Silvestre Date: Tue, 8 Aug 2023 19:57:07 -0300 Subject: [PATCH 2/6] Missing file --- .../example_editor/text_input_visualizer.dart | 500 ++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100644 super_editor/example/lib/demos/example_editor/text_input_visualizer.dart 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..4c0475ee86 --- /dev/null +++ b/super_editor/example/lib/demos/example_editor/text_input_visualizer.dart @@ -0,0 +1,500 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:super_editor/super_editor.dart'; +import 'package:super_text_layout/super_text_layout.dart'; + +class SuperEditorImeDebugger extends StatefulWidget { + const SuperEditorImeDebugger({ + Key? key, + required this.watcher, + this.componentBuilders = const [ + _buildSetEditingTextEvent, + _buildTextDeltaEvents, + _buildKeyEvent, + _buildGenericEvent, + ], + }) : super(key: key); + + final TextInputDebugger watcher; + + final List componentBuilders; + + @override + State createState() => _SuperEditorImeDebuggerState(); +} + +class _SuperEditorImeDebuggerState extends State { + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + widget.watcher.addListener(_scrollOnNextFrame); + _scrollOnNextFrame(); + } + + @override + void didUpdateWidget(covariant SuperEditorImeDebugger oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.watcher != widget.watcher) { + oldWidget.watcher.removeListener(_scrollOnNextFrame); + widget.watcher.addListener(_scrollOnNextFrame); + _scrollOnNextFrame(); + } + } + + @override + void dispose() { + widget.watcher.removeListener(_scrollOnNextFrame); + _scrollController.dispose(); + super.dispose(); + } + + void _scrollOnNextFrame() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + if (!mounted) { + return; + } + + _scrollController.jumpTo(_scrollController.position.maxScrollExtent); + }); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Expanded( + child: ListenableBuilder( + listenable: widget.watcher, + builder: (context, child) { + return ListView.separated( + controller: _scrollController, + itemCount: widget.watcher.events.length, + itemBuilder: (context, index) => _buildLogItem(context, widget.watcher.events[index]), + separatorBuilder: (context, index) => SizedBox(height: 20), + ); + }, + ), + ), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: widget.watcher.clear, + child: Text('Clear'), + ), + ), + ], + ), + ); + } + + Widget _buildLogItem(BuildContext context, TextInputDebugEvent event) { + Widget? content; + for (final builder in widget.componentBuilders) { + content = builder(context, event); + if (content != null) { + break; + } + } + + if (content == null) { + throw Exception('No component for $event'); + } + + return content; + } +} + +typedef TextInputDebugComponentBuilder = Widget? Function(BuildContext context, TextInputDebugEvent event); + +const defaultTextInputDebugComponentBuilders = [ + _buildSetEditingTextEvent, + _buildTextDeltaEvents, + _buildKeyEvent, + _buildGenericEvent, +]; + +Widget? _buildSetEditingTextEvent(BuildContext context, TextInputDebugEvent event) { + final textEditingValue = event.data; + if (textEditingValue is! TextEditingValue) { + return null; + } + + return ListTile( + leading: Icon( + Icons.arrow_upward, + color: Colors.green, + ), + title: Text( + event.method, + style: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _createTextWithSingleAttribution( + label: 'Text', + text: textEditingValue.text, + attribution: _insertionOffsetAttribution, + selection: textEditingValue.selection, + ), + SizedBox(height: 5), + _createTextWithSingleAttribution( + label: 'Selection', + text: textEditingValue.selection.toString(), + ), + SizedBox(height: 5), + _createTextWithSingleAttribution( + label: 'Composing region', + text: textEditingValue.composing.toString(), + ), + ], + ), + ); +} + +Widget? _buildTextDeltaEvents(BuildContext context, TextInputDebugEvent event) { + 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 ListTile( + leading: Icon( + Icons.arrow_downward, + color: Colors.blue, + ), + title: Text( + event.method, + style: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...deltaComponents, + ], + ), + ); +} + +Widget _buildInsertionDeltaEvent(BuildContext context, TextEditingDeltaInsertion delta) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _createTextWithSingleAttribution( + label: 'Delta kind', + text: delta.runtimeType.toString(), + ), + SizedBox(height: 5), + _createTextWithSingleAttribution( + label: 'OldText', + text: delta.oldText, + attribution: _insertionOffsetAttribution, + range: TextRange( + start: delta.insertionOffset, + end: delta.insertionOffset, + ), + ), + SizedBox(height: 5), + _createTextWithSingleAttribution( + label: 'Inserted Text', + text: delta.textInserted, + ), + SizedBox(height: 5), + _createTextWithSingleAttribution( + label: 'Insertion Offset', + text: delta.insertionOffset.toString(), + ), + SizedBox(height: 5), + _createTextWithSingleAttribution( + label: 'IME new text', + text: delta.apply(TextEditingValue(text: delta.oldText)).text, + attribution: _newTextAttribution, + range: TextRange( + start: delta.insertionOffset, + end: delta.insertionOffset + delta.textInserted.length, + ), + selection: delta.selection, + ), + _createTextWithSingleAttribution( + label: 'Selection', + text: delta.selection.toString(), + ), + SizedBox(height: 5), + _createTextWithSingleAttribution( + label: 'Composing region', + text: delta.composing.toString(), + ), + ], + ); +} + +Widget _buildReplacementDeltaEvent(BuildContext context, TextEditingDeltaReplacement delta) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _createTextWithSingleAttribution( + label: 'Delta kind', + text: delta.runtimeType.toString(), + ), + SizedBox(height: 5), + _createTextWithSingleAttribution( + label: 'OldText', + text: delta.oldText, + attribution: _textRemovedAttribution, + range: delta.replacedRange, + ), + SizedBox(height: 5), + _createTextWithSingleAttribution( + label: 'Range', + text: delta.replacedRange.toString(), + ), + SizedBox(height: 5), + _createTextWithSingleAttribution( + label: 'IME new text', + text: delta.apply(TextEditingValue(text: delta.oldText)).text, + attribution: _newTextAttribution, + range: TextRange( + start: delta.replacedRange.start, + end: delta.replacedRange.start + delta.replacementText.length, + ), + selection: delta.selection, + ), + _createTextWithSingleAttribution( + label: 'Selection', + text: delta.selection.toString(), + ), + SizedBox(height: 5), + _createTextWithSingleAttribution( + label: 'Composing region', + text: delta.composing.toString(), + ), + ], + ); +} + +Widget _buildDeletionDeltaEvent(BuildContext context, TextEditingDeltaDeletion delta) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _createTextWithSingleAttribution( + label: 'Delta kind', + text: delta.runtimeType.toString(), + ), + SizedBox(height: 5), + _createTextWithSingleAttribution( + label: 'OldText', + text: delta.oldText, + attribution: _textRemovedAttribution, + range: delta.deletedRange, + ), + SizedBox(height: 5), + _createTextWithSingleAttribution( + label: 'Range', + text: delta.deletedRange.toString(), + ), + SizedBox(height: 5), + _createTextWithSingleAttribution( + label: 'IME new text', + text: delta.apply(TextEditingValue(text: delta.oldText)).text, + selection: delta.selection, + ), + _createTextWithSingleAttribution( + label: 'Selection', + text: delta.selection.toString(), + ), + SizedBox(height: 5), + _createTextWithSingleAttribution( + label: 'Composing region', + text: delta.composing.toString(), + ), + ], + ); +} + +Widget _buildNonTextDeltaEvent(BuildContext context, TextEditingDeltaNonTextUpdate delta) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _createTextWithSingleAttribution( + label: 'Delta kind', + text: delta.runtimeType.toString(), + ), + SizedBox(height: 5), + _createTextWithSingleAttribution( + label: 'IME Text', + text: delta.apply(TextEditingValue(text: delta.oldText)).text, + selection: delta.selection, + ), + _createTextWithSingleAttribution( + label: 'Selection', + text: delta.selection.toString(), + ), + SizedBox(height: 5), + _createTextWithSingleAttribution( + label: 'Composing region', + text: delta.composing.toString(), + ), + ], + ); +} + +Widget? _buildKeyEvent(BuildContext context, TextInputDebugEvent event) { + final keyEvent = event.data; + if (keyEvent is! RawKeyDownEvent) { + return null; + } + + return ListTile( + leading: Icon( + Icons.arrow_downward, + color: Colors.blue, + ), + title: Text( + event.method, + style: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _createTextWithSingleAttribution( + label: 'Key', + text: keyEvent.data.physicalKey.debugName ?? keyEvent.data.keyLabel, + ), + ], + ), + ); +} + +Widget _buildGenericEvent(BuildContext context, TextInputDebugEvent event) { + return ListTile( + leading: event.direction == TextInputMessageDirection.fromIme // + ? Icon( + Icons.arrow_downward, + color: Colors.blue, + ) + : Icon( + Icons.arrow_upward, + color: Colors.green, + ), + title: Text( + event.method, + style: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text(event.data.toString()), + ); +} + +TextStyle _deltaTextStyler(Set attributions) { + TextStyle newStyle = TextStyle( + color: Colors.black, + fontSize: 14, + ); + + if (attributions.contains(boldAttribution)) { + newStyle = newStyle.copyWith(fontWeight: FontWeight.bold); + } + + if (attributions.contains(_insertionOffsetAttribution)) { + newStyle = newStyle.copyWith( + decoration: TextDecoration.underline, + fontWeight: FontWeight.bold, + ); + } + + if (attributions.contains(_newTextAttribution)) { + 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; +} + +Widget _createTextWithSingleAttribution({ + String label = '', + required String text, + Attribution? attribution, + TextRange? range, + TextSelection? selection, +}) { + 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, + ); +} + +const _insertionOffsetAttribution = const NamedAttribution('insertionOffset'); +const _newTextAttribution = const NamedAttribution('newTextOffset'); +const _textRemovedAttribution = const NamedAttribution('textRemovedAttribution'); From 64ea2b253c0fe896d410525a912096e0d9d1be21 Mon Sep 17 00:00:00 2001 From: Angelo Silvestre Date: Wed, 9 Aug 2023 23:45:10 -0300 Subject: [PATCH 3/6] minor fix --- .../demos/example_editor/text_input_visualizer.dart | 13 ++++--------- .../document_physical_keyboard.dart | 1 - 2 files changed, 4 insertions(+), 10 deletions(-) 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 index 4c0475ee86..5495c1927d 100644 --- a/super_editor/example/lib/demos/example_editor/text_input_visualizer.dart +++ b/super_editor/example/lib/demos/example_editor/text_input_visualizer.dart @@ -399,15 +399,10 @@ Widget? _buildKeyEvent(BuildContext context, TextInputDebugEvent event) { Widget _buildGenericEvent(BuildContext context, TextInputDebugEvent event) { return ListTile( - leading: event.direction == TextInputMessageDirection.fromIme // - ? Icon( - Icons.arrow_downward, - color: Colors.blue, - ) - : Icon( - Icons.arrow_upward, - color: Colors.green, - ), + leading: Icon( + Icons.info, + color: Colors.blue, + ), title: Text( event.method, style: TextStyle(fontWeight: FontWeight.bold), 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 a5a5584f54..b0d3a34a53 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 @@ -75,7 +75,6 @@ class _SuperEditorHardwareKeyHandlerState extends State Date: Sun, 13 Aug 2023 18:35:03 -0300 Subject: [PATCH 4/6] Add doc and comments --- .../demos/example_editor/example_editor.dart | 2 +- .../example_editor/text_input_visualizer.dart | 150 +++++++++--------- .../document_physical_keyboard.dart | 7 + 3 files changed, 86 insertions(+), 73 deletions(-) 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 fb3c54ba6d..4e1fa81839 100644 --- a/super_editor/example/lib/demos/example_editor/example_editor.dart +++ b/super_editor/example/lib/demos/example_editor/example_editor.dart @@ -347,7 +347,7 @@ class _ExampleEditorState extends State { SizedBox( width: 400, child: SuperEditorImeDebugger( - watcher: _textInputDebugger, + debugger: _textInputDebugger, ), ), ], 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 index 5495c1927d..e7e4daa15a 100644 --- a/super_editor/example/lib/demos/example_editor/text_input_visualizer.dart +++ b/super_editor/example/lib/demos/example_editor/text_input_visualizer.dart @@ -3,20 +3,22 @@ import 'package:flutter/services.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.watcher, - this.componentBuilders = const [ - _buildSetEditingTextEvent, - _buildTextDeltaEvents, - _buildKeyEvent, - _buildGenericEvent, - ], + required this.debugger, + this.componentBuilders = defaultTextInputDebugComponentBuilders, }) : super(key: key); - final TextInputDebugger watcher; + /// 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 @@ -29,28 +31,28 @@ class _SuperEditorImeDebuggerState extends State { @override void initState() { super.initState(); - widget.watcher.addListener(_scrollOnNextFrame); - _scrollOnNextFrame(); + widget.debugger.addListener(_scrollToTheEndOnNextFrame); + _scrollToTheEndOnNextFrame(); } @override void didUpdateWidget(covariant SuperEditorImeDebugger oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.watcher != widget.watcher) { - oldWidget.watcher.removeListener(_scrollOnNextFrame); - widget.watcher.addListener(_scrollOnNextFrame); - _scrollOnNextFrame(); + if (oldWidget.debugger != widget.debugger) { + oldWidget.debugger.removeListener(_scrollToTheEndOnNextFrame); + widget.debugger.addListener(_scrollToTheEndOnNextFrame); + _scrollToTheEndOnNextFrame(); } } @override void dispose() { - widget.watcher.removeListener(_scrollOnNextFrame); + widget.debugger.removeListener(_scrollToTheEndOnNextFrame); _scrollController.dispose(); super.dispose(); } - void _scrollOnNextFrame() { + void _scrollToTheEndOnNextFrame() { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { if (!mounted) { return; @@ -68,12 +70,12 @@ class _SuperEditorImeDebuggerState extends State { children: [ Expanded( child: ListenableBuilder( - listenable: widget.watcher, + listenable: widget.debugger, builder: (context, child) { return ListView.separated( controller: _scrollController, - itemCount: widget.watcher.events.length, - itemBuilder: (context, index) => _buildLogItem(context, widget.watcher.events[index]), + itemCount: widget.debugger.events.length, + itemBuilder: (context, index) => _buildEvent(context, widget.debugger.events[index]), separatorBuilder: (context, index) => SizedBox(height: 20), ); }, @@ -82,7 +84,7 @@ class _SuperEditorImeDebuggerState extends State { SizedBox( width: double.infinity, child: ElevatedButton( - onPressed: widget.watcher.clear, + onPressed: widget.debugger.clear, child: Text('Clear'), ), ), @@ -91,7 +93,10 @@ class _SuperEditorImeDebuggerState extends State { ); } - Widget _buildLogItem(BuildContext context, TextInputDebugEvent event) { + /// 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); @@ -110,14 +115,16 @@ class _SuperEditorImeDebuggerState extends State { typedef TextInputDebugComponentBuilder = Widget? Function(BuildContext context, TextInputDebugEvent event); +/// Component builders that generate a visual representation for the events. const defaultTextInputDebugComponentBuilders = [ - _buildSetEditingTextEvent, + _buildSetEditingStateEvent, _buildTextDeltaEvents, _buildKeyEvent, _buildGenericEvent, ]; -Widget? _buildSetEditingTextEvent(BuildContext context, TextInputDebugEvent event) { +/// Build the widget for a `setEditingState` call. +Widget? _buildSetEditingStateEvent(BuildContext context, TextInputDebugEvent event) { final textEditingValue = event.data; if (textEditingValue is! TextEditingValue) { return null; @@ -136,19 +143,18 @@ Widget? _buildSetEditingTextEvent(BuildContext context, TextInputDebugEvent even mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'Text', text: textEditingValue.text, - attribution: _insertionOffsetAttribution, selection: textEditingValue.selection, ), SizedBox(height: 5), - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'Selection', text: textEditingValue.selection.toString(), ), SizedBox(height: 5), - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'Composing region', text: textEditingValue.composing.toString(), ), @@ -157,6 +163,7 @@ Widget? _buildSetEditingTextEvent(BuildContext context, TextInputDebugEvent even ); } +/// Build the widget for an `updateEditingStateWithDeltas` call. Widget? _buildTextDeltaEvents(BuildContext context, TextInputDebugEvent event) { final params = event.data; if (params is! List) { @@ -203,52 +210,48 @@ Widget? _buildTextDeltaEvents(BuildContext context, TextInputDebugEvent event) { ); } +/// Build the widget for a `TextEditingDeltaInsertion`. Widget _buildInsertionDeltaEvent(BuildContext context, TextEditingDeltaInsertion delta) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'Delta kind', text: delta.runtimeType.toString(), ), SizedBox(height: 5), - _createTextWithSingleAttribution( - label: 'OldText', + _createLabelAndValue( + label: 'Old Text', text: delta.oldText, - attribution: _insertionOffsetAttribution, - range: TextRange( - start: delta.insertionOffset, - end: delta.insertionOffset, - ), ), SizedBox(height: 5), - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'Inserted Text', text: delta.textInserted, ), SizedBox(height: 5), - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'Insertion Offset', text: delta.insertionOffset.toString(), ), SizedBox(height: 5), - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'IME new text', text: delta.apply(TextEditingValue(text: delta.oldText)).text, - attribution: _newTextAttribution, + attribution: _insertedTextAttribution, range: TextRange( start: delta.insertionOffset, end: delta.insertionOffset + delta.textInserted.length, ), selection: delta.selection, ), - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'Selection', text: delta.selection.toString(), ), SizedBox(height: 5), - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'Composing region', text: delta.composing.toString(), ), @@ -256,44 +259,45 @@ Widget _buildInsertionDeltaEvent(BuildContext context, TextEditingDeltaInsertion ); } +/// Build the widget for a `TextEditingDeltaReplacement`. Widget _buildReplacementDeltaEvent(BuildContext context, TextEditingDeltaReplacement delta) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'Delta kind', text: delta.runtimeType.toString(), ), SizedBox(height: 5), - _createTextWithSingleAttribution( - label: 'OldText', + _createLabelAndValue( + label: 'Old Text', text: delta.oldText, attribution: _textRemovedAttribution, range: delta.replacedRange, ), SizedBox(height: 5), - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'Range', text: delta.replacedRange.toString(), ), SizedBox(height: 5), - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'IME new text', text: delta.apply(TextEditingValue(text: delta.oldText)).text, - attribution: _newTextAttribution, + attribution: _insertedTextAttribution, range: TextRange( start: delta.replacedRange.start, end: delta.replacedRange.start + delta.replacementText.length, ), selection: delta.selection, ), - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'Selection', text: delta.selection.toString(), ), SizedBox(height: 5), - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'Composing region', text: delta.composing.toString(), ), @@ -301,39 +305,40 @@ Widget _buildReplacementDeltaEvent(BuildContext context, TextEditingDeltaReplace ); } +/// Build the widget for a `TextEditingDeltaDeletion`. Widget _buildDeletionDeltaEvent(BuildContext context, TextEditingDeltaDeletion delta) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'Delta kind', text: delta.runtimeType.toString(), ), SizedBox(height: 5), - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'OldText', text: delta.oldText, attribution: _textRemovedAttribution, range: delta.deletedRange, ), SizedBox(height: 5), - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'Range', text: delta.deletedRange.toString(), ), SizedBox(height: 5), - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'IME new text', text: delta.apply(TextEditingValue(text: delta.oldText)).text, selection: delta.selection, ), - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'Selection', text: delta.selection.toString(), ), SizedBox(height: 5), - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'Composing region', text: delta.composing.toString(), ), @@ -341,27 +346,28 @@ Widget _buildDeletionDeltaEvent(BuildContext context, TextEditingDeltaDeletion d ); } +/// Build the widget for a `TextEditingDeltaNonTextUpdate`. Widget _buildNonTextDeltaEvent(BuildContext context, TextEditingDeltaNonTextUpdate delta) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'Delta kind', text: delta.runtimeType.toString(), ), SizedBox(height: 5), - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'IME Text', text: delta.apply(TextEditingValue(text: delta.oldText)).text, selection: delta.selection, ), - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'Selection', text: delta.selection.toString(), ), SizedBox(height: 5), - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'Composing region', text: delta.composing.toString(), ), @@ -369,6 +375,7 @@ Widget _buildNonTextDeltaEvent(BuildContext context, TextEditingDeltaNonTextUpda ); } +/// Build the widget for an `onKey` call. Widget? _buildKeyEvent(BuildContext context, TextInputDebugEvent event) { final keyEvent = event.data; if (keyEvent is! RawKeyDownEvent) { @@ -388,7 +395,7 @@ Widget? _buildKeyEvent(BuildContext context, TextInputDebugEvent event) { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - _createTextWithSingleAttribution( + _createLabelAndValue( label: 'Key', text: keyEvent.data.physicalKey.debugName ?? keyEvent.data.keyLabel, ), @@ -397,6 +404,7 @@ Widget? _buildKeyEvent(BuildContext context, TextInputDebugEvent event) { ); } +/// Build the widget for any given [event]. Widget _buildGenericEvent(BuildContext context, TextInputDebugEvent event) { return ListTile( leading: Icon( @@ -411,6 +419,7 @@ Widget _buildGenericEvent(BuildContext context, TextInputDebugEvent event) { ); } +/// Applies styles to highlight text insertions and deletions. TextStyle _deltaTextStyler(Set attributions) { TextStyle newStyle = TextStyle( color: Colors.black, @@ -421,14 +430,7 @@ TextStyle _deltaTextStyler(Set attributions) { newStyle = newStyle.copyWith(fontWeight: FontWeight.bold); } - if (attributions.contains(_insertionOffsetAttribution)) { - newStyle = newStyle.copyWith( - decoration: TextDecoration.underline, - fontWeight: FontWeight.bold, - ); - } - - if (attributions.contains(_newTextAttribution)) { + if (attributions.contains(_insertedTextAttribution)) { newStyle = newStyle.copyWith( decoration: TextDecoration.underline, color: Colors.blue, @@ -447,13 +449,15 @@ TextStyle _deltaTextStyler(Set attributions) { return newStyle; } -Widget _createTextWithSingleAttribution({ - String label = '', +/// 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, @@ -490,6 +494,8 @@ Widget _createTextWithSingleAttribution({ ); } -const _insertionOffsetAttribution = const NamedAttribution('insertionOffset'); -const _newTextAttribution = const NamedAttribution('newTextOffset'); +/// [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'); 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 b0d3a34a53..fdfada9342 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 @@ -91,6 +91,13 @@ class _SuperEditorHardwareKeyHandlerState extends State Date: Sun, 13 Aug 2023 18:42:49 -0300 Subject: [PATCH 5/6] Minor docs --- .../lib/demos/example_editor/text_input_visualizer.dart | 9 ++------- .../document_physical_keyboard.dart | 1 + .../document_ime/document_ime_communication.dart | 1 + .../document_ime/supereditor_ime_interactor.dart | 1 + super_editor/lib/src/default_editor/super_editor.dart | 2 +- 5 files changed, 6 insertions(+), 8 deletions(-) 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 index e7e4daa15a..2066ed6cd6 100644 --- a/super_editor/example/lib/demos/example_editor/text_input_visualizer.dart +++ b/super_editor/example/lib/demos/example_editor/text_input_visualizer.dart @@ -1,5 +1,6 @@ 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'; @@ -53,13 +54,7 @@ class _SuperEditorImeDebuggerState extends State { } void _scrollToTheEndOnNextFrame() { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - if (!mounted) { - return; - } - - _scrollController.jumpTo(_scrollController.position.maxScrollExtent); - }); + onNextFrame((_) => _scrollController.jumpTo(_scrollController.position.maxScrollExtent)); } @override 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 fdfada9342..d92bda0319 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 @@ -45,6 +45,7 @@ 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 diff --git a/super_editor/lib/src/default_editor/document_ime/document_ime_communication.dart b/super_editor/lib/src/default_editor/document_ime/document_ime_communication.dart index 511ae11189..0839b6221f 100644 --- a/super_editor/lib/src/default_editor/document_ime/document_ime_communication.dart +++ b/super_editor/lib/src/default_editor/document_ime/document_ime_communication.dart @@ -64,6 +64,7 @@ class DocumentImeInputClient extends TextInputConnectionDecorator with TextInput // TODO: get floating cursor out of here. Use a multi-client IME decorator to split responsibilities late FloatingCursorController? _floatingCursorController; + /// Event collector for debugging purposes. final TextInputDebugger? debugger; void _onContentChange() { 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 393f51b9c9..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 @@ -117,6 +117,7 @@ class SuperEditorImeInteractor extends StatefulWidget { /// a property on this IME interactor. final FloatingCursorController? floatingCursorController; + /// Event collector for debugging purposes. final TextInputDebugger? textInputDebugger; final Widget child; diff --git a/super_editor/lib/src/default_editor/super_editor.dart b/super_editor/lib/src/default_editor/super_editor.dart index 6ff6787528..ca8f4ef76a 100644 --- a/super_editor/lib/src/default_editor/super_editor.dart +++ b/super_editor/lib/src/default_editor/super_editor.dart @@ -273,6 +273,7 @@ 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 @@ -551,7 +552,6 @@ class SuperEditorState extends State { ...plugin.keyboardActions, ...widget.keyboardActions, ], - keyboardActions: widget.keyboardActions, textInputDebugger: widget.textInputDebugger, child: child, ); From e41555277d46f2d12b03d1fbe9ba7a85dbc16a0b Mon Sep 17 00:00:00 2001 From: Angelo Silvestre Date: Sun, 27 Aug 2023 16:23:35 -0300 Subject: [PATCH 6/6] PR updates --- .../example_editor/text_input_visualizer.dart | 266 +++++++++++++----- .../document_physical_keyboard.dart | 3 + 2 files changed, 195 insertions(+), 74 deletions(-) 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 index 2066ed6cd6..691827498f 100644 --- a/super_editor/example/lib/demos/example_editor/text_input_visualizer.dart +++ b/super_editor/example/lib/demos/example_editor/text_input_visualizer.dart @@ -29,6 +29,8 @@ class SuperEditorImeDebugger extends StatefulWidget { class _SuperEditorImeDebuggerState extends State { final ScrollController _scrollController = ScrollController(); + bool _isExpanded = false; + @override void initState() { super.initState(); @@ -59,31 +61,74 @@ class _SuperEditorImeDebuggerState extends State { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - Expanded( - child: ListenableBuilder( - listenable: widget.debugger, - builder: (context, child) { - return ListView.separated( + 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), - ); - }, - ), - ), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: widget.debugger.clear, - child: Text('Clear'), - ), + ), + ), + ], + ); + }, + ), + ); + } + + 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, + ), + ), + ), + ], ), - ], + ), ), ); } @@ -94,7 +139,7 @@ class _SuperEditorImeDebuggerState extends State { Widget _buildEvent(BuildContext context, TextInputDebugEvent event) { Widget? content; for (final builder in widget.componentBuilders) { - content = builder(context, event); + content = builder(context, event, _isExpanded); if (content != null) { break; } @@ -108,7 +153,8 @@ class _SuperEditorImeDebuggerState extends State { } } -typedef TextInputDebugComponentBuilder = Widget? Function(BuildContext context, TextInputDebugEvent event); +typedef TextInputDebugComponentBuilder = Widget? Function( + BuildContext context, TextInputDebugEvent event, bool detailed); /// Component builders that generate a visual representation for the events. const defaultTextInputDebugComponentBuilders = [ @@ -119,47 +165,50 @@ const defaultTextInputDebugComponentBuilders = [ ]; /// Build the widget for a `setEditingState` call. -Widget? _buildSetEditingStateEvent(BuildContext context, TextInputDebugEvent event) { +/// +/// 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 ListTile( + return _ExpandableTile( + expanded: detailed, leading: Icon( Icons.arrow_upward, color: Colors.green, ), title: Text( event.method, - style: TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - 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(), - ), - ], + 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) { +Widget? _buildTextDeltaEvents(BuildContext context, TextInputDebugEvent event, bool detailed) { final params = event.data; if (params is! List) { return null; @@ -186,22 +235,20 @@ Widget? _buildTextDeltaEvents(BuildContext context, TextInputDebugEvent event) { return SizedBox(); }, ).toList(); - return ListTile( + return _ExpandableTile( + expanded: detailed, leading: Icon( Icons.arrow_downward, color: Colors.blue, ), title: Text( event.method, - style: TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...deltaComponents, - ], + style: TextStyle( + fontWeight: FontWeight.bold, + color: _fontColor, + ), ), + children: deltaComponents, ); } @@ -371,53 +418,65 @@ Widget _buildNonTextDeltaEvent(BuildContext context, TextEditingDeltaNonTextUpda } /// Build the widget for an `onKey` call. -Widget? _buildKeyEvent(BuildContext context, TextInputDebugEvent event) { +Widget? _buildKeyEvent(BuildContext context, TextInputDebugEvent event, bool detailed) { final keyEvent = event.data; if (keyEvent is! RawKeyDownEvent) { return null; } - return ListTile( + return _ExpandableTile( + expanded: detailed, leading: Icon( Icons.arrow_downward, color: Colors.blue, ), title: Text( event.method, - style: TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _createLabelAndValue( - label: 'Key', - text: keyEvent.data.physicalKey.debugName ?? keyEvent.data.keyLabel, - ), - ], + 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) { - return ListTile( +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), + style: TextStyle( + fontWeight: FontWeight.bold, + color: _fontColor, + ), ), - subtitle: Text(event.data.toString()), + children: [ + Text(event.data.toString()), + ], ); } /// Applies styles to highlight text insertions and deletions. TextStyle _deltaTextStyler(Set attributions) { TextStyle newStyle = TextStyle( - color: Colors.black, + color: Color(0xFFCCCCCC), fontSize: 14, ); @@ -494,3 +553,62 @@ 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 d92bda0319..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 @@ -74,6 +74,9 @@ class _SuperEditorHardwareKeyHandlerState extends State