From 26f8ea373dae0f057b3d423582fa124a424839ec Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Fri, 8 Jul 2022 18:05:06 -0700 Subject: [PATCH 1/3] [SuperTextField] - generalized the ability to override gestures and moved right-click behavior to the new system (Resolves #278) --- .../demo_textfield_with_context_menu.dart | 171 +++++++++++ super_editor/example/lib/main.dart | 8 + .../desktop/desktop_textfield.dart | 290 +++++++++++++++--- .../desktop_textfield_gesture_extensions.dart | 103 +++++++ .../super_textfield/super_textfield.dart | 15 +- ...top_textfield_gesture_extensions_test.dart | 93 ++++++ 6 files changed, 643 insertions(+), 37 deletions(-) create mode 100644 super_editor/example/lib/demos/supertextfield/demo_textfield_with_context_menu.dart create mode 100644 super_editor/lib/src/infrastructure/super_textfield/desktop/desktop_textfield_gesture_extensions.dart create mode 100644 super_editor/test/super_textfield/super_desktop_textfield_gesture_extensions_test.dart diff --git a/super_editor/example/lib/demos/supertextfield/demo_textfield_with_context_menu.dart b/super_editor/example/lib/demos/supertextfield/demo_textfield_with_context_menu.dart new file mode 100644 index 000000000..7fea807c4 --- /dev/null +++ b/super_editor/example/lib/demos/supertextfield/demo_textfield_with_context_menu.dart @@ -0,0 +1,171 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:super_editor/super_editor.dart'; + +/// Demo of [SuperTextField] with a context menu that opens in response +/// to various gestures on desktop. +class TextFieldWithContextMenuDemo extends StatefulWidget { + @override + _TextFieldWithContextMenuDemoState createState() => _TextFieldWithContextMenuDemoState(); +} + +class _TextFieldWithContextMenuDemoState extends State { + final _superTextFieldKey = GlobalKey(); + + late final _contextMenuOverlay; + + KeyMessageHandler? _existingKeyMessageHandler; + bool _isCtrlPressed = false; + + @override + void initState() { + super.initState(); + _contextMenuOverlay = _ContextMenuOverlay(); + + _existingKeyMessageHandler = ServicesBinding.instance.keyEventManager.keyMessageHandler; + ServicesBinding.instance.keyEventManager.keyMessageHandler = _handleGlobalKeyMessage; + } + + @override + void dispose() { + ServicesBinding.instance.keyEventManager.keyMessageHandler = _existingKeyMessageHandler; + + _contextMenuOverlay.hide(); + super.dispose(); + } + + bool _handleGlobalKeyMessage(KeyMessage keyMessage) { + for (final event in keyMessage.events) { + if (event.logicalKey.synonyms.contains(LogicalKeyboardKey.control)) { + if (event is KeyDownEvent) { + setState(() { + _isCtrlPressed = true; + }); + } else if (event is KeyUpEvent) { + setState(() { + _isCtrlPressed = false; + }); + } + } + } + + return _existingKeyMessageHandler?.call(keyMessage) ?? false; + } + + GestureOverrideResult _onTapDown(details) { + if (_isCtrlPressed && defaultTargetPlatform == TargetPlatform.macOS) { + _contextMenuOverlay.show(context, details.globalOffset); + return GestureOverrideResult.handled; + } + + return GestureOverrideResult.notHandled; + } + + GestureOverrideResult _onRightTapDown(details) { + if (defaultTargetPlatform == TargetPlatform.macOS) { + _contextMenuOverlay.show(context, details.globalOffset); + return GestureOverrideResult.handled; + } + + return GestureOverrideResult.notHandled; + } + + GestureOverrideResult _onRightTapUp(details) { + if (defaultTargetPlatform == TargetPlatform.windows) { + _contextMenuOverlay.show(context, details.globalOffset); + return GestureOverrideResult.handled; + } + + return GestureOverrideResult.notHandled; + } + + @override + Widget build(BuildContext context) { + // TODO: conditionally add gesture callbacks when conditions call for them. + // For example, for ALT + LEFT CLICK, start with no callback. When the keyboard + // listener reports ALT is pressed, add the callback. Do this in two ways: first, + // immediately call replaceGestureRecognizers on the RawGestureDetectorState, and + // then in the next build, include the callback in the widget tree. + + return Center( + child: SuperTextFieldDesktopGestureExtensions( + superTextFieldKey: _superTextFieldKey, + onTapDown: _onTapDown, + onRightTapDown: _onRightTapDown, + onRightTapUp: _onRightTapUp, + child: _buildTextField(), + ), + ); + } + + Widget _buildTextField() { + return ConstrainedBox( + constraints: const BoxConstraints(minWidth: 300), + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all(color: Colors.black), + ), + child: SuperTextField( + key: _superTextFieldKey, + hintBuilder: (context) { + return Text( + "enter text here...", + style: const TextStyle(color: Colors.grey), + ); + }, + textStyleBuilder: (_) { + return const TextStyle( + color: Colors.black, + fontSize: 18, + ); + }, + ), + ), + ); + } +} + +class _ContextMenuOverlay { + OverlayEntry? _entry; + late Offset _globalOffset; + + void show(BuildContext context, Offset globalOffset) { + _globalOffset = globalOffset; + + if (_entry == null) { + _entry = OverlayEntry(builder: (innerContext) { + return Stack( + children: [ + Positioned.fill( + child: GestureDetector( + onTap: () => hide(), + child: Container( + color: Colors.black.withOpacity(0.1), + ), + ), + ), + Positioned( + left: _globalOffset.dx, + top: _globalOffset.dy, + child: Container( + width: 200, + height: 125, + color: Colors.grey.shade800, + ), + ), + ], + ); + }); + + Overlay.of(context)!.insert(_entry!); + } else { + _entry!.markNeedsBuild(); + } + } + + void hide() { + _entry?.remove(); + _entry = null; + } +} diff --git a/super_editor/example/lib/main.dart b/super_editor/example/lib/main.dart index 908690576..68047ceb7 100644 --- a/super_editor/example/lib/main.dart +++ b/super_editor/example/lib/main.dart @@ -30,6 +30,7 @@ import 'demos/demo_switch_document_content.dart'; import 'demos/scrolling/demo_task_and_chat_with_renderobject.dart'; import 'demos/super_document/demo_read_only_scrolling_document.dart'; import 'demos/supertextfield/android/demo_superandroidtextfield.dart'; +import 'demos/supertextfield/demo_textfield_with_context_menu.dart'; /// Demo of a basic text editor, as well as various widgets that /// are available in this package. @@ -326,6 +327,13 @@ final _menu = <_MenuGroup>[ return TextFieldDemo(); }, ), + _MenuItem( + icon: Icons.text_fields, + title: 'SuperTextField with context menu', + pageBuilder: (context) { + return TextFieldWithContextMenuDemo(); + }, + ), _MenuItem( icon: Icons.text_fields, title: 'Super iOS Textfield', diff --git a/super_editor/lib/src/infrastructure/super_textfield/desktop/desktop_textfield.dart b/super_editor/lib/src/infrastructure/super_textfield/desktop/desktop_textfield.dart index 0cd89bdfb..e4b21e968 100644 --- a/super_editor/lib/src/infrastructure/super_textfield/desktop/desktop_textfield.dart +++ b/super_editor/lib/src/infrastructure/super_textfield/desktop/desktop_textfield.dart @@ -11,6 +11,7 @@ import 'package:super_editor/src/infrastructure/_listenable_builder.dart'; import 'package:super_editor/src/infrastructure/_logging.dart'; import 'package:super_editor/src/infrastructure/attributed_text_styles.dart'; import 'package:super_editor/src/infrastructure/platform_detector.dart'; +import 'package:super_editor/src/infrastructure/super_textfield/desktop/desktop_textfield_gesture_extensions.dart'; import 'package:super_editor/src/infrastructure/super_textfield/infrastructure/attributed_text_editing_controller.dart'; import 'package:super_editor/src/infrastructure/super_textfield/infrastructure/hint_text.dart'; import 'package:super_text_layout/super_text_layout.dart'; @@ -55,6 +56,7 @@ class SuperDesktopTextField extends StatefulWidget { this.minLines, this.maxLines = 1, this.decorationBuilder, + this.gestureOverrides = SuperTextFieldGestureOverrides.none, this.onRightClick, this.keyboardHandlers = defaultTextFieldKeyboardHandlers, }) : super(key: key); @@ -91,6 +93,14 @@ class SuperDesktopTextField extends StatefulWidget { final DecorationBuilder? decorationBuilder; + /// Overrides default text field gestures, so that clients can handle + /// some of them instead. + /// + /// For example, a client may want to open a context menu on right-click, + /// or on ALT + left-click. + final SuperTextFieldGestureOverrides gestureOverrides; + + @Deprecated("Use the SuperTextField gesture override system to take control of desired gestures") final RightClickListener? onRightClick; /// Priority list of handlers that process all physical keyboard @@ -279,6 +289,7 @@ class SuperDesktopTextFieldState extends State implements textKey: _textKey, textScrollKey: _textScrollKey, isMultiline: isMultiline, + gestureOverrides: widget.gestureOverrides, onRightClick: widget.onRightClick, child: MultiListenableBuilder( listenables: { @@ -359,6 +370,7 @@ class SuperTextFieldGestureInteractor extends StatefulWidget { required this.textKey, required this.textScrollKey, required this.isMultiline, + this.gestureOverrides = SuperTextFieldGestureOverrides.none, this.onRightClick, required this.child, }) : super(key: key); @@ -381,7 +393,15 @@ class SuperTextFieldGestureInteractor extends StatefulWidget { /// Whether or not this text field supports multiple lines of text. final bool isMultiline; + /// Overrides default text field gestures, so that clients can handle + /// some of them instead. + /// + /// For example, a client may want to open a context menu on right-click, + /// or on ALT + left-click. + final SuperTextFieldGestureOverrides gestureOverrides; + /// Callback invoked when the user right clicks on this text field. + @Deprecated("Use the gesture override system to take control of desired gestures") final RightClickListener? onRightClick; /// The rest of the subtree for this text field. @@ -408,6 +428,17 @@ class _SuperTextFieldGestureInteractorState extends State{ - TapSequenceGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => TapSequenceGestureRecognizer(), - (TapSequenceGestureRecognizer recognizer) { - recognizer - ..onTapDown = _onTapDown - ..onDoubleTapDown = _onDoubleTapDown - ..onDoubleTap = _onDoubleTap - ..onTripleTapDown = _onTripleTapDown - ..onTripleTap = _onTripleTap; - }, - ), - PanGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => PanGestureRecognizer(), - (PanGestureRecognizer recognizer) { - recognizer - ..onStart = _onPanStart - ..onUpdate = _onPanUpdate - ..onEnd = _onPanEnd - ..onCancel = _onPanCancel; - }, - ), - }, - child: MouseRegion( - cursor: SystemMouseCursors.text, - child: widget.child, + child: RawGestureDetector( + behavior: HitTestBehavior.translucent, + gestures: { + TapSequenceGestureRecognizer: GestureRecognizerFactoryWithHandlers( + () => TapSequenceGestureRecognizer(), + (TapSequenceGestureRecognizer recognizer) { + recognizer + ..onTapDown = _onTapDown + ..onTapUp = _onTapUp + ..onDoubleTapDown = _onDoubleTapDown + ..onDoubleTapUp = _onDoubleTapUp + ..onTripleTapDown = _onTripleTapDown + ..onTripleTapUp = _onTripleTapUp; + }, + ), + PanGestureRecognizer: GestureRecognizerFactoryWithHandlers( + () => PanGestureRecognizer(), + (PanGestureRecognizer recognizer) { + recognizer + ..onStart = _onPanStart + ..onUpdate = _onPanUpdate + ..onEnd = _onPanEnd + ..onCancel = _onPanCancel; + }, ), + }, + child: MouseRegion( + cursor: SystemMouseCursors.text, + child: widget.child, ), ), ); } } +/// Overrides desired [SuperDesktopTextField] default gesture behaviors and +/// replaces them with the given implementation. +/// +/// Implement this interface, or extent this class to respond to gestures. +class SuperTextFieldGestureOverrides { + static const none = _NoSuperTextFieldGestureOverrides(); + + GestureOverrideResult onTapDown(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; + GestureOverrideResult onTapUp(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; + + GestureOverrideResult onRightTapDown(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; + GestureOverrideResult onRightTapUp(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; + + GestureOverrideResult onDoubleTapDown(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; + GestureOverrideResult onDoubleTapUp(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; + + GestureOverrideResult onTripleTapDown(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; + GestureOverrideResult onTripleTapUp(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; +} + +class CallbackSuperTextFieldGestureOverrides implements SuperTextFieldGestureOverrides { + CallbackSuperTextFieldGestureOverrides({ + GestureOverrideHandler? onTapDown, + GestureOverrideHandler? onTapUp, + GestureOverrideHandler? onDoubleTapDown, + GestureOverrideHandler? onDoubleTapUp, + GestureOverrideHandler? onTripleTapDown, + GestureOverrideHandler? onTripleTapUp, + GestureOverrideHandler? onRightTapDown, + GestureOverrideHandler? onRightTapUp, + }) : _onTapDown = onTapDown, + _onTapUp = onTapUp, + _onDoubleTapDown = onDoubleTapDown, + _onDoubleTapUp = onDoubleTapUp, + _onTripleTapDown = onTripleTapDown, + _onTripleTapUp = onTripleTapUp, + _onRightTapDown = onRightTapDown, + _onRightTapUp = onRightTapUp; + + @override + GestureOverrideResult onTapDown(SuperTextFieldTapDetails details) => + _onTapDown?.call(details) ?? GestureOverrideResult.notHandled; + final GestureOverrideHandler? _onTapDown; + + @override + GestureOverrideResult onTapUp(SuperTextFieldTapDetails details) => + _onTapUp?.call(details) ?? GestureOverrideResult.notHandled; + final GestureOverrideHandler? _onTapUp; + + @override + GestureOverrideResult onRightTapDown(SuperTextFieldTapDetails details) => + _onRightTapDown?.call(details) ?? GestureOverrideResult.notHandled; + final GestureOverrideHandler? _onRightTapDown; + + @override + GestureOverrideResult onRightTapUp(SuperTextFieldTapDetails details) => + _onRightTapUp?.call(details) ?? GestureOverrideResult.notHandled; + final GestureOverrideHandler? _onRightTapUp; + + @override + GestureOverrideResult onDoubleTapDown(SuperTextFieldTapDetails details) => + _onDoubleTapDown?.call(details) ?? GestureOverrideResult.notHandled; + final GestureOverrideHandler? _onDoubleTapDown; + + @override + GestureOverrideResult onDoubleTapUp(SuperTextFieldTapDetails details) => + _onDoubleTapUp?.call(details) ?? GestureOverrideResult.notHandled; + final GestureOverrideHandler? _onDoubleTapUp; + + @override + GestureOverrideResult onTripleTapDown(SuperTextFieldTapDetails details) => + _onTripleTapDown?.call(details) ?? GestureOverrideResult.notHandled; + final GestureOverrideHandler? _onTripleTapDown; + + @override + GestureOverrideResult onTripleTapUp(SuperTextFieldTapDetails details) => + _onTripleTapUp?.call(details) ?? GestureOverrideResult.notHandled; + final GestureOverrideHandler? _onTripleTapUp; +} + +/// A no-op implementation of [SuperTextFieldGestureOverrides], which is available as +/// a public constant in [SuperTextFieldGestureOverrides.none]. +class _NoSuperTextFieldGestureOverrides implements SuperTextFieldGestureOverrides { + const _NoSuperTextFieldGestureOverrides(); + + @override + GestureOverrideResult onTapDown(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; + @override + GestureOverrideResult onTapUp(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; + + @override + GestureOverrideResult onRightTapDown(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; + @override + GestureOverrideResult onRightTapUp(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; + + @override + GestureOverrideResult onDoubleTapDown(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; + @override + GestureOverrideResult onDoubleTapUp(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; + + @override + GestureOverrideResult onTripleTapDown(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; + @override + GestureOverrideResult onTripleTapUp(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; +} + +typedef GestureOverrideHandler = GestureOverrideResult Function(SuperTextFieldTapDetails details); + +/// Details about where a tap took place in a [SuperTextField] so that a client +/// can respond with a gesture override. +class SuperTextFieldTapDetails { + const SuperTextFieldTapDetails({ + required this.globalOffset, + required this.textFieldRenderBox, + required this.nearestTextPosition, + }); + + final Offset globalOffset; + final RenderBox textFieldRenderBox; + final TextPosition nearestTextPosition; +} + +enum GestureOverrideResult { + /// The gesture should NOT be handled by SuperTextField. + handled, + + /// The gesture should be handled by SuperTextField. + notHandled, +} + /// Handles all keyboard interactions for text entry in a text field. /// /// [SuperTextFieldKeyboardInteractor] is intended to operate as a piece within diff --git a/super_editor/lib/src/infrastructure/super_textfield/desktop/desktop_textfield_gesture_extensions.dart b/super_editor/lib/src/infrastructure/super_textfield/desktop/desktop_textfield_gesture_extensions.dart new file mode 100644 index 000000000..1d964c5f2 --- /dev/null +++ b/super_editor/lib/src/infrastructure/super_textfield/desktop/desktop_textfield_gesture_extensions.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:super_text_layout/super_text_layout.dart'; + +import 'desktop_textfield.dart'; + +/// Widget that wraps a [SuperTextField] and handles desired gestures on +/// desktop that go beyond the standard interactions of a [SuperTextField]. +class SuperTextFieldDesktopGestureExtensions extends StatefulWidget { + static SuperTextFieldDesktopGestureExtensionsState? of(BuildContext context) { + return context.findAncestorStateOfType(); + } + + const SuperTextFieldDesktopGestureExtensions({ + Key? key, + required this.superTextFieldKey, + this.onTapDown, + this.onTapUp, + this.onRightTapDown, + this.onRightTapUp, + required this.child, + }) : super(key: key); + + /// [GlobalKey] that's bound to the [SuperTextField] within this + /// [SuperTextFieldDesktopGestureExtensions]. + final GlobalKey superTextFieldKey; + + final GestureOverrideResult Function(SuperTextFieldTapDetails)? onTapDown; + + final GestureOverrideResult Function(SuperTextFieldTapDetails)? onTapUp; + + final GestureOverrideResult Function(SuperTextFieldTapDetails)? onRightTapDown; + + final GestureOverrideResult Function(SuperTextFieldTapDetails)? onRightTapUp; + + /// This widget's child, which should include a [SuperTextField] that's + /// bound to [superTextFieldKey]. + final Widget child; + + @override + State createState() => SuperTextFieldDesktopGestureExtensionsState(); +} + +class SuperTextFieldDesktopGestureExtensionsState extends State + with SuperTextFieldGestureOverrides { + @override + GestureOverrideResult onTapDown(SuperTextFieldTapDetails details) { + return widget.onTapDown?.call(details) ?? GestureOverrideResult.notHandled; + } + + @override + GestureOverrideResult onTapUp(SuperTextFieldTapDetails details) { + return widget.onTapUp?.call(details) ?? GestureOverrideResult.notHandled; + } + + void _onRightTapDown(TapDownDetails details) { + widget.onRightTapDown?.call(_tapDetailsFromTapDown(details)); + } + + void _onRightTapUp(TapUpDetails details) { + widget.onRightTapUp?.call(_tapDetailsFromTapUp(details)); + } + + // Converts a Flutter `TapDownDetails` to our `SuperTextFieldTapDetails`. + SuperTextFieldTapDetails _tapDetailsFromTapDown(TapDownDetails details) { + final textLayout = (widget.superTextFieldKey.currentState as ProseTextBlock).textLayout; + final textBox = (widget.superTextFieldKey.currentContext)!.findRenderObject() as RenderBox; + final nearestTextPosition = textLayout.getPositionNearestToOffset( + textBox.globalToLocal(details.globalPosition), + ); + + return SuperTextFieldTapDetails( + globalOffset: details.globalPosition, + textFieldRenderBox: textBox, + nearestTextPosition: nearestTextPosition, + ); + } + + // Converts a Flutter `TapUpDetails` to our `SuperTextFieldTapDetails`. + SuperTextFieldTapDetails _tapDetailsFromTapUp(TapUpDetails details) { + final textLayout = (widget.superTextFieldKey.currentState as ProseTextBlock).textLayout; + final textBox = (widget.superTextFieldKey.currentContext)!.findRenderObject() as RenderBox; + final nearestTextPosition = textLayout.getPositionNearestToOffset( + textBox.globalToLocal(details.globalPosition), + ); + + return SuperTextFieldTapDetails( + globalOffset: details.globalPosition, + textFieldRenderBox: textBox, + nearestTextPosition: nearestTextPosition, + ); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onSecondaryTapDown: widget.onRightTapDown != null ? _onRightTapDown : null, + onSecondaryTapUp: widget.onRightTapUp != null ? _onRightTapUp : null, + behavior: HitTestBehavior.translucent, + child: widget.child, + ); + } +} diff --git a/super_editor/lib/src/infrastructure/super_textfield/super_textfield.dart b/super_editor/lib/src/infrastructure/super_textfield/super_textfield.dart index b9fa7d101..e61977a19 100644 --- a/super_editor/lib/src/infrastructure/super_textfield/super_textfield.dart +++ b/super_editor/lib/src/infrastructure/super_textfield/super_textfield.dart @@ -14,6 +14,7 @@ import 'package:super_text_layout/super_text_layout.dart'; import 'styles.dart'; export 'android/android_textfield.dart'; +export 'desktop/desktop_textfield_gesture_extensions.dart'; export 'desktop/desktop_textfield.dart'; export 'infrastructure/attributed_text_editing_controller.dart'; export 'infrastructure/hint_text.dart'; @@ -63,6 +64,7 @@ class SuperTextField extends StatefulWidget { this.maxLines = 1, this.lineHeight, this.keyboardHandlers = defaultTextFieldKeyboardHandlers, + this.desktopGestureOverrides = SuperTextFieldGestureOverrides.none, }) : super(key: key); final FocusNode? focusNode; @@ -141,11 +143,19 @@ class SuperTextField extends StatefulWidget { /// Only used on desktop. final List keyboardHandlers; + /// Gesture overrides for desktop interaction. + /// + /// Gesture overrides are useful, for example, when implementing a + /// popover context menu on right-click, or on ALT + left-click. + /// Ordinarily, these gestures would be handled by [SuperTextField], + /// but [SuperTextField] defers to these overrides. + final SuperTextFieldGestureOverrides desktopGestureOverrides; + @override State createState() => SuperTextFieldState(); } -class SuperTextFieldState extends State { +class SuperTextFieldState extends State implements ProseTextBlock { final _platformFieldKey = GlobalKey(); late ImeAttributedTextEditingController _controller; @@ -176,7 +186,7 @@ class SuperTextFieldState extends State { @visibleForTesting AttributedTextEditingController get controller => _controller; - @visibleForTesting + @override ProseTextLayout get textLayout => (_platformFieldKey.currentState as ProseTextBlock).textLayout; bool get _isMultiline => (widget.minLines ?? 1) != 1 || (widget.maxLines ?? 1) != 1; @@ -238,6 +248,7 @@ class SuperTextFieldState extends State { minLines: widget.minLines, maxLines: widget.maxLines, keyboardHandlers: widget.keyboardHandlers, + gestureOverrides: widget.desktopGestureOverrides, ); case SuperTextFieldPlatformConfiguration.android: return Shortcuts( diff --git a/super_editor/test/super_textfield/super_desktop_textfield_gesture_extensions_test.dart b/super_editor/test/super_textfield/super_desktop_textfield_gesture_extensions_test.dart new file mode 100644 index 000000000..86bb36223 --- /dev/null +++ b/super_editor/test/super_textfield/super_desktop_textfield_gesture_extensions_test.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:super_editor/src/infrastructure/multi_tap_gesture.dart'; +import 'package:super_editor/src/infrastructure/super_textfield/super_textfield.dart'; + +import '../test_tools.dart'; +import 'super_textfield_inspector.dart'; + +void main() { + // TODO: try this approach: + // SuperTextField( + // customDesktopGesturesBuilder: (context, proseTextBlock, child) { + // return ContextMenuTextFieldGestures( + // child: child, + // ); + // }, + // ); + + group("SuperTextField on desktop", () { + group("overrides gestures", () { + testWidgetsOnDesktop("with widget properties", (tester) async { + int tapDownCount = 0; + + await _pumpScaffold( + tester, + child: SuperTextField( + desktopGestureOverrides: CallbackSuperTextFieldGestureOverrides(onTapDown: (details) { + tapDownCount += 1; + return GestureOverrideResult.handled; + }), + ), + ); + + // Attempt to place the caret with a tap. + await tester.tap(find.byType(SuperTextField)); + await tester.pump(kTapMinTime + const Duration(milliseconds: 1)); + + // Ensure that our override was called. + expect(tapDownCount, 1); + + // Ensure that our override prevented text field selection. + expect(SuperTextFieldInspector.findSelection(), const TextSelection.collapsed(offset: -1)); + }); + + testWidgetsOnDesktop("with an ancestor SuperTextFieldDesktopGestureExtensions", (tester) async { + final textFieldKey = GlobalKey(); + int tapDownCount = 0; + + await _pumpScaffold( + tester, + child: SuperTextFieldDesktopGestureExtensions( + superTextFieldKey: textFieldKey, + onTapDown: (details) { + tapDownCount += 1; + return GestureOverrideResult.handled; + }, + child: SuperTextField( + key: textFieldKey, + ), + ), + ); + + // Attempt to place the caret with a tap. + await tester.tap(find.byType(SuperTextField)); + await tester.pump(kTapMinTime + const Duration(milliseconds: 1)); + + // Ensure that our override was called. + expect(tapDownCount, 1); + + // Ensure that our override prevented text field selection. + expect(SuperTextFieldInspector.findSelection(), const TextSelection.collapsed(offset: -1)); + }); + }); + }); +} + +Future _pumpScaffold( + WidgetTester tester, { + required Widget child, +}) { + return tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 300), + child: child, + ), + ), + ), + ), + ); +} From 093b7167f6d40f10f78ad5908d5f165579924273 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Sat, 9 Jul 2022 15:07:45 -0700 Subject: [PATCH 2/3] Re-implemented gesture overiddes with a widget instead of callbacks --- .../demo_textfield_with_context_menu.dart | 121 ++++++++++------- .../android/_user_interaction.dart | 5 + .../android/android_textfield.dart | 5 + .../desktop/desktop_textfield.dart | 125 ++++++------------ .../desktop_textfield_gesture_extensions.dart | 103 --------------- .../infrastructure/gesture_overrides.dart | 17 +++ .../ios/_user_interaction.dart | 16 ++- .../super_textfield/ios/ios_textfield.dart | 7 +- .../super_textfield/super_textfield.dart | 14 +- ...top_textfield_gesture_extensions_test.dart | 60 +++------ 10 files changed, 184 insertions(+), 289 deletions(-) delete mode 100644 super_editor/lib/src/infrastructure/super_textfield/desktop/desktop_textfield_gesture_extensions.dart create mode 100644 super_editor/lib/src/infrastructure/super_textfield/infrastructure/gesture_overrides.dart diff --git a/super_editor/example/lib/demos/supertextfield/demo_textfield_with_context_menu.dart b/super_editor/example/lib/demos/supertextfield/demo_textfield_with_context_menu.dart index 7fea807c4..859f6abb0 100644 --- a/super_editor/example/lib/demos/supertextfield/demo_textfield_with_context_menu.dart +++ b/super_editor/example/lib/demos/supertextfield/demo_textfield_with_context_menu.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; 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'; /// Demo of [SuperTextField] with a context menu that opens in response /// to various gestures on desktop. @@ -11,8 +12,7 @@ class TextFieldWithContextMenuDemo extends StatefulWidget { } class _TextFieldWithContextMenuDemoState extends State { - final _superTextFieldKey = GlobalKey(); - + GlobalKey? _textFieldKey; late final _contextMenuOverlay; KeyMessageHandler? _existingKeyMessageHandler; @@ -53,74 +53,95 @@ class _TextFieldWithContextMenuDemoState extends State keyboardHandlers; + final GestureOverrideBuilder? gestureOverrideBuilder; + + final Map>? gestureOverrideRecognizer; + @override SuperDesktopTextFieldState createState() => SuperDesktopTextFieldState(); } @@ -289,8 +295,8 @@ class SuperDesktopTextFieldState extends State implements textKey: _textKey, textScrollKey: _textScrollKey, isMultiline: isMultiline, - gestureOverrides: widget.gestureOverrides, onRightClick: widget.onRightClick, + gestureOverrideBuilder: widget.gestureOverrideBuilder, child: MultiListenableBuilder( listenables: { _focusNode, @@ -370,8 +376,8 @@ class SuperTextFieldGestureInteractor extends StatefulWidget { required this.textKey, required this.textScrollKey, required this.isMultiline, - this.gestureOverrides = SuperTextFieldGestureOverrides.none, this.onRightClick, + this.gestureOverrideBuilder, required this.child, }) : super(key: key); @@ -393,17 +399,21 @@ class SuperTextFieldGestureInteractor extends StatefulWidget { /// Whether or not this text field supports multiple lines of text. final bool isMultiline; - /// Overrides default text field gestures, so that clients can handle - /// some of them instead. - /// - /// For example, a client may want to open a context menu on right-click, - /// or on ALT + left-click. - final SuperTextFieldGestureOverrides gestureOverrides; - /// Callback invoked when the user right clicks on this text field. @Deprecated("Use the gesture override system to take control of desired gestures") final RightClickListener? onRightClick; + /// {@template SuperTextField_gestureOverrideBuilder} + /// Widget that takes up as much space as the text field, and can be used to override + /// default text field gestures, so that clients can handle some of them instead. + /// + /// For example, a client may want to open a context menu on right-click, + /// or on ALT + left-click. Rather than add all possible gesture controls to this + /// text field, clients can pass a [gestureOverrideBuilder] to respond to any + /// set of desired gestures. + /// {@endtemplate} + final GestureOverrideBuilder? gestureOverrideBuilder; + /// The rest of the subtree for this text field. final Widget child; @@ -428,17 +438,6 @@ class _SuperTextFieldGestureInteractorState extends State{ + TapGestureRecognizer: GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(), + (TapGestureRecognizer recognizer) { + recognizer.onSecondaryTapUp = _onRightTapUp; + }, + ), TapSequenceGestureRecognizer: GestureRecognizerFactoryWithHandlers( () => TapSequenceGestureRecognizer(), (TapSequenceGestureRecognizer recognizer) { recognizer ..onTapDown = _onTapDown - ..onTapUp = _onTapUp ..onDoubleTapDown = _onDoubleTapDown ..onDoubleTapUp = _onDoubleTapUp ..onTripleTapDown = _onTripleTapDown @@ -833,13 +778,23 @@ class _SuperTextFieldGestureInteractorState extends State createState() => SuperTextFieldDesktopGestureExtensionsState(); -} - -class SuperTextFieldDesktopGestureExtensionsState extends State - with SuperTextFieldGestureOverrides { - @override - GestureOverrideResult onTapDown(SuperTextFieldTapDetails details) { - return widget.onTapDown?.call(details) ?? GestureOverrideResult.notHandled; - } - - @override - GestureOverrideResult onTapUp(SuperTextFieldTapDetails details) { - return widget.onTapUp?.call(details) ?? GestureOverrideResult.notHandled; - } - - void _onRightTapDown(TapDownDetails details) { - widget.onRightTapDown?.call(_tapDetailsFromTapDown(details)); - } - - void _onRightTapUp(TapUpDetails details) { - widget.onRightTapUp?.call(_tapDetailsFromTapUp(details)); - } - - // Converts a Flutter `TapDownDetails` to our `SuperTextFieldTapDetails`. - SuperTextFieldTapDetails _tapDetailsFromTapDown(TapDownDetails details) { - final textLayout = (widget.superTextFieldKey.currentState as ProseTextBlock).textLayout; - final textBox = (widget.superTextFieldKey.currentContext)!.findRenderObject() as RenderBox; - final nearestTextPosition = textLayout.getPositionNearestToOffset( - textBox.globalToLocal(details.globalPosition), - ); - - return SuperTextFieldTapDetails( - globalOffset: details.globalPosition, - textFieldRenderBox: textBox, - nearestTextPosition: nearestTextPosition, - ); - } - - // Converts a Flutter `TapUpDetails` to our `SuperTextFieldTapDetails`. - SuperTextFieldTapDetails _tapDetailsFromTapUp(TapUpDetails details) { - final textLayout = (widget.superTextFieldKey.currentState as ProseTextBlock).textLayout; - final textBox = (widget.superTextFieldKey.currentContext)!.findRenderObject() as RenderBox; - final nearestTextPosition = textLayout.getPositionNearestToOffset( - textBox.globalToLocal(details.globalPosition), - ); - - return SuperTextFieldTapDetails( - globalOffset: details.globalPosition, - textFieldRenderBox: textBox, - nearestTextPosition: nearestTextPosition, - ); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onSecondaryTapDown: widget.onRightTapDown != null ? _onRightTapDown : null, - onSecondaryTapUp: widget.onRightTapUp != null ? _onRightTapUp : null, - behavior: HitTestBehavior.translucent, - child: widget.child, - ); - } -} diff --git a/super_editor/lib/src/infrastructure/super_textfield/infrastructure/gesture_overrides.dart b/super_editor/lib/src/infrastructure/super_textfield/infrastructure/gesture_overrides.dart new file mode 100644 index 000000000..a42b04d9b --- /dev/null +++ b/super_editor/lib/src/infrastructure/super_textfield/infrastructure/gesture_overrides.dart @@ -0,0 +1,17 @@ +import 'package:flutter/widgets.dart'; + +/// Builder that returns a widget that handles desired gestures, instead of +/// letting [SuperTextField] respond to those gestures. +/// +/// The given [textFieldKey] can be used to access the [RenderBox] and the +/// [ProseTextLayout] for the [SuperTextField]: +/// +/// ``` +/// final renderBox = textFieldKey.currentContext.findRenderObject as RenderBox; +/// final textLayout = (textFieldKey.currentState as ProseTextBlock).textLayout; +/// ``` +/// +/// The given [child] represents everything in [SuperTextField] beneath the gesture +/// system. If [child] is non-null, the return [Widget] **must** include that [child] +/// in its sub-tree. +typedef GestureOverrideBuilder = Widget Function(BuildContext, GlobalKey textFieldKey, [Widget? child]); diff --git a/super_editor/lib/src/infrastructure/super_textfield/ios/_user_interaction.dart b/super_editor/lib/src/infrastructure/super_textfield/ios/_user_interaction.dart index 0c19bc60a..7ba06c6c0 100644 --- a/super_editor/lib/src/infrastructure/super_textfield/ios/_user_interaction.dart +++ b/super_editor/lib/src/infrastructure/super_textfield/ios/_user_interaction.dart @@ -2,6 +2,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:super_editor/src/infrastructure/_logging.dart'; import 'package:super_editor/src/infrastructure/multi_tap_gesture.dart'; +import 'package:super_editor/src/infrastructure/super_textfield/infrastructure/gesture_overrides.dart'; import 'package:super_editor/src/infrastructure/super_textfield/super_textfield.dart'; import 'package:super_text_layout/super_text_layout.dart'; @@ -39,9 +40,10 @@ class IOSTextFieldTouchInteractor extends StatefulWidget { required this.textController, required this.editingOverlayController, required this.textScrollController, - required this.selectableTextKey, + required this.textKey, required this.isMultiline, required this.handleColor, + this.gestureOverrideBuilder, this.showDebugPaint = false, required this.child, }) : super(key: key); @@ -71,7 +73,7 @@ class IOSTextFieldTouchInteractor extends StatefulWidget { /// [GlobalKey] that references the widget that contains the field's /// text. - final GlobalKey selectableTextKey; + final GlobalKey textKey; /// Whether the text field that owns this [IOSTextFieldInteractor] is /// a multiline text field. @@ -80,6 +82,8 @@ class IOSTextFieldTouchInteractor extends StatefulWidget { /// The color of expanded selection drag handles. final Color handleColor; + final GestureOverrideBuilder? gestureOverrideBuilder; + /// Whether to paint debugging guides and regions. final bool showDebugPaint; @@ -123,7 +127,7 @@ class IOSTextFieldTouchInteractorState extends State widget.selectableTextKey.currentState!.textLayout; + ProseTextLayout get _textLayout => widget.textKey.currentState!.textLayout; void _onTapDown(TapDownDetails details) { _log.fine("User tapped down"); @@ -299,7 +303,7 @@ class IOSTextFieldTouchInteractorState extends State link: _textFieldLayerLink, child: IOSTextFieldTouchInteractor( focusNode: _focusNode, - selectableTextKey: _textContentKey, + textKey: _textContentKey, textFieldLayerLink: _textFieldLayerLink, textController: _textEditingController, editingOverlayController: _editingOverlayController, diff --git a/super_editor/lib/src/infrastructure/super_textfield/super_textfield.dart b/super_editor/lib/src/infrastructure/super_textfield/super_textfield.dart index e61977a19..d2ba50f7a 100644 --- a/super_editor/lib/src/infrastructure/super_textfield/super_textfield.dart +++ b/super_editor/lib/src/infrastructure/super_textfield/super_textfield.dart @@ -1,11 +1,13 @@ import 'package:attributed_text/attributed_text.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:super_editor/src/infrastructure/attributed_text_styles.dart'; import 'package:super_editor/src/infrastructure/super_textfield/android/android_textfield.dart'; import 'package:super_editor/src/infrastructure/super_textfield/desktop/desktop_textfield.dart'; import 'package:super_editor/src/infrastructure/super_textfield/infrastructure/attributed_text_editing_controller.dart'; +import 'package:super_editor/src/infrastructure/super_textfield/infrastructure/gesture_overrides.dart'; import 'package:super_editor/src/infrastructure/super_textfield/infrastructure/hint_text.dart'; import 'package:super_editor/src/infrastructure/super_textfield/input_method_engine/_ime_text_editing_controller.dart'; import 'package:super_editor/src/infrastructure/super_textfield/ios/ios_textfield.dart'; @@ -14,9 +16,9 @@ import 'package:super_text_layout/super_text_layout.dart'; import 'styles.dart'; export 'android/android_textfield.dart'; -export 'desktop/desktop_textfield_gesture_extensions.dart'; export 'desktop/desktop_textfield.dart'; export 'infrastructure/attributed_text_editing_controller.dart'; +export 'infrastructure/gesture_overrides.dart'; export 'infrastructure/hint_text.dart'; export 'infrastructure/magnifier.dart'; export 'infrastructure/text_scrollview.dart'; @@ -65,6 +67,8 @@ class SuperTextField extends StatefulWidget { this.lineHeight, this.keyboardHandlers = defaultTextFieldKeyboardHandlers, this.desktopGestureOverrides = SuperTextFieldGestureOverrides.none, + this.gestureOverrideBuilder, + this.desktopGestureOverrideRecognizer, }) : super(key: key); final FocusNode? focusNode; @@ -151,6 +155,10 @@ class SuperTextField extends StatefulWidget { /// but [SuperTextField] defers to these overrides. final SuperTextFieldGestureOverrides desktopGestureOverrides; + final GestureOverrideBuilder? gestureOverrideBuilder; + + final Map>? desktopGestureOverrideRecognizer; + @override State createState() => SuperTextFieldState(); } @@ -249,6 +257,8 @@ class SuperTextFieldState extends State implements ProseTextBloc maxLines: widget.maxLines, keyboardHandlers: widget.keyboardHandlers, gestureOverrides: widget.desktopGestureOverrides, + gestureOverrideBuilder: widget.gestureOverrideBuilder, + gestureOverrideRecognizer: widget.desktopGestureOverrideRecognizer, ); case SuperTextFieldPlatformConfiguration.android: return Shortcuts( @@ -268,6 +278,7 @@ class SuperTextFieldState extends State implements ProseTextBloc maxLines: widget.maxLines, lineHeight: widget.lineHeight, textInputAction: _isMultiline ? TextInputAction.newline : TextInputAction.done, + gestureOverrideBuilder: widget.gestureOverrideBuilder, ), ); case SuperTextFieldPlatformConfiguration.iOS: @@ -288,6 +299,7 @@ class SuperTextFieldState extends State implements ProseTextBloc maxLines: widget.maxLines, lineHeight: widget.lineHeight, textInputAction: _isMultiline ? TextInputAction.newline : TextInputAction.done, + gestureOverrideBuilder: widget.gestureOverrideBuilder, ), ); } diff --git a/super_editor/test/super_textfield/super_desktop_textfield_gesture_extensions_test.dart b/super_editor/test/super_textfield/super_desktop_textfield_gesture_extensions_test.dart index 86bb36223..059bced0b 100644 --- a/super_editor/test/super_textfield/super_desktop_textfield_gesture_extensions_test.dart +++ b/super_editor/test/super_textfield/super_desktop_textfield_gesture_extensions_test.dart @@ -1,21 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:super_editor/src/infrastructure/multi_tap_gesture.dart'; -import 'package:super_editor/src/infrastructure/super_textfield/super_textfield.dart'; +import 'package:super_editor/super_editor.dart'; import '../test_tools.dart'; import 'super_textfield_inspector.dart'; void main() { - // TODO: try this approach: - // SuperTextField( - // customDesktopGesturesBuilder: (context, proseTextBlock, child) { - // return ContextMenuTextFieldGestures( - // child: child, - // ); - // }, - // ); - group("SuperTextField on desktop", () { group("overrides gestures", () { testWidgetsOnDesktop("with widget properties", (tester) async { @@ -24,39 +14,23 @@ void main() { await _pumpScaffold( tester, child: SuperTextField( - desktopGestureOverrides: CallbackSuperTextFieldGestureOverrides(onTapDown: (details) { - tapDownCount += 1; - return GestureOverrideResult.handled; - }), - ), - ); - - // Attempt to place the caret with a tap. - await tester.tap(find.byType(SuperTextField)); - await tester.pump(kTapMinTime + const Duration(milliseconds: 1)); - - // Ensure that our override was called. - expect(tapDownCount, 1); - - // Ensure that our override prevented text field selection. - expect(SuperTextFieldInspector.findSelection(), const TextSelection.collapsed(offset: -1)); - }); - - testWidgetsOnDesktop("with an ancestor SuperTextFieldDesktopGestureExtensions", (tester) async { - final textFieldKey = GlobalKey(); - int tapDownCount = 0; - - await _pumpScaffold( - tester, - child: SuperTextFieldDesktopGestureExtensions( - superTextFieldKey: textFieldKey, - onTapDown: (details) { - tapDownCount += 1; - return GestureOverrideResult.handled; + // TODO: need to provide ProseTextBlock to the callback to look up + // text position + gestureOverrideBuilder: (context, textLayoutResolver, [child]) { + print("BUILDING OVERRIDE GESTURE DETECTOR"); + return GestureDetector( + onTapDown: (details) { + tapDownCount += 1; + }, + behavior: HitTestBehavior.translucent, + child: Container( + decoration: BoxDecoration( + color: Colors.red.withOpacity(1.0), + ), + child: child, + ), + ); }, - child: SuperTextField( - key: textFieldKey, - ), ), ); From 93c262c9a5f2b497b4eace7900a507816bc2851d Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Sat, 9 Jul 2022 15:26:09 -0700 Subject: [PATCH 3/3] PR Updates --- .../android/_user_interaction.dart | 4 +- .../desktop/desktop_textfield.dart | 144 +----------------- .../ios/_user_interaction.dart | 4 +- .../super_textfield/super_textfield.dart | 16 +- ...top_textfield_gesture_extensions_test.dart | 3 - 5 files changed, 9 insertions(+), 162 deletions(-) diff --git a/super_editor/lib/src/infrastructure/super_textfield/android/_user_interaction.dart b/super_editor/lib/src/infrastructure/super_textfield/android/_user_interaction.dart index 51721e4f2..cdc13bebd 100644 --- a/super_editor/lib/src/infrastructure/super_textfield/android/_user_interaction.dart +++ b/super_editor/lib/src/infrastructure/super_textfield/android/_user_interaction.dart @@ -3,7 +3,8 @@ import 'package:flutter/material.dart'; import 'package:super_editor/src/infrastructure/_logging.dart'; import 'package:super_editor/src/infrastructure/multi_tap_gesture.dart'; import 'package:super_editor/src/infrastructure/super_textfield/infrastructure/gesture_overrides.dart'; -import 'package:super_editor/src/infrastructure/super_textfield/super_textfield.dart'; +import 'package:super_editor/src/infrastructure/super_textfield/infrastructure/text_scrollview.dart'; +import 'package:super_editor/src/infrastructure/super_textfield/input_method_engine/_ime_text_editing_controller.dart'; import 'package:super_text_layout/super_text_layout.dart'; import '_editing_controls.dart'; @@ -86,6 +87,7 @@ class AndroidTextFieldTouchInteractor extends StatefulWidget { /// The color of expanded selection drag handles. final Color handleColor; + /// {@macros SuperTextField_gestureOverrideBuilder} final GestureOverrideBuilder? gestureOverrideBuilder; /// Whether to paint debugging guides and regions. diff --git a/super_editor/lib/src/infrastructure/super_textfield/desktop/desktop_textfield.dart b/super_editor/lib/src/infrastructure/super_textfield/desktop/desktop_textfield.dart index 7eeb5d4ce..246e0581f 100644 --- a/super_editor/lib/src/infrastructure/super_textfield/desktop/desktop_textfield.dart +++ b/super_editor/lib/src/infrastructure/super_textfield/desktop/desktop_textfield.dart @@ -56,8 +56,6 @@ class SuperDesktopTextField extends StatefulWidget { this.minLines, this.maxLines = 1, this.decorationBuilder, - this.gestureOverrides = SuperTextFieldGestureOverrides.none, - this.gestureOverrideRecognizer, this.onRightClick, this.keyboardHandlers = defaultTextFieldKeyboardHandlers, this.gestureOverrideBuilder, @@ -95,13 +93,6 @@ class SuperDesktopTextField extends StatefulWidget { final DecorationBuilder? decorationBuilder; - /// Overrides default text field gestures, so that clients can handle - /// some of them instead. - /// - /// For example, a client may want to open a context menu on right-click, - /// or on ALT + left-click. - final SuperTextFieldGestureOverrides gestureOverrides; - @Deprecated("Use gestureOverrideBuilder to take control of desired gestures") final RightClickListener? onRightClick; @@ -109,10 +100,9 @@ class SuperDesktopTextField extends StatefulWidget { /// key presses, for text input, deletion, caret movement, etc. final List keyboardHandlers; + /// {@macros SuperTextField_gestureOverrideBuilder} final GestureOverrideBuilder? gestureOverrideBuilder; - final Map>? gestureOverrideRecognizer; - @override SuperDesktopTextFieldState createState() => SuperDesktopTextFieldState(); } @@ -400,7 +390,7 @@ class SuperTextFieldGestureInteractor extends StatefulWidget { final bool isMultiline; /// Callback invoked when the user right clicks on this text field. - @Deprecated("Use the gesture override system to take control of desired gestures") + @Deprecated("Use gestureOverrideBuilder to take control of desired gestures") final RightClickListener? onRightClick; /// {@template SuperTextField_gestureOverrideBuilder} @@ -797,136 +787,6 @@ class _SuperTextFieldGestureInteractorState extends State GestureOverrideResult.notHandled; - GestureOverrideResult onTapUp(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; - - GestureOverrideResult onRightTapDown(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; - GestureOverrideResult onRightTapUp(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; - - GestureOverrideResult onDoubleTapDown(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; - GestureOverrideResult onDoubleTapUp(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; - - GestureOverrideResult onTripleTapDown(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; - GestureOverrideResult onTripleTapUp(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; -} - -class CallbackSuperTextFieldGestureOverrides implements SuperTextFieldGestureOverrides { - CallbackSuperTextFieldGestureOverrides({ - GestureOverrideHandler? onTapDown, - GestureOverrideHandler? onTapUp, - GestureOverrideHandler? onDoubleTapDown, - GestureOverrideHandler? onDoubleTapUp, - GestureOverrideHandler? onTripleTapDown, - GestureOverrideHandler? onTripleTapUp, - GestureOverrideHandler? onRightTapDown, - GestureOverrideHandler? onRightTapUp, - }) : _onTapDown = onTapDown, - _onTapUp = onTapUp, - _onDoubleTapDown = onDoubleTapDown, - _onDoubleTapUp = onDoubleTapUp, - _onTripleTapDown = onTripleTapDown, - _onTripleTapUp = onTripleTapUp, - _onRightTapDown = onRightTapDown, - _onRightTapUp = onRightTapUp; - - @override - GestureOverrideResult onTapDown(SuperTextFieldTapDetails details) => - _onTapDown?.call(details) ?? GestureOverrideResult.notHandled; - final GestureOverrideHandler? _onTapDown; - - @override - GestureOverrideResult onTapUp(SuperTextFieldTapDetails details) => - _onTapUp?.call(details) ?? GestureOverrideResult.notHandled; - final GestureOverrideHandler? _onTapUp; - - @override - GestureOverrideResult onRightTapDown(SuperTextFieldTapDetails details) => - _onRightTapDown?.call(details) ?? GestureOverrideResult.notHandled; - final GestureOverrideHandler? _onRightTapDown; - - @override - GestureOverrideResult onRightTapUp(SuperTextFieldTapDetails details) => - _onRightTapUp?.call(details) ?? GestureOverrideResult.notHandled; - final GestureOverrideHandler? _onRightTapUp; - - @override - GestureOverrideResult onDoubleTapDown(SuperTextFieldTapDetails details) => - _onDoubleTapDown?.call(details) ?? GestureOverrideResult.notHandled; - final GestureOverrideHandler? _onDoubleTapDown; - - @override - GestureOverrideResult onDoubleTapUp(SuperTextFieldTapDetails details) => - _onDoubleTapUp?.call(details) ?? GestureOverrideResult.notHandled; - final GestureOverrideHandler? _onDoubleTapUp; - - @override - GestureOverrideResult onTripleTapDown(SuperTextFieldTapDetails details) => - _onTripleTapDown?.call(details) ?? GestureOverrideResult.notHandled; - final GestureOverrideHandler? _onTripleTapDown; - - @override - GestureOverrideResult onTripleTapUp(SuperTextFieldTapDetails details) => - _onTripleTapUp?.call(details) ?? GestureOverrideResult.notHandled; - final GestureOverrideHandler? _onTripleTapUp; -} - -/// A no-op implementation of [SuperTextFieldGestureOverrides], which is available as -/// a public constant in [SuperTextFieldGestureOverrides.none]. -class _NoSuperTextFieldGestureOverrides implements SuperTextFieldGestureOverrides { - const _NoSuperTextFieldGestureOverrides(); - - @override - GestureOverrideResult onTapDown(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; - @override - GestureOverrideResult onTapUp(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; - - @override - GestureOverrideResult onRightTapDown(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; - @override - GestureOverrideResult onRightTapUp(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; - - @override - GestureOverrideResult onDoubleTapDown(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; - @override - GestureOverrideResult onDoubleTapUp(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; - - @override - GestureOverrideResult onTripleTapDown(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; - @override - GestureOverrideResult onTripleTapUp(SuperTextFieldTapDetails details) => GestureOverrideResult.notHandled; -} - -typedef GestureOverrideHandler = GestureOverrideResult Function(SuperTextFieldTapDetails details); - -/// Details about where a tap took place in a [SuperTextField] so that a client -/// can respond with a gesture override. -class SuperTextFieldTapDetails { - const SuperTextFieldTapDetails({ - required this.globalOffset, - required this.textFieldRenderBox, - required this.nearestTextPosition, - }); - - final Offset globalOffset; - final RenderBox textFieldRenderBox; - final TextPosition nearestTextPosition; -} - -enum GestureOverrideResult { - /// The gesture should NOT be handled by SuperTextField. - handled, - - /// The gesture should be handled by SuperTextField. - notHandled, -} - /// Handles all keyboard interactions for text entry in a text field. /// /// [SuperTextFieldKeyboardInteractor] is intended to operate as a piece within diff --git a/super_editor/lib/src/infrastructure/super_textfield/ios/_user_interaction.dart b/super_editor/lib/src/infrastructure/super_textfield/ios/_user_interaction.dart index 7ba06c6c0..f2c7960df 100644 --- a/super_editor/lib/src/infrastructure/super_textfield/ios/_user_interaction.dart +++ b/super_editor/lib/src/infrastructure/super_textfield/ios/_user_interaction.dart @@ -3,7 +3,8 @@ import 'package:flutter/material.dart'; import 'package:super_editor/src/infrastructure/_logging.dart'; import 'package:super_editor/src/infrastructure/multi_tap_gesture.dart'; import 'package:super_editor/src/infrastructure/super_textfield/infrastructure/gesture_overrides.dart'; -import 'package:super_editor/src/infrastructure/super_textfield/super_textfield.dart'; +import 'package:super_editor/src/infrastructure/super_textfield/infrastructure/text_scrollview.dart'; +import 'package:super_editor/src/infrastructure/super_textfield/input_method_engine/_ime_text_editing_controller.dart'; import 'package:super_text_layout/super_text_layout.dart'; import '_editing_controls.dart'; @@ -82,6 +83,7 @@ class IOSTextFieldTouchInteractor extends StatefulWidget { /// The color of expanded selection drag handles. final Color handleColor; + /// {@macros SuperTextField_gestureOverrideBuilder} final GestureOverrideBuilder? gestureOverrideBuilder; /// Whether to paint debugging guides and regions. diff --git a/super_editor/lib/src/infrastructure/super_textfield/super_textfield.dart b/super_editor/lib/src/infrastructure/super_textfield/super_textfield.dart index d2ba50f7a..6e41475a4 100644 --- a/super_editor/lib/src/infrastructure/super_textfield/super_textfield.dart +++ b/super_editor/lib/src/infrastructure/super_textfield/super_textfield.dart @@ -1,6 +1,5 @@ import 'package:attributed_text/attributed_text.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:super_editor/src/infrastructure/attributed_text_styles.dart'; @@ -66,9 +65,7 @@ class SuperTextField extends StatefulWidget { this.maxLines = 1, this.lineHeight, this.keyboardHandlers = defaultTextFieldKeyboardHandlers, - this.desktopGestureOverrides = SuperTextFieldGestureOverrides.none, this.gestureOverrideBuilder, - this.desktopGestureOverrideRecognizer, }) : super(key: key); final FocusNode? focusNode; @@ -147,18 +144,9 @@ class SuperTextField extends StatefulWidget { /// Only used on desktop. final List keyboardHandlers; - /// Gesture overrides for desktop interaction. - /// - /// Gesture overrides are useful, for example, when implementing a - /// popover context menu on right-click, or on ALT + left-click. - /// Ordinarily, these gestures would be handled by [SuperTextField], - /// but [SuperTextField] defers to these overrides. - final SuperTextFieldGestureOverrides desktopGestureOverrides; - + /// {@macros SuperTextField_gestureOverrideBuilder} final GestureOverrideBuilder? gestureOverrideBuilder; - final Map>? desktopGestureOverrideRecognizer; - @override State createState() => SuperTextFieldState(); } @@ -256,9 +244,7 @@ class SuperTextFieldState extends State implements ProseTextBloc minLines: widget.minLines, maxLines: widget.maxLines, keyboardHandlers: widget.keyboardHandlers, - gestureOverrides: widget.desktopGestureOverrides, gestureOverrideBuilder: widget.gestureOverrideBuilder, - gestureOverrideRecognizer: widget.desktopGestureOverrideRecognizer, ); case SuperTextFieldPlatformConfiguration.android: return Shortcuts( diff --git a/super_editor/test/super_textfield/super_desktop_textfield_gesture_extensions_test.dart b/super_editor/test/super_textfield/super_desktop_textfield_gesture_extensions_test.dart index 059bced0b..4e1d477e0 100644 --- a/super_editor/test/super_textfield/super_desktop_textfield_gesture_extensions_test.dart +++ b/super_editor/test/super_textfield/super_desktop_textfield_gesture_extensions_test.dart @@ -14,10 +14,7 @@ void main() { await _pumpScaffold( tester, child: SuperTextField( - // TODO: need to provide ProseTextBlock to the callback to look up - // text position gestureOverrideBuilder: (context, textLayoutResolver, [child]) { - print("BUILDING OVERRIDE GESTURE DETECTOR"); return GestureDetector( onTapDown: (details) { tapDownCount += 1;