Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add popover toolbar builder that uses Flutter's new support for an iOS 16+ system toolbar (Resolves #2032) #2058

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions super_editor/.run/Flutter - Text Field.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Flutter - Text Field" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="filePath" value="$PROJECT_DIR$/example/lib/flutter_demos/main_flutter_textfield.dart" />
<method v="2" />
</configuration>
</component>
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class _SuperIOSTextFieldDemoState extends State<SuperIOSTextFieldDemo> {
maxLines: config.maxLines,
lineHeight: lineHeight,
textInputAction: TextInputAction.done,
popoverToolbarBuilder: iOSSystemPopoverToolbarWithBackupFlutterVersion,
showDebugPaint: config.showDebugPaint,
),
);
Expand Down
63 changes: 63 additions & 0 deletions super_editor/example/lib/flutter_demos/main_flutter_textfield.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
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) {
// 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,
);
});
}
}
Original file line number Diff line number Diff line change
@@ -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<IOSSystemContextMenu> createState() => _IOSSystemContextMenuState();
}

class _IOSSystemContextMenuState extends State<IOSSystemContextMenu> {
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();
}
}
35 changes: 27 additions & 8 deletions super_editor/lib/src/super_textfield/ios/ios_textfield.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -694,7 +697,23 @@ class SuperIOSTextFieldState extends State<SuperIOSTextField>
}
}

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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is WithBackupFlutterVersion an appropriate name? I think iOSSystemPopoverToolbarWithFallback would be simpler.

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: () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import 'package:super_editor/src/super_textfield/super_textfield.dart';
import 'package:super_editor/src/test/test_globals.dart';
import 'package:super_text_layout/super_text_layout.dart';

import '_editing_controls.dart';
import 'editing_controls.dart';

final _log = iosTextFieldLog;

Expand Down
Loading