From a95873b21ad0f5934764bbe7fd109a9cd40a2d98 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Sun, 26 May 2024 01:35:10 -0700 Subject: [PATCH 1/6] Use native iOS edit menu when available (Resolves #2032) --- .../.run/Flutter - Text Field.run.xml | 6 ++ .../flutter_demos/main_flutter_textfield.dart | 55 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 super_editor/.run/Flutter - Text Field.run.xml create mode 100644 super_editor/example/lib/flutter_demos/main_flutter_textfield.dart diff --git a/super_editor/.run/Flutter - Text Field.run.xml b/super_editor/.run/Flutter - Text Field.run.xml new file mode 100644 index 000000000..bc96023a3 --- /dev/null +++ b/super_editor/.run/Flutter - Text Field.run.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/super_editor/example/lib/flutter_demos/main_flutter_textfield.dart b/super_editor/example/lib/flutter_demos/main_flutter_textfield.dart new file mode 100644 index 000000000..c0dad4794 --- /dev/null +++ b/super_editor/example/lib/flutter_demos/main_flutter_textfield.dart @@ -0,0 +1,55 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp(_FlutterTextFieldDemoApp()); +} + +class _FlutterTextFieldDemoApp extends StatelessWidget { + const _FlutterTextFieldDemoApp(); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: Padding( + padding: const EdgeInsets.all(24), + child: Center( + child: _DemoTextField(), + ), + ), + ), + ); + } +} + +class _DemoTextField extends StatefulWidget { + const _DemoTextField(); + + @override + State<_DemoTextField> createState() => _DemoTextFieldState(); +} + +class _DemoTextFieldState extends State<_DemoTextField> { + final _textController = TextEditingController(); + + @override + void dispose() { + _textController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return TextField( + controller: _textController, + decoration: InputDecoration( + hintText: "Enter text...", + ), + contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) { + // return AdaptiveTextSelectionToolbar.editableText( + // editableTextState: editableTextState, + // ); + }); + } +} From 150bf522ef1aed3f7572660326b4382980d7a192 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Tue, 28 May 2024 22:44:14 -0700 Subject: [PATCH 2/6] Add popover toolbar builder that uses Flutter's new support for an iOS 16+ system toolbar (Resolves #2032) --- .../flutter_demos/main_flutter_textfield.dart | 14 ++- .../ios/{_caret.dart => caret.dart} | 0 ...ng_controls.dart => editing_controls.dart} | 0 ...ating_cursor.dart => floating_cursor.dart} | 0 .../ios/ios_system_context_menu.dart | 87 +++++++++++++++++++ .../super_textfield/ios/ios_textfield.dart | 35 ++++++-- ...interaction.dart => user_interaction.dart} | 2 +- 7 files changed, 126 insertions(+), 12 deletions(-) rename super_editor/lib/src/super_textfield/ios/{_caret.dart => caret.dart} (100%) rename super_editor/lib/src/super_textfield/ios/{_editing_controls.dart => editing_controls.dart} (100%) rename super_editor/lib/src/super_textfield/ios/{_floating_cursor.dart => floating_cursor.dart} (100%) create mode 100644 super_editor/lib/src/super_textfield/ios/ios_system_context_menu.dart rename super_editor/lib/src/super_textfield/ios/{_user_interaction.dart => user_interaction.dart} (99%) diff --git a/super_editor/example/lib/flutter_demos/main_flutter_textfield.dart b/super_editor/example/lib/flutter_demos/main_flutter_textfield.dart index c0dad4794..873461833 100644 --- a/super_editor/example/lib/flutter_demos/main_flutter_textfield.dart +++ b/super_editor/example/lib/flutter_demos/main_flutter_textfield.dart @@ -47,9 +47,17 @@ class _DemoTextFieldState extends State<_DemoTextField> { hintText: "Enter text...", ), contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) { - // return AdaptiveTextSelectionToolbar.editableText( - // editableTextState: editableTextState, - // ); + // If supported, show the system context menu. + if (SystemContextMenu.isSupported(context)) { + return SystemContextMenu.editableText( + editableTextState: editableTextState, + ); + } + // Otherwise, show the flutter-rendered context menu for the current + // platform. + return AdaptiveTextSelectionToolbar.editableText( + editableTextState: editableTextState, + ); }); } } diff --git a/super_editor/lib/src/super_textfield/ios/_caret.dart b/super_editor/lib/src/super_textfield/ios/caret.dart similarity index 100% rename from super_editor/lib/src/super_textfield/ios/_caret.dart rename to super_editor/lib/src/super_textfield/ios/caret.dart diff --git a/super_editor/lib/src/super_textfield/ios/_editing_controls.dart b/super_editor/lib/src/super_textfield/ios/editing_controls.dart similarity index 100% rename from super_editor/lib/src/super_textfield/ios/_editing_controls.dart rename to super_editor/lib/src/super_textfield/ios/editing_controls.dart diff --git a/super_editor/lib/src/super_textfield/ios/_floating_cursor.dart b/super_editor/lib/src/super_textfield/ios/floating_cursor.dart similarity index 100% rename from super_editor/lib/src/super_textfield/ios/_floating_cursor.dart rename to super_editor/lib/src/super_textfield/ios/floating_cursor.dart diff --git a/super_editor/lib/src/super_textfield/ios/ios_system_context_menu.dart b/super_editor/lib/src/super_textfield/ios/ios_system_context_menu.dart new file mode 100644 index 000000000..32c0a5af1 --- /dev/null +++ b/super_editor/lib/src/super_textfield/ios/ios_system_context_menu.dart @@ -0,0 +1,87 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +/// Displays the iOS system context menu on top of the Flutter view. +/// +/// This class was copied and adjusted from Flutter's [SystemContextMenu]. +/// +/// Currently, only supports iOS 16.0 and above. +/// +/// The context menu is the menu that appears, for example, when doing text +/// selection. Flutter typically draws this menu itself, but this class deals +/// with the platform-rendered context menu instead. +/// +/// There can only be one system context menu visible at a time. Building this +/// widget when the system context menu is already visible will hide the old one +/// and display this one. A system context menu that is hidden is informed via +/// [onSystemHide]. +/// +/// To check if the current device supports showing the system context menu, +/// call [isSupported]. +/// +/// See also: +/// +/// * [SystemContextMenuController], which directly controls the hiding and +/// showing of the system context menu. +class IOSSystemContextMenu extends StatefulWidget { + /// Whether the current device supports showing the system context menu. + /// + /// Currently, this is only supported on iOS 16.0 and above. + static bool isSupported(BuildContext context) { + return MediaQuery.maybeSupportsShowingSystemContextMenu(context) ?? false; + } + + const IOSSystemContextMenu({ + super.key, + required this.anchor, + this.onSystemHide, + }); + + /// The [Rect] that the context menu should point to. + final Rect anchor; + + /// Called when the system hides this context menu. + /// + /// For example, tapping outside of the context menu typically causes the + /// system to hide the menu. + /// + /// This is not called when showing a new system context menu causes another + /// to be hidden. + final VoidCallback? onSystemHide; + + @override + State createState() => _IOSSystemContextMenuState(); +} + +class _IOSSystemContextMenuState extends State { + late final SystemContextMenuController _systemContextMenuController; + + @override + void initState() { + super.initState(); + _systemContextMenuController = SystemContextMenuController( + onSystemHide: widget.onSystemHide, + ); + _systemContextMenuController.show(widget.anchor); + } + + @override + void didUpdateWidget(IOSSystemContextMenu oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.anchor != oldWidget.anchor) { + _systemContextMenuController.show(widget.anchor); + } + } + + @override + void dispose() { + _systemContextMenuController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + assert(IOSSystemContextMenu.isSupported(context)); + return const SizedBox.shrink(); + } +} diff --git a/super_editor/lib/src/super_textfield/ios/ios_textfield.dart b/super_editor/lib/src/super_textfield/ios/ios_textfield.dart index d3e165368..55f8fa95d 100644 --- a/super_editor/lib/src/super_textfield/ios/ios_textfield.dart +++ b/super_editor/lib/src/super_textfield/ios/ios_textfield.dart @@ -15,17 +15,20 @@ import 'package:super_editor/src/super_textfield/infrastructure/fill_width_if_co import 'package:super_editor/src/super_textfield/infrastructure/hint_text.dart'; import 'package:super_editor/src/super_textfield/infrastructure/text_scrollview.dart'; import 'package:super_editor/src/super_textfield/input_method_engine/_ime_text_editing_controller.dart'; -import 'package:super_editor/src/super_textfield/ios/_editing_controls.dart'; +import 'package:super_editor/src/super_textfield/ios/editing_controls.dart'; import 'package:super_text_layout/super_text_layout.dart'; import '../metrics.dart'; import '../styles.dart'; -import '_floating_cursor.dart'; -import '_user_interaction.dart'; +import 'floating_cursor.dart'; +import 'ios_system_context_menu.dart'; +import 'user_interaction.dart'; export '../infrastructure/magnifier.dart'; -export '_caret.dart'; -export '_user_interaction.dart'; +export 'caret.dart'; +export 'editing_controls.dart'; +export 'ios_system_context_menu.dart'; +export 'user_interaction.dart'; final _log = iosTextFieldLog; @@ -50,7 +53,7 @@ class SuperIOSTextField extends StatefulWidget { this.textInputAction, this.imeConfiguration, this.showComposingUnderline = true, - this.popoverToolbarBuilder = _defaultPopoverToolbarBuilder, + this.popoverToolbarBuilder = defaultIosPopoverToolbarBuilder, this.showDebugPaint = false, }) : super(key: key); @@ -149,7 +152,7 @@ class SuperIOSTextField extends StatefulWidget { final bool showComposingUnderline; /// Builder that creates the popover toolbar widget that appears when text is selected. - final Widget Function(BuildContext, IOSEditingOverlayController) popoverToolbarBuilder; + final IOSPopoverToolbarBuilder popoverToolbarBuilder; /// Whether to paint debug guides. final bool showDebugPaint; @@ -694,7 +697,23 @@ class SuperIOSTextFieldState extends State } } -Widget _defaultPopoverToolbarBuilder(BuildContext context, IOSEditingOverlayController controller) { +/// Builder that returns a widget for an iOS-style popover editing toolbar. +typedef IOSPopoverToolbarBuilder = Widget Function(BuildContext, IOSEditingOverlayController); + +/// An [IOSPopoverToolbarBuilder] that displays the iOS system popover toolbar, if the version of +/// iOS is recent enough, otherwise builds [defaultIosPopoverToolbarBuilder]. +Widget iOSSystemPopoverToolbarWithBackupFlutterVersion(BuildContext context, IOSEditingOverlayController controller) { + if (IOSSystemContextMenu.isSupported(context)) { + return IOSSystemContextMenu( + anchor: controller.toolbarFocalPoint.offset! & controller.toolbarFocalPoint.leaderSize!, + ); + } + + return defaultIosPopoverToolbarBuilder(context, controller); +} + +/// Returns a widget for the default/standard iOS-style popover provided by Super Text Field. +Widget defaultIosPopoverToolbarBuilder(BuildContext context, IOSEditingOverlayController controller) { return IOSTextEditingFloatingToolbar( focalPoint: controller.toolbarFocalPoint, onCutPressed: () { diff --git a/super_editor/lib/src/super_textfield/ios/_user_interaction.dart b/super_editor/lib/src/super_textfield/ios/user_interaction.dart similarity index 99% rename from super_editor/lib/src/super_textfield/ios/_user_interaction.dart rename to super_editor/lib/src/super_textfield/ios/user_interaction.dart index ba53e5434..237237f13 100644 --- a/super_editor/lib/src/super_textfield/ios/_user_interaction.dart +++ b/super_editor/lib/src/super_textfield/ios/user_interaction.dart @@ -8,7 +8,7 @@ import 'package:super_editor/src/infrastructure/multi_tap_gesture.dart'; import 'package:super_editor/src/super_textfield/super_textfield.dart'; import 'package:super_text_layout/super_text_layout.dart'; -import '_editing_controls.dart'; +import 'editing_controls.dart'; final _log = iosTextFieldLog; From fa38418becc23e2aeeb3d08e57bd2cdb55b4197c Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Tue, 9 Jul 2024 15:00:34 -0700 Subject: [PATCH 3/6] Switched iOS text field demo to use native context menu --- .../lib/demos/supertextfield/ios/demo_superiostextfield.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/super_editor/example/lib/demos/supertextfield/ios/demo_superiostextfield.dart b/super_editor/example/lib/demos/supertextfield/ios/demo_superiostextfield.dart index 07f6df7e0..f467c0f88 100644 --- a/super_editor/example/lib/demos/supertextfield/ios/demo_superiostextfield.dart +++ b/super_editor/example/lib/demos/supertextfield/ios/demo_superiostextfield.dart @@ -76,6 +76,7 @@ class _SuperIOSTextFieldDemoState extends State { maxLines: config.maxLines, lineHeight: lineHeight, textInputAction: TextInputAction.done, + popoverToolbarBuilder: iOSSystemPopoverToolbarWithBackupFlutterVersion, showDebugPaint: config.showDebugPaint, ), ); From 0e74c945eeaf095adc4fc95be5c374166fde613b Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Wed, 25 Sep 2024 16:18:57 -0700 Subject: [PATCH 4/6] WIP: Integrating iOS context menu with Super Editor --- .../feature_ios_native_context_menu.dart | 186 ++++++++++++++++++ .../in_the_lab/feature_pattern_tags.dart | 20 +- .../demos/in_the_lab/in_the_lab_scaffold.dart | 88 ++++++++- .../ios/demo_superiostextfield.dart | 2 +- super_editor/example/lib/main.dart | 8 + .../lib/src/default_editor/super_editor.dart | 37 ++++ .../ios/ios_system_context_menu.dart | 0 .../super_textfield/ios/ios_textfield.dart | 6 +- super_editor/lib/super_editor.dart | 1 + 9 files changed, 325 insertions(+), 23 deletions(-) create mode 100644 super_editor/example/lib/demos/in_the_lab/feature_ios_native_context_menu.dart rename super_editor/lib/src/{super_textfield => infrastructure/platforms}/ios/ios_system_context_menu.dart (100%) diff --git a/super_editor/example/lib/demos/in_the_lab/feature_ios_native_context_menu.dart b/super_editor/example/lib/demos/in_the_lab/feature_ios_native_context_menu.dart new file mode 100644 index 000000000..4d8b02112 --- /dev/null +++ b/super_editor/example/lib/demos/in_the_lab/feature_ios_native_context_menu.dart @@ -0,0 +1,186 @@ +import 'package:example/demos/in_the_lab/in_the_lab_scaffold.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:follow_the_leader/follow_the_leader.dart'; +import 'package:super_editor/super_editor.dart'; + +/// Super Editor demo that uses the native iOS context menu as the floating toolbar +/// for both Super Editor and Super Text Field. +/// +/// By default, Super Editor and Super Text Field display a floating toolbar that's +/// painted by Flutter. By using Flutter, you gain full control over appearance, and +/// the available options. However, recent versions of iOS have security settings +/// that bring up an annoying warning if you attempt to run a "paste" command without +/// using their native iOS toolbar. For that reason, Super Editor makes it possible +/// to show the native iOS toolbar. +class NativeIosContextMenuFeatureDemo extends StatefulWidget { + const NativeIosContextMenuFeatureDemo({super.key}); + + @override + State createState() => _NativeIosContextMenuFeatureDemoState(); +} + +class _NativeIosContextMenuFeatureDemoState extends State { + final _documentLayoutKey = GlobalKey(); + + late final MutableDocument _document; + late final MutableDocumentComposer _composer; + late final Editor _editor; + late final CommonEditorOperations _commonEditorOperations; + + late final SuperEditorIosControlsController _toolbarController; + + @override + void initState() { + super.initState(); + + _document = MutableDocument.empty(); + _composer = MutableDocumentComposer(); + _editor = Editor( + editables: { + Editor.documentKey: _document, + Editor.composerKey: _composer, + }, + requestHandlers: [ + ...defaultRequestHandlers, + ], + ); + _commonEditorOperations = CommonEditorOperations( + document: _document, + editor: _editor, + composer: _composer, + documentLayoutResolver: () => _documentLayoutKey.currentState as DocumentLayout, + ); + + _toolbarController = SuperEditorIosControlsController( + toolbarBuilder: _buildToolbar, + ); + } + + @override + void dispose() { + _toolbarController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return InTheLabScaffold( + content: _buildEditor(), + supplemental: _buildTextField(), + ); + } + + Widget _buildEditor() { + return SuperEditorIosControlsScope( + controller: _toolbarController, + child: IntrinsicHeight( + child: SuperEditor( + editor: _editor, + document: _document, + composer: _composer, + documentLayoutKey: _documentLayoutKey, + selectionStyle: SelectionStyles( + selectionColor: Colors.red.withOpacity(0.3), + ), + stylesheet: defaultStylesheet.copyWith( + addRulesAfter: [ + ...darkModeStyles, + ], + ), + documentOverlayBuilders: [ + if (defaultTargetPlatform == TargetPlatform.iOS) ...[ + // Adds a Leader around the document selection at a focal point for the + // iOS floating toolbar. + SuperEditorIosToolbarFocalPointDocumentLayerBuilder(), + // Displays caret and drag handles, specifically for iOS. + SuperEditorIosHandlesDocumentLayerBuilder( + handleColor: Colors.red, + ), + ], + + if (defaultTargetPlatform == TargetPlatform.android) ...[ + // Adds a Leader around the document selection at a focal point for the + // Android floating toolbar. + SuperEditorAndroidToolbarFocalPointDocumentLayerBuilder(), + // Displays caret and drag handles, specifically for Android. + SuperEditorAndroidHandlesDocumentLayerBuilder( + caretColor: Colors.red, + ), + ], + + // Displays caret for typical desktop use-cases. + DefaultCaretOverlayBuilder( + caretStyle: const CaretStyle().copyWith(color: Colors.redAccent), + ), + ], + ), + ), + ); + } + + Widget _buildToolbar( + BuildContext context, + Key mobileToolbarKey, + LeaderLink focalPoint, + ) { + return iOSSystemPopoverEditorToolbarWithFallbackBuilder( + context, + mobileToolbarKey, + focalPoint, + _commonEditorOperations, + SuperEditorIosControlsScope.rootOf(context), + ); + } + + Widget _buildTextField() { + return Padding( + padding: const EdgeInsets.all(24), + child: _SuperTextFieldWithNativeContextMenu(), + ); + } +} + +class _SuperTextFieldWithNativeContextMenu extends StatefulWidget { + const _SuperTextFieldWithNativeContextMenu({Key? key}) : super(key: key); + + @override + State<_SuperTextFieldWithNativeContextMenu> createState() => _SuperTextFieldWithNativeContextMenuState(); +} + +class _SuperTextFieldWithNativeContextMenuState extends State<_SuperTextFieldWithNativeContextMenu> { + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(4), + // color: const Color(0xFF222222), + ), + child: SuperIOSTextField( + padding: const EdgeInsets.all(12), + caretStyle: CaretStyle(color: Colors.red), + selectionColor: defaultSelectionColor, + handlesColor: Colors.red, + textStyleBuilder: (attributions) { + return defaultTextFieldStyleBuilder(attributions).copyWith( + color: Colors.white, + fontSize: 18, + ); + }, + hintBehavior: HintBehavior.displayHintUntilTextEntered, + hintBuilder: (_) { + return Text( + "Enter text and open toolbar", + style: TextStyle( + color: Colors.grey, + fontSize: 18, + ), + ); + }, + popoverToolbarBuilder: iOSSystemPopoverTextFieldToolbarWithFallback, + ), + ); + } +} diff --git a/super_editor/example/lib/demos/in_the_lab/feature_pattern_tags.dart b/super_editor/example/lib/demos/in_the_lab/feature_pattern_tags.dart index ab2651fbb..96354f042 100644 --- a/super_editor/example/lib/demos/in_the_lab/feature_pattern_tags.dart +++ b/super_editor/example/lib/demos/in_the_lab/feature_pattern_tags.dart @@ -99,15 +99,17 @@ class _HashTagsFeatureDemoState extends State { return const SizedBox(); } - return SingleChildScrollView( - child: Wrap( - spacing: 12, - runSpacing: 12, - alignment: WrapAlignment.center, - children: [ - for (final tag in _tags) // - Chip(label: Text(tag.tag.raw)), - ], + return IntrinsicHeight( + child: SingleChildScrollView( + child: Wrap( + spacing: 12, + runSpacing: 12, + alignment: WrapAlignment.center, + children: [ + for (final tag in _tags) // + Chip(label: Text(tag.tag.raw)), + ], + ), ), ); } diff --git a/super_editor/example/lib/demos/in_the_lab/in_the_lab_scaffold.dart b/super_editor/example/lib/demos/in_the_lab/in_the_lab_scaffold.dart index 2a63470ce..b78395df5 100644 --- a/super_editor/example/lib/demos/in_the_lab/in_the_lab_scaffold.dart +++ b/super_editor/example/lib/demos/in_the_lab/in_the_lab_scaffold.dart @@ -30,15 +30,7 @@ class InTheLabScaffold extends StatelessWidget { body: Stack( children: [ Positioned.fill( - child: Row( - children: [ - Expanded( - child: content, - ), - if (supplemental != null) // - _buildSupplementalPanel(), - ], - ), + child: _buildContent(), ), if (overlay != null) // Positioned.fill( @@ -52,7 +44,31 @@ class InTheLabScaffold extends StatelessWidget { ); } - Widget _buildSupplementalPanel() { + Widget _buildContent() { + return LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth / constraints.maxHeight >= 1) { + return _buildContentForDesktop(); + } else { + return _buildContentForMobile(); + } + }, + ); + } + + Widget _buildContentForDesktop() { + return Row( + children: [ + Expanded( + child: content, + ), + if (supplemental != null) // + _buildSupplementalSidePanel(), + ], + ); + } + + Widget _buildSupplementalSidePanel() { return Container( width: 250, height: double.infinity, @@ -82,6 +98,58 @@ class InTheLabScaffold extends StatelessWidget { ), ); } + + Widget _buildContentForMobile() { + return SafeArea( + left: false, + right: false, + bottom: false, + child: Padding( + // Push the content down below the nav drawer menu button. + padding: const EdgeInsets.only(top: 24), + child: Column( + children: [ + Expanded( + child: content, + ), + if (supplemental != null) // + _buildSupplementalBottomPanel(), + ], + ), + ), + ); + } + + Widget _buildSupplementalBottomPanel() { + return Container( + width: double.infinity, + height: 200, + decoration: BoxDecoration( + border: Border(top: BorderSide(color: Colors.white.withOpacity(0.1))), + ), + child: Stack( + children: [ + Center( + child: Icon( + Icons.biotech, + color: Colors.white.withOpacity(0.05), + size: 84, + ), + ), + Positioned.fill( + child: Center( + child: SizedBox( + width: double.infinity, + child: SingleChildScrollView( + child: supplemental!, + ), + ), + ), + ), + ], + ), + ); + } } // Makes text light, for use during dark mode styling. diff --git a/super_editor/example/lib/demos/supertextfield/ios/demo_superiostextfield.dart b/super_editor/example/lib/demos/supertextfield/ios/demo_superiostextfield.dart index f467c0f88..0ca007697 100644 --- a/super_editor/example/lib/demos/supertextfield/ios/demo_superiostextfield.dart +++ b/super_editor/example/lib/demos/supertextfield/ios/demo_superiostextfield.dart @@ -76,7 +76,7 @@ class _SuperIOSTextFieldDemoState extends State { maxLines: config.maxLines, lineHeight: lineHeight, textInputAction: TextInputAction.done, - popoverToolbarBuilder: iOSSystemPopoverToolbarWithBackupFlutterVersion, + popoverToolbarBuilder: iOSSystemPopoverTextFieldToolbarWithFallback, showDebugPaint: config.showDebugPaint, ), ); diff --git a/super_editor/example/lib/main.dart b/super_editor/example/lib/main.dart index 36a72e90f..bcc1070e3 100644 --- a/super_editor/example/lib/main.dart +++ b/super_editor/example/lib/main.dart @@ -15,6 +15,7 @@ import 'package:example/demos/flutter_features/demo_inline_widgets.dart'; import 'package:example/demos/flutter_features/textinputclient/basic_text_input_client.dart'; import 'package:example/demos/flutter_features/textinputclient/textfield.dart'; import 'package:example/demos/in_the_lab/feature_action_tags.dart'; +import 'package:example/demos/in_the_lab/feature_ios_native_context_menu.dart'; import 'package:example/demos/in_the_lab/feature_pattern_tags.dart'; import 'package:example/demos/in_the_lab/feature_stable_tags.dart'; import 'package:example/demos/in_the_lab/selected_text_colors_demo.dart'; @@ -308,6 +309,13 @@ final _menu = <_MenuGroup>[ return const ActionTagsFeatureDemo(); }, ), + _MenuItem( + icon: Icons.apple, + title: 'Native iOS Toolbar', + pageBuilder: (context) { + return const NativeIosContextMenuFeatureDemo(); + }, + ), ], ), _MenuGroup( diff --git a/super_editor/lib/src/default_editor/super_editor.dart b/super_editor/lib/src/default_editor/super_editor.dart index 0a7955aa6..cb56d0619 100644 --- a/super_editor/lib/src/default_editor/super_editor.dart +++ b/super_editor/lib/src/default_editor/super_editor.dart @@ -36,6 +36,7 @@ import 'package:super_editor/src/undo_redo.dart'; import 'package:super_text_layout/super_text_layout.dart'; import '../infrastructure/document_gestures_interaction_overrides.dart'; +import '../infrastructure/platforms/ios/ios_system_context_menu.dart'; import '../infrastructure/platforms/mobile_documents.dart'; import 'attributions.dart'; import 'blockquote.dart'; @@ -828,6 +829,42 @@ class SuperEditorState extends State { } } +/// A [DocumentFloatingToolbarBuilder] that displays the iOS system popover toolbar, if the version of +/// iOS is recent enough, otherwise builds [defaultIosEditorToolbarBuilder]. +Widget iOSSystemPopoverEditorToolbarWithFallbackBuilder( + BuildContext context, + Key floatingToolbarKey, + LeaderLink focalPoint, + CommonEditorOperations editorOps, + SuperEditorIosControlsController editorControlsController, +) { + if (CurrentPlatform.isWeb) { + // On web, we defer to the browser's internal overlay controls for mobile. + return const SizedBox(); + } + + if (focalPoint.offset == null || focalPoint.leaderSize == null) { + // It's unclear when/why this might happen. But there seem to be some + // cases, such as placing a caret in an empty document and tapping again + // to show the toolbar. + return const SizedBox(); + } + + if (IOSSystemContextMenu.isSupported(context)) { + return IOSSystemContextMenu( + anchor: focalPoint.offset! & focalPoint.leaderSize!, + ); + } + + return defaultIosEditorToolbarBuilder( + context, + floatingToolbarKey, + focalPoint, + editorOps, + editorControlsController, + ); +} + /// Builds a standard editor-style iOS floating toolbar. Widget defaultIosEditorToolbarBuilder( BuildContext context, diff --git a/super_editor/lib/src/super_textfield/ios/ios_system_context_menu.dart b/super_editor/lib/src/infrastructure/platforms/ios/ios_system_context_menu.dart similarity index 100% rename from super_editor/lib/src/super_textfield/ios/ios_system_context_menu.dart rename to super_editor/lib/src/infrastructure/platforms/ios/ios_system_context_menu.dart diff --git a/super_editor/lib/src/super_textfield/ios/ios_textfield.dart b/super_editor/lib/src/super_textfield/ios/ios_textfield.dart index 55f8fa95d..e32cfa578 100644 --- a/super_editor/lib/src/super_textfield/ios/ios_textfield.dart +++ b/super_editor/lib/src/super_textfield/ios/ios_textfield.dart @@ -21,13 +21,13 @@ import 'package:super_text_layout/super_text_layout.dart'; import '../metrics.dart'; import '../styles.dart'; import 'floating_cursor.dart'; -import 'ios_system_context_menu.dart'; +import '../../infrastructure/platforms/ios/ios_system_context_menu.dart'; import 'user_interaction.dart'; export '../infrastructure/magnifier.dart'; export 'caret.dart'; export 'editing_controls.dart'; -export 'ios_system_context_menu.dart'; +export '../../infrastructure/platforms/ios/ios_system_context_menu.dart'; export 'user_interaction.dart'; final _log = iosTextFieldLog; @@ -702,7 +702,7 @@ typedef IOSPopoverToolbarBuilder = Widget Function(BuildContext, IOSEditingOverl /// An [IOSPopoverToolbarBuilder] that displays the iOS system popover toolbar, if the version of /// iOS is recent enough, otherwise builds [defaultIosPopoverToolbarBuilder]. -Widget iOSSystemPopoverToolbarWithBackupFlutterVersion(BuildContext context, IOSEditingOverlayController controller) { +Widget iOSSystemPopoverTextFieldToolbarWithFallback(BuildContext context, IOSEditingOverlayController controller) { if (IOSSystemContextMenu.isSupported(context)) { return IOSSystemContextMenu( anchor: controller.toolbarFocalPoint.offset! & controller.toolbarFocalPoint.leaderSize!, diff --git a/super_editor/lib/super_editor.dart b/super_editor/lib/super_editor.dart index f62b58865..3bbc9f21e 100644 --- a/super_editor/lib/super_editor.dart +++ b/super_editor/lib/super_editor.dart @@ -75,6 +75,7 @@ export 'src/infrastructure/flutter/text_selection.dart'; export 'src/infrastructure/platforms/android/android_document_controls.dart'; export 'src/infrastructure/platforms/android/toolbar.dart'; export 'src/infrastructure/platforms/ios/ios_document_controls.dart'; +export 'src/infrastructure/platforms/ios/ios_system_context_menu.dart'; export 'src/infrastructure/platforms/ios/floating_cursor.dart'; export 'src/infrastructure/platforms/ios/toolbar.dart'; export 'src/infrastructure/platforms/ios/magnifier.dart'; From a4b341a7e18cc6d31f14c8008b95b2cfb661c78c Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Wed, 25 Sep 2024 19:57:03 -0700 Subject: [PATCH 5/6] Reworked pattern tags demo to layout on mobile --- .../in_the_lab/feature_pattern_tags.dart | 60 +++++++++---------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/super_editor/example/lib/demos/in_the_lab/feature_pattern_tags.dart b/super_editor/example/lib/demos/in_the_lab/feature_pattern_tags.dart index 00f416714..fc39e28be 100644 --- a/super_editor/example/lib/demos/in_the_lab/feature_pattern_tags.dart +++ b/super_editor/example/lib/demos/in_the_lab/feature_pattern_tags.dart @@ -68,27 +68,27 @@ class _HashTagsFeatureDemoState extends State { inlineTextStyler: (attributions, existingStyle) { TextStyle style = defaultInlineTextStyler(attributions, existingStyle); - if (attributions.whereType().isNotEmpty) { - style = style.copyWith( - color: Colors.orange, - ); - } - - return style; - }, - addRulesAfter: [ - ...darkModeStyles, - ], - ), - documentOverlayBuilders: [ - DefaultCaretOverlayBuilder( - caretStyle: CaretStyle().copyWith(color: Colors.redAccent), + if (attributions.whereType().isNotEmpty) { + style = style.copyWith( + color: Colors.orange, + ); + } + + return style; + }, + addRulesAfter: [ + ...darkModeStyles, + ], ), - ], - plugins: { - _hashTagPlugin, - }, - ); + documentOverlayBuilders: [ + DefaultCaretOverlayBuilder( + caretStyle: CaretStyle().copyWith(color: Colors.redAccent), + ), + ], + plugins: { + _hashTagPlugin, + }, + ); } Widget _buildTagList() { @@ -96,17 +96,15 @@ class _HashTagsFeatureDemoState extends State { return const SizedBox(); } - return IntrinsicHeight( - child: SingleChildScrollView( - child: Wrap( - spacing: 12, - runSpacing: 12, - alignment: WrapAlignment.center, - children: [ - for (final tag in _tags) // - Chip(label: Text(tag.tag.raw)), - ], - ), + return SingleChildScrollView( + child: Wrap( + spacing: 12, + runSpacing: 12, + alignment: WrapAlignment.center, + children: [ + for (final tag in _tags) // + Chip(label: Text(tag.tag.raw)), + ], ), ); } From 8bf3656bb9c5e5654ffe7787bfa8dc59546b1bd0 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Sun, 29 Sep 2024 10:12:03 -0700 Subject: [PATCH 6/6] Removed commented code, removed a couple deprecated properties. --- .../lib/demos/in_the_lab/feature_ios_native_context_menu.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/super_editor/example/lib/demos/in_the_lab/feature_ios_native_context_menu.dart b/super_editor/example/lib/demos/in_the_lab/feature_ios_native_context_menu.dart index 4d8b02112..d91b07a5b 100644 --- a/super_editor/example/lib/demos/in_the_lab/feature_ios_native_context_menu.dart +++ b/super_editor/example/lib/demos/in_the_lab/feature_ios_native_context_menu.dart @@ -78,8 +78,6 @@ class _NativeIosContextMenuFeatureDemoState extends State