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

[SuperTextField] - Right-click and other gesture overrides (Resolves #278) #692

Open
wants to merge 3 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
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.
class TextFieldWithContextMenuDemo extends StatefulWidget {
@override
_TextFieldWithContextMenuDemoState createState() => _TextFieldWithContextMenuDemoState();
}

class _TextFieldWithContextMenuDemoState extends State<TextFieldWithContextMenuDemo> {
GlobalKey? _textFieldKey;
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;
}

void _onTapDown(TapDownDetails details) {
if (_isCtrlPressed && defaultTargetPlatform == TargetPlatform.macOS) {
_contextMenuOverlay.show(context, details.globalPosition);
}
}

void _onRightTapDown(TapDownDetails details) {
if (defaultTargetPlatform == TargetPlatform.macOS) {
_contextMenuOverlay.show(context, details.globalPosition);

// This shows you how you would calculate the text position for
// a gesture override.
final textFieldBox = _textFieldKey!.currentContext!.findRenderObject() as RenderBox;
final textLayout = (_textFieldKey!.currentState as ProseTextBlock).textLayout;
final textPosition = textLayout.getPositionNearestToOffset(
textFieldBox.globalToLocal(details.globalPosition),
);
print("You right-clicked near text position: $textPosition");
}
}

void _onRightTapUp(TapUpDetails details) {
if (defaultTargetPlatform == TargetPlatform.windows) {
_contextMenuOverlay.show(context, details.globalPosition);
}
}

@override
Widget build(BuildContext context) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildTextField(),
const SizedBox(height: 24),
_buildDescription(),
],
),
),
);
}

Widget _buildTextField() {
return DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
),
child: SuperTextField(
hintBuilder: (context) {
return Text(
"enter text here...",
style: const TextStyle(color: Colors.grey),
);
},
textStyleBuilder: (_) {
return const TextStyle(
color: Colors.black,
fontSize: 18,
);
},
gestureOverrideBuilder: _gestureOverrideBuilder,
),
);
}

Widget _gestureOverrideBuilder(BuildContext context, textFieldGlobalKey, [Widget? child]) {
_textFieldKey = textFieldGlobalKey;
return GestureDetector(
onTapDown: _onTapDown,
onSecondaryTapDown: _onRightTapDown,
onSecondaryTapUp: _onRightTapUp,
behavior: HitTestBehavior.translucent,
child: child,
);
}

Widget _buildDescription() {
return const Text(
"This SuperTextField includes gesture overrides:\n"
" • Right tap down on Mac to open a context menu\n"
" • Right tap up on Windows to open a context menu\n"
" • Control + Left tap down on Mac to open a context menu",
style: TextStyle(
color: Colors.grey,
fontSize: 14,
height: 1.8,
),
);
}
}

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;
}
}
8 changes: 8 additions & 0 deletions super_editor/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ 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/super_textfield.dart';
import 'package:super_editor/src/infrastructure/super_textfield/infrastructure/gesture_overrides.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';
Expand Down Expand Up @@ -46,6 +48,7 @@ class AndroidTextFieldTouchInteractor extends StatefulWidget {
required this.textKey,
required this.isMultiline,
required this.handleColor,
this.gestureOverrideBuilder,
this.showDebugPaint = false,
required this.child,
}) : super(key: key);
Expand Down Expand Up @@ -84,6 +87,9 @@ 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.
final bool showDebugPaint;

Expand Down Expand Up @@ -437,6 +443,7 @@ class AndroidTextFieldTouchInteractorState extends State<AndroidTextFieldTouchIn
},
),
},
child: widget.gestureOverrideBuilder?.call(context, widget.textKey),
),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:super_editor/src/infrastructure/_listenable_builder.dart';
import 'package:super_editor/src/infrastructure/attributed_text_styles.dart';
import 'package:super_editor/src/infrastructure/super_textfield/android/_editing_controls.dart';
import 'package:super_editor/src/infrastructure/super_textfield/android/_user_interaction.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/infrastructure/text_scrollview.dart';
import 'package:super_editor/src/infrastructure/super_textfield/input_method_engine/_ime_text_editing_controller.dart';
Expand Down Expand Up @@ -35,6 +36,7 @@ class SuperAndroidTextField extends StatefulWidget {
required this.selectionColor,
required this.handlesColor,
this.textInputAction = TextInputAction.done,
this.gestureOverrideBuilder,
this.popoverToolbarBuilder = _defaultAndroidToolbarBuilder,
this.showDebugPaint = false,
}) : super(key: key);
Expand Down Expand Up @@ -112,6 +114,9 @@ class SuperAndroidTextField extends StatefulWidget {
/// keyboard.
final TextInputAction textInputAction;

/// {@macros SuperTextField_gestureOverrideBuilder}
final GestureOverrideBuilder? gestureOverrideBuilder;

/// Whether to paint debug guides.
final bool showDebugPaint;

Expand Down
Loading