Skip to content

Commit

Permalink
[SuperEditor][SuperTextField][iOS] - Add popover toolbar builder that…
Browse files Browse the repository at this point in the history
… uses Flutter's new support for an iOS 16+ system toolbar (Resolves #2032) (#2058)
  • Loading branch information
matthew-carroll committed Sep 29, 2024
1 parent 5998c75 commit 5e0e046
Show file tree
Hide file tree
Showing 15 changed files with 512 additions and 39 deletions.
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
@@ -0,0 +1,183 @@
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<NativeIosContextMenuFeatureDemo> createState() => _NativeIosContextMenuFeatureDemoState();
}

class _NativeIosContextMenuFeatureDemoState extends State<NativeIosContextMenuFeatureDemo> {
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,
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),
),
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,
),
);
}
}
40 changes: 20 additions & 20 deletions super_editor/example/lib/demos/in_the_lab/feature_pattern_tags.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,27 +68,27 @@ class _HashTagsFeatureDemoState extends State<HashTagsFeatureDemo> {
inlineTextStyler: (attributions, existingStyle) {
TextStyle style = defaultInlineTextStyler(attributions, existingStyle);

if (attributions.whereType<PatternTagAttribution>().isNotEmpty) {
style = style.copyWith(
color: Colors.orange,
);
}

return style;
},
addRulesAfter: [
...darkModeStyles,
],
),
documentOverlayBuilders: [
DefaultCaretOverlayBuilder(
caretStyle: CaretStyle().copyWith(color: Colors.redAccent),
if (attributions.whereType<PatternTagAttribution>().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() {
Expand Down
88 changes: 78 additions & 10 deletions super_editor/example/lib/demos/in_the_lab/in_the_lab_scaffold.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
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: iOSSystemPopoverTextFieldToolbarWithFallback,
showDebugPaint: config.showDebugPaint,
),
);
Expand Down
Loading

0 comments on commit 5e0e046

Please sign in to comment.