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

Cherry Pick: [SuperEditor] - Make it possible to open a keyboard panel before the keyboard opens #2477

Merged
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
123 changes: 100 additions & 23 deletions super_editor/example/lib/demos/mobile_chat/demo_mobile_chat.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'package:example/demos/mobile_chat/giphy_keyboard_panel.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:super_editor/super_editor.dart';
import 'package:super_keyboard/super_keyboard.dart';

/// A UI with a chat message editor at the bottom, and a fake chat conversation
/// behind it.
Expand Down Expand Up @@ -33,17 +35,18 @@ class _MobileChatDemoState extends State<MobileChatDemo> {
final FocusNode _editorFocusNode = FocusNode();
late final Editor _editor;

late final KeyboardPanelController _keyboardPanelController;
late final KeyboardPanelController<_Panel> _keyboardPanelController;
final SoftwareKeyboardController _softwareKeyboardController = SoftwareKeyboardController();

final _imeConnectionNotifier = ValueNotifier<bool>(false);

_Panel? _visiblePanel;

@override
void initState() {
super.initState();

SuperKeyboard.initLogs();
initLoggers(Level.ALL, {keyboardPanelLog});

final document = MutableDocument.empty();
final composer = MutableDocumentComposer();
_editor = createDefaultDocumentEditor(document: document, composer: composer);
Expand All @@ -64,16 +67,36 @@ class _MobileChatDemoState extends State<MobileChatDemo> {
super.dispose();
}

void _openPanelFromAppBar() {
// This action is here to verify that we can open keyboard panels
// before opening the keyboard.

// Focus the editor and place the caret.
_editorFocusNode.requestFocus();
final document = _editor.context.document;
_editor.execute([
ChangeSelectionRequest(
DocumentSelection.collapsed(
position: DocumentPosition(
nodeId: document.last.id,
nodePosition: document.last.endPosition,
),
),
SelectionChangeType.placeCaret,
SelectionReason.userInteraction,
),
]);

// Open a panel.
_keyboardPanelController.showKeyboardPanel(_Panel.panel1);
}

void _togglePanel(_Panel panel) {
setState(() {
if (_visiblePanel == panel) {
_visiblePanel = null;
_keyboardPanelController.showSoftwareKeyboard();
} else {
_visiblePanel = panel;
_keyboardPanelController.showKeyboardPanel();
}
});
if (_keyboardPanelController.openPanel == panel) {
_keyboardPanelController.showSoftwareKeyboard();
} else {
_keyboardPanelController.showKeyboardPanel(panel);
}
}

@override
Expand All @@ -95,6 +118,18 @@ class _MobileChatDemoState extends State<MobileChatDemo> {

PreferredSizeWidget _buildAppBar() {
return AppBar(
actions: [
IconButton(
icon: Icon(Icons.open_in_new),
onPressed: _openPanelFromAppBar,
),
IconButton(
icon: Icon(Icons.settings),
onPressed: () {
Navigator.of(context).pushNamed("/second");
},
),
],
bottom: const TabBar(
tabs: [
Tab(icon: Icon(Icons.chat)),
Expand All @@ -111,6 +146,7 @@ class _MobileChatDemoState extends State<MobileChatDemo> {
child: GestureDetector(
onTap: () {
_screenFocusNode.requestFocus();
_keyboardPanelController.closeKeyboardAndPanel();
},
child: Focus(
focusNode: _screenFocusNode,
Expand Down Expand Up @@ -158,15 +194,15 @@ class _MobileChatDemoState extends State<MobileChatDemo> {

Widget _buildCommentEditor() {
return Opacity(
// Opacity is here so we can easily check what's behind it.
opacity: 1.0,
// ^ opacity is for testing, so we can see the chat behind it.
child: KeyboardPanelScaffold(
child: KeyboardPanelScaffold<_Panel>(
controller: _keyboardPanelController,
isImeConnected: _imeConnectionNotifier,
toolbarBuilder: _buildKeyboardToolbar,
fallbackPanelHeight: MediaQuery.sizeOf(context).height / 3,
keyboardPanelBuilder: (context) {
switch (_visiblePanel) {
keyboardPanelBuilder: (context, panel) {
switch (panel) {
case _Panel.panel1:
return Container(
color: Colors.blue,
Expand Down Expand Up @@ -224,10 +260,20 @@ class _MobileChatDemoState extends State<MobileChatDemo> {
shrinkWrap: true,
stylesheet: _chatStylesheet,
selectionPolicies: const SuperEditorSelectionPolicies(
openKeyboardWhenTappingExistingSelection: false,
clearSelectionWhenEditorLosesFocus: true,
clearSelectionWhenImeConnectionCloses: false,
),
imePolicies: SuperEditorImePolicies(
openKeyboardOnGainPrimaryFocus: false,
openKeyboardOnSelectionChange: false,
closeKeyboardOnSelectionLost: false,
),
isImeConnected: _imeConnectionNotifier,
contentTapDelegateFactories: [
superEditorLaunchLinkTapHandlerFactory,
_tapToFocusEditor,
],
),
),
],
Expand All @@ -239,11 +285,14 @@ class _MobileChatDemoState extends State<MobileChatDemo> {
);
}

Widget _buildKeyboardToolbar(BuildContext context, bool isKeyboardPanelVisible) {
if (!isKeyboardPanelVisible) {
_visiblePanel = null;
}
ContentTapDelegate _tapToFocusEditor(SuperEditorContext editContext) {
return _TapToFocusEditor(
_editorFocusNode,
_keyboardPanelController,
);
}

Widget _buildKeyboardToolbar(BuildContext context, _Panel? openPanel) {
return Row(
children: [
Expanded(
Expand All @@ -258,13 +307,13 @@ class _MobileChatDemoState extends State<MobileChatDemo> {
const Spacer(),
_PanelButton(
icon: Icons.text_fields,
isActive: _visiblePanel == _Panel.panel1,
isActive: _keyboardPanelController.openPanel == _Panel.panel1,
onPressed: () => _togglePanel(_Panel.panel1),
),
const SizedBox(width: 16),
_PanelButton(
icon: Icons.align_horizontal_left,
isActive: _visiblePanel == _Panel.panel2,
isActive: _keyboardPanelController.openPanel == _Panel.panel2,
onPressed: () => _togglePanel(_Panel.panel2),
),
const SizedBox(width: 16),
Expand All @@ -279,7 +328,13 @@ class _MobileChatDemoState extends State<MobileChatDemo> {
),
const Spacer(),
GestureDetector(
onTap: _keyboardPanelController.closeKeyboardAndPanel,
onTap: () {
_keyboardPanelController.closeKeyboardAndPanel();

// We need to explicitly unfocus so that the caret doesn't
// keep blinking in the editor.
_editorFocusNode.unfocus();
},
child: Icon(Icons.keyboard_hide),
),
const SizedBox(width: 24),
Expand All @@ -301,6 +356,28 @@ class _MobileChatDemoState extends State<MobileChatDemo> {
}
}

class _TapToFocusEditor extends ContentTapDelegate {
_TapToFocusEditor(
this.editorFocusNode,
this.keyboardPanelController,
);

final FocusNode editorFocusNode;
final KeyboardPanelController keyboardPanelController;

@override
TapHandlingInstruction onTap(DocumentTapDetails details) {
if (!keyboardPanelController.isSoftwareKeyboardOpen && !keyboardPanelController.isKeyboardPanelOpen) {
// The user tapped on the editor and the software keyboard isn't up, nor is a panel.
// Open the software keyboard.
editorFocusNode.requestFocus();
keyboardPanelController.showSoftwareKeyboard();
}

return TapHandlingInstruction.continueHandling;
}
}

enum _Panel {
panel1,
panel2,
Expand Down
10 changes: 0 additions & 10 deletions super_editor/example/lib/main_super_editor_chat.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,6 @@ void main() {
MaterialApp(
routes: {
"/": (context) => Scaffold(
appBar: AppBar(
actions: [
IconButton(
onPressed: () {
Navigator.of(context).pushNamed("/second");
},
icon: Icon(Icons.settings),
),
],
),
resizeToAvoidBottomInset: false,
body: MobileChatDemo(),
),
Expand Down
1 change: 1 addition & 0 deletions super_editor/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ dependencies:
git:
url: https://github.com/superlistapp/super_editor.git
path: super_text_layout
super_keyboard: ^0.1.0
follow_the_leader: ^0.0.4+7
overlord: ^0.0.3+5

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ class AndroidDocumentTouchInteractor extends StatefulWidget {
required this.document,
required this.getDocumentLayout,
required this.selection,
this.openKeyboardWhenTappingExistingSelection = true,
required this.openSoftwareKeyboard,
required this.scrollController,
required this.fillViewport,
Expand All @@ -419,6 +420,9 @@ class AndroidDocumentTouchInteractor extends StatefulWidget {
final DocumentLayout Function() getDocumentLayout;
final ValueListenable<DocumentSelection?> selection;

/// {@macro openKeyboardWhenTappingExistingSelection}
final bool openKeyboardWhenTappingExistingSelection;

/// A callback that should open the software keyboard when invoked.
final VoidCallback openSoftwareKeyboard;

Expand Down Expand Up @@ -780,7 +784,7 @@ class _AndroidDocumentTouchInteractorState extends State<AndroidDocumentTouchInt

_showAndHideEditingControlsAfterTapSelection(didTapOnExistingSelection: didTapOnExistingSelection);

if (didTapOnExistingSelection) {
if (didTapOnExistingSelection && widget.openKeyboardWhenTappingExistingSelection) {
// The user tapped on the existing selection. Show the software keyboard.
//
// If the user didn't tap on an existing selection, the software keyboard will
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ class IosDocumentTouchInteractor extends StatefulWidget {
required this.document,
required this.getDocumentLayout,
required this.selection,
this.openKeyboardWhenTappingExistingSelection = true,
required this.openSoftwareKeyboard,
required this.scrollController,
required this.dragHandleAutoScroller,
Expand All @@ -260,6 +261,9 @@ class IosDocumentTouchInteractor extends StatefulWidget {
final DocumentLayout Function() getDocumentLayout;
final ValueListenable<DocumentSelection?> selection;

/// {@macro openKeyboardWhenTappingExistingSelection}
final bool openKeyboardWhenTappingExistingSelection;

/// A callback that should open the software keyboard when invoked.
final VoidCallback openSoftwareKeyboard;

Expand Down Expand Up @@ -607,7 +611,11 @@ class _IosDocumentTouchInteractorState extends State<IosDocumentTouchInteractor>
// The user tapped on an expanded selection. Toggle the toolbar and show
// the software keyboard.
_controlsController!.toggleToolbar();
widget.openSoftwareKeyboard();

if (widget.openKeyboardWhenTappingExistingSelection) {
widget.openSoftwareKeyboard();
}

return;
}

Expand Down Expand Up @@ -659,7 +667,7 @@ class _IosDocumentTouchInteractorState extends State<IosDocumentTouchInteractor>
_selectPosition(adjustedSelectionPosition);
}

if (didTapOnExistingSelection) {
if (didTapOnExistingSelection && widget.openKeyboardWhenTappingExistingSelection) {
// The user tapped on the existing selection. Show the software keyboard.
//
// If the user didn't tap on an existing selection, the software keyboard will
Expand Down
27 changes: 27 additions & 0 deletions super_editor/lib/src/default_editor/super_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,7 @@ class SuperEditorState extends State<SuperEditor> {
document: editContext.document,
getDocumentLayout: () => editContext.documentLayout,
selection: editContext.composer.selectionNotifier,
openKeyboardWhenTappingExistingSelection: widget.selectionPolicies.openKeyboardWhenTappingExistingSelection,
openSoftwareKeyboard: _openSoftareKeyboard,
contentTapHandlers: _contentTapHandlers,
scrollController: _scrollController,
Expand All @@ -897,6 +898,7 @@ class SuperEditorState extends State<SuperEditor> {
document: editContext.document,
getDocumentLayout: () => editContext.documentLayout,
selection: editContext.composer.selectionNotifier,
openKeyboardWhenTappingExistingSelection: widget.selectionPolicies.openKeyboardWhenTappingExistingSelection,
openSoftwareKeyboard: _openSoftareKeyboard,
contentTapHandlers: _contentTapHandlers,
scrollController: _scrollController,
Expand Down Expand Up @@ -1154,6 +1156,7 @@ class SuperEditorSelectionPolicies {
const SuperEditorSelectionPolicies({
this.placeCaretAtEndOfDocumentOnGainFocus = true,
this.restorePreviousSelectionOnGainFocus = true,
this.openKeyboardWhenTappingExistingSelection = true,
this.clearSelectionWhenEditorLosesFocus = true,
this.clearSelectionWhenImeConnectionCloses = true,
});
Expand All @@ -1168,6 +1171,30 @@ class SuperEditorSelectionPolicies {
/// focus, after having previous lost focus.
final bool restorePreviousSelectionOnGainFocus;

/// {@template openKeyboardWhenTappingExistingSelection}
/// Whether the software keyboard should be opened when the user taps on the existing
/// selection.
///
/// Defaults to `true`.
///
/// Typically, when an editor has a selection, the software keyboard is already open.
/// However, in some cases, the user might want to temporarily close the keyboard. For
/// example, the user might replace the keyboard with a custom emoji picker panel.
///
/// When the user is done with the temporary keyboard replacement, the user then wants to
/// open the keyboard again, so the user taps on the caret. If this property is `true`
/// then tapping on the caret will open the keyboard again.
///
/// In other, similar cases, the user might want to be able to tap on the editor without
/// opening the keyboard. For example, the user might open a keyboard panel that can insert
/// various types of content. In that case, the user might want to move the caret to then
/// insert something from the panel. In this case, it's easy to accidentally tap on the
/// existing caret, which would then close the panel and open the keyboard. To avoid this
/// annoyance, this property can be set to `false`, in which case tapping on the caret won't
/// automatically open the keyboard. It's left to the app to re-open the keyboard when desired.
/// {@endtemplate}
final bool openKeyboardWhenTappingExistingSelection;

/// Whether the editor's selection should be removed when the editor loses
/// all focus (not just primary focus).
///
Expand Down
2 changes: 2 additions & 0 deletions super_editor/lib/src/infrastructure/_logging.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class LogNames {
static const iosTextField = 'textfield.ios';

static const infrastructure = 'infrastructure';
static const keyboardPanel = 'infrastructure.keyboardPanel';
static const longPressSelection = 'infrastructure.gestures.longPress';
static const scheduler = 'infrastructure.scheduler';
static const contentLayers = 'infrastructure.content_layers';
Expand Down Expand Up @@ -88,6 +89,7 @@ final iosTextFieldLog = logging.Logger(LogNames.iosTextField);

final docGesturesLog = logging.Logger(LogNames.documentGestures);
final infrastructureLog = logging.Logger(LogNames.infrastructure);
final keyboardPanelLog = logging.Logger(LogNames.keyboardPanel);
final longPressSelectionLog = logging.Logger(LogNames.longPressSelection);
final schedulerLog = logging.Logger(LogNames.scheduler);
final contentLayersLog = logging.Logger(LogNames.contentLayers);
Expand Down
Loading
Loading