mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-12-16 09:47:53 +00:00
fix(flutter_desktop): document search (#6669)
* fix: double dispose on find menu * fix: empty query not resetting search service * fix: input focus getting lost after clicking button or pressing enter * chore: remove unused focus node and text controller * chore: bump appflowy editor * chore: code cleanup * chore: fix focus getting lost on submission * fix: next match focuses on title after jumping * chore: bump appflowy editor * revert: unnecessary changes to FlowyFormTextInput * fix: title requesting focus unexpectedly * Update frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_title.dart Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> * chore: merge conflicts * chore: code cleanup * test: add integration test * fix: show replace menu icon color in dark mode --------- Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
This commit is contained in:
parent
1952ef0853
commit
941b7cf04c
@ -0,0 +1,144 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
String generateRandomString(int len) {
|
||||
final r = Random();
|
||||
return String.fromCharCodes(
|
||||
List.generate(len, (index) => r.nextInt(33) + 89),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets(
|
||||
'document find menu test',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
|
||||
// tap editor to get focus
|
||||
await tester.tapButton(find.byType(AppFlowyEditor));
|
||||
|
||||
// set clipboard data
|
||||
final data = [
|
||||
"123456\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n"),
|
||||
"1234567\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n"),
|
||||
"12345678\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n"),
|
||||
].join();
|
||||
await getIt<ClipboardService>().setData(
|
||||
ClipboardServiceData(
|
||||
plainText: data,
|
||||
),
|
||||
);
|
||||
|
||||
// paste
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// go back to beginning of document
|
||||
// FIXME: Cannot run Ctrl+F unless selection is on screen
|
||||
await tester.editor
|
||||
.updateSelection(Selection.collapsed(Position(path: [0])));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(FindAndReplaceMenuWidget), findsNothing);
|
||||
|
||||
// press cmd/ctrl+F to display the find menu
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyF,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(FindAndReplaceMenuWidget), findsOneWidget);
|
||||
|
||||
final textField = find.descendant(
|
||||
of: find.byType(FindAndReplaceMenuWidget),
|
||||
matching: find.byType(TextField),
|
||||
);
|
||||
|
||||
await tester.enterText(
|
||||
textField,
|
||||
"123456",
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.text("123456", findRichText: true),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.text("1234567", findRichText: true),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.showKeyboard(textField);
|
||||
await tester.idle();
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.text("12345678", findRichText: true),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// tap next button, go back to beginning of document
|
||||
await tester.tapButton(
|
||||
find.descendant(
|
||||
of: find.byType(FindMenu),
|
||||
matching: find.byFlowySvg(FlowySvgs.arrow_down_s),
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.text("123456", findRichText: true),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'document_block_option_test.dart' as document_block_option_test;
|
||||
import 'document_find_menu_test.dart' as document_find_menu_test;
|
||||
import 'document_inline_page_reference_test.dart'
|
||||
as document_inline_page_reference_test;
|
||||
import 'document_more_actions_test.dart' as document_more_actions_test;
|
||||
@ -22,5 +23,6 @@ void main() {
|
||||
document_with_file_test.main();
|
||||
document_shortcuts_test.main();
|
||||
document_block_option_test.main();
|
||||
document_find_menu_test.main();
|
||||
document_toolbar_test.main();
|
||||
}
|
||||
|
||||
@ -161,7 +161,16 @@ class _DocumentPageState extends State<DocumentPage>
|
||||
}
|
||||
|
||||
return Provider(
|
||||
create: (_) => SharedEditorContext(),
|
||||
create: (_) {
|
||||
final context = SharedEditorContext();
|
||||
if (widget.view.name.isEmpty) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.coverTitleFocusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
return context;
|
||||
},
|
||||
dispose: (buildContext, editorContext) => editorContext.dispose(),
|
||||
child: EditorTransactionService(
|
||||
viewId: widget.view.id,
|
||||
editorState: state.editorState!,
|
||||
|
||||
@ -485,6 +485,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: FindAndReplaceMenuWidget(
|
||||
showReplaceMenu: showReplaceMenu,
|
||||
editorState: editorState,
|
||||
onDismiss: onDismiss,
|
||||
),
|
||||
|
||||
@ -1,62 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_input.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FindAndReplaceMenuWidget extends StatefulWidget {
|
||||
const FindAndReplaceMenuWidget({
|
||||
super.key,
|
||||
required this.onDismiss,
|
||||
required this.editorState,
|
||||
required this.showReplaceMenu,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
final VoidCallback onDismiss;
|
||||
|
||||
/// Whether to show the replace menu initially
|
||||
final bool showReplaceMenu;
|
||||
|
||||
@override
|
||||
State<FindAndReplaceMenuWidget> createState() =>
|
||||
_FindAndReplaceMenuWidgetState();
|
||||
}
|
||||
|
||||
class _FindAndReplaceMenuWidgetState extends State<FindAndReplaceMenuWidget> {
|
||||
bool showReplaceMenu = false;
|
||||
late bool showReplaceMenu = widget.showReplaceMenu;
|
||||
|
||||
final findFocusNode = FocusNode();
|
||||
final replaceFocusNode = FocusNode();
|
||||
|
||||
late SearchServiceV3 searchService = SearchServiceV3(
|
||||
editorState: widget.editorState,
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (widget.showReplaceMenu) {
|
||||
replaceFocusNode.requestFocus();
|
||||
} else {
|
||||
findFocusNode.requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
findFocusNode.dispose();
|
||||
replaceFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: FindMenu(
|
||||
onDismiss: widget.onDismiss,
|
||||
editorState: widget.editorState,
|
||||
searchService: searchService,
|
||||
onShowReplace: (value) => setState(
|
||||
() => showReplaceMenu = value,
|
||||
return TextFieldTapRegion(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: FindMenu(
|
||||
onDismiss: widget.onDismiss,
|
||||
editorState: widget.editorState,
|
||||
searchService: searchService,
|
||||
focusNode: findFocusNode,
|
||||
showReplaceMenu: showReplaceMenu,
|
||||
onToggleShowReplace: () => setState(() {
|
||||
showReplaceMenu = !showReplaceMenu;
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
showReplaceMenu
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 8.0,
|
||||
),
|
||||
child: ReplaceMenu(
|
||||
editorState: widget.editorState,
|
||||
searchService: searchService,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
if (showReplaceMenu)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 8.0,
|
||||
),
|
||||
child: ReplaceMenu(
|
||||
editorState: widget.editorState,
|
||||
searchService: searchService,
|
||||
focusNode: replaceFocusNode,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -64,29 +92,30 @@ class _FindAndReplaceMenuWidgetState extends State<FindAndReplaceMenuWidget> {
|
||||
class FindMenu extends StatefulWidget {
|
||||
const FindMenu({
|
||||
super.key,
|
||||
required this.onDismiss,
|
||||
required this.editorState,
|
||||
required this.searchService,
|
||||
required this.onShowReplace,
|
||||
required this.showReplaceMenu,
|
||||
required this.focusNode,
|
||||
required this.onDismiss,
|
||||
required this.onToggleShowReplace,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
final VoidCallback onDismiss;
|
||||
final SearchServiceV3 searchService;
|
||||
final void Function(bool value) onShowReplace;
|
||||
|
||||
final bool showReplaceMenu;
|
||||
final FocusNode focusNode;
|
||||
|
||||
final VoidCallback onDismiss;
|
||||
final void Function() onToggleShowReplace;
|
||||
|
||||
@override
|
||||
State<FindMenu> createState() => _FindMenuState();
|
||||
}
|
||||
|
||||
class _FindMenuState extends State<FindMenu> {
|
||||
late final FocusNode findTextFieldFocusNode;
|
||||
final textController = TextEditingController();
|
||||
|
||||
final findTextEditingController = TextEditingController();
|
||||
|
||||
String queriedPattern = '';
|
||||
|
||||
bool showReplaceMenu = false;
|
||||
bool caseSensitive = false;
|
||||
|
||||
@override
|
||||
@ -96,11 +125,7 @@ class _FindMenuState extends State<FindMenu> {
|
||||
widget.searchService.matchWrappers.addListener(_setState);
|
||||
widget.searchService.currentSelectedIndex.addListener(_setState);
|
||||
|
||||
findTextEditingController.addListener(_searchPattern);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
findTextFieldFocusNode.requestFocus();
|
||||
});
|
||||
textController.addListener(_searchPattern);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -108,9 +133,7 @@ class _FindMenuState extends State<FindMenu> {
|
||||
widget.searchService.matchWrappers.removeListener(_setState);
|
||||
widget.searchService.currentSelectedIndex.removeListener(_setState);
|
||||
widget.searchService.dispose();
|
||||
findTextEditingController.removeListener(_searchPattern);
|
||||
findTextEditingController.dispose();
|
||||
findTextFieldFocusNode.dispose();
|
||||
textController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -124,42 +147,36 @@ class _FindMenuState extends State<FindMenu> {
|
||||
const HSpace(4.0),
|
||||
// expand/collapse button
|
||||
_FindAndReplaceIcon(
|
||||
icon: showReplaceMenu
|
||||
icon: widget.showReplaceMenu
|
||||
? FlowySvgs.drop_menu_show_s
|
||||
: FlowySvgs.drop_menu_hide_s,
|
||||
tooltipText: '',
|
||||
onPressed: () {
|
||||
widget.onShowReplace(!showReplaceMenu);
|
||||
setState(
|
||||
() => showReplaceMenu = !showReplaceMenu,
|
||||
);
|
||||
},
|
||||
onPressed: widget.onToggleShowReplace,
|
||||
),
|
||||
const HSpace(4.0),
|
||||
// find text input
|
||||
SizedBox(
|
||||
width: 150,
|
||||
width: 200,
|
||||
height: 30,
|
||||
child: FlowyFormTextInput(
|
||||
onFocusCreated: (focusNode) {
|
||||
findTextFieldFocusNode = focusNode;
|
||||
},
|
||||
onEditingComplete: () {
|
||||
child: TextField(
|
||||
key: const Key('findTextField'),
|
||||
focusNode: widget.focusNode,
|
||||
controller: textController,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
onSubmitted: (_) {
|
||||
widget.searchService.navigateToMatch();
|
||||
|
||||
// after update selection or navigate to match, the editor
|
||||
// will request focus, here's a workaround to request the
|
||||
// focus back to the findTextField
|
||||
Future.delayed(const Duration(milliseconds: 50), () {
|
||||
if (context.mounted) {
|
||||
FocusScope.of(context).requestFocus(
|
||||
findTextFieldFocusNode,
|
||||
);
|
||||
}
|
||||
});
|
||||
// will request focus, here's a workaround to request the
|
||||
// focus back to the text field
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 50),
|
||||
() => widget.focusNode.requestFocus(),
|
||||
);
|
||||
},
|
||||
controller: findTextEditingController,
|
||||
hintText: LocaleKeys.findAndReplace_find.tr(),
|
||||
textAlign: TextAlign.left,
|
||||
decoration: _buildInputDecoration(
|
||||
LocaleKeys.findAndReplace_find.tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
// the count of matches
|
||||
@ -210,11 +227,8 @@ class _FindMenuState extends State<FindMenu> {
|
||||
}
|
||||
|
||||
void _searchPattern() {
|
||||
if (findTextEditingController.text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
widget.searchService.findAndHighlight(findTextEditingController.text);
|
||||
setState(() => queriedPattern = findTextEditingController.text);
|
||||
widget.searchService.findAndHighlight(textController.text);
|
||||
_setState();
|
||||
}
|
||||
|
||||
void _setState() {
|
||||
@ -227,27 +241,24 @@ class ReplaceMenu extends StatefulWidget {
|
||||
super.key,
|
||||
required this.editorState,
|
||||
required this.searchService,
|
||||
this.localizations,
|
||||
required this.focusNode,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
|
||||
/// The localizations of the find and replace menu
|
||||
final FindReplaceLocalizations? localizations;
|
||||
|
||||
final SearchServiceV3 searchService;
|
||||
|
||||
final FocusNode focusNode;
|
||||
|
||||
@override
|
||||
State<ReplaceMenu> createState() => _ReplaceMenuState();
|
||||
}
|
||||
|
||||
class _ReplaceMenuState extends State<ReplaceMenu> {
|
||||
late final FocusNode replaceTextFieldFocusNode;
|
||||
final replaceTextEditingController = TextEditingController();
|
||||
final textController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
replaceTextEditingController.dispose();
|
||||
textController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -258,31 +269,26 @@ class _ReplaceMenuState extends State<ReplaceMenu> {
|
||||
// placeholder for aligning the replace menu
|
||||
const HSpace(30),
|
||||
SizedBox(
|
||||
width: 150,
|
||||
width: 200,
|
||||
height: 30,
|
||||
child: FlowyFormTextInput(
|
||||
onFocusCreated: (focusNode) {
|
||||
replaceTextFieldFocusNode = focusNode;
|
||||
child: TextField(
|
||||
key: const Key('replaceTextField'),
|
||||
focusNode: widget.focusNode,
|
||||
controller: textController,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
onSubmitted: (_) {
|
||||
_replaceSelectedWord();
|
||||
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 50),
|
||||
() => widget.focusNode.requestFocus(),
|
||||
);
|
||||
},
|
||||
onEditingComplete: () {
|
||||
widget.searchService.navigateToMatch();
|
||||
// after update selection or navigate to match, the editor
|
||||
// will request focus, here's a workaround to request the
|
||||
// focus back to the findTextField
|
||||
Future.delayed(const Duration(milliseconds: 50), () {
|
||||
if (context.mounted) {
|
||||
FocusScope.of(context).requestFocus(
|
||||
replaceTextFieldFocusNode,
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
controller: replaceTextEditingController,
|
||||
hintText: LocaleKeys.findAndReplace_replace.tr(),
|
||||
textAlign: TextAlign.left,
|
||||
decoration: _buildInputDecoration(
|
||||
LocaleKeys.findAndReplace_replace.tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const HSpace(4.0),
|
||||
_FindAndReplaceIcon(
|
||||
onPressed: _replaceSelectedWord,
|
||||
iconBuilder: (_) => const Icon(
|
||||
@ -299,7 +305,7 @@ class _ReplaceMenuState extends State<ReplaceMenu> {
|
||||
),
|
||||
tooltipText: LocaleKeys.findAndReplace_replaceAll.tr(),
|
||||
onPressed: () => widget.searchService.replaceAllMatches(
|
||||
replaceTextEditingController.text,
|
||||
textController.text,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -307,7 +313,7 @@ class _ReplaceMenuState extends State<ReplaceMenu> {
|
||||
}
|
||||
|
||||
void _replaceSelectedWord() {
|
||||
widget.searchService.replaceSelectedWord(replaceTextEditingController.text);
|
||||
widget.searchService.replaceSelectedWord(textController.text);
|
||||
}
|
||||
}
|
||||
|
||||
@ -333,10 +339,20 @@ class _FindAndReplaceIcon extends StatelessWidget {
|
||||
height: 24,
|
||||
onPressed: onPressed,
|
||||
icon: iconBuilder?.call(context) ??
|
||||
(icon != null ? FlowySvg(icon!) : const Placeholder()),
|
||||
(icon != null
|
||||
? FlowySvg(icon!, color: Theme.of(context).iconTheme.color)
|
||||
: const Placeholder()),
|
||||
tooltipText: tooltipText,
|
||||
isSelected: isSelected,
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InputDecoration _buildInputDecoration(String hintText) {
|
||||
return InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
border: const UnderlineInputBorder(),
|
||||
hintText: hintText,
|
||||
);
|
||||
}
|
||||
|
||||
@ -45,11 +45,10 @@ class _InnerCoverTitle extends StatefulWidget {
|
||||
|
||||
class _InnerCoverTitleState extends State<_InnerCoverTitle> {
|
||||
final titleTextController = TextEditingController();
|
||||
final titleFocusNode = FocusNode();
|
||||
|
||||
late final editorContext = context.read<SharedEditorContext>();
|
||||
late final editorState = context.read<EditorState>();
|
||||
bool isTitleFocused = false;
|
||||
late final titleFocusNode = editorContext.coverTitleFocusNode;
|
||||
int lineCount = 1;
|
||||
|
||||
@override
|
||||
@ -58,53 +57,32 @@ class _InnerCoverTitleState extends State<_InnerCoverTitle> {
|
||||
|
||||
titleTextController.text = widget.view.name;
|
||||
titleTextController.addListener(_onViewNameChanged);
|
||||
titleFocusNode.onKeyEvent = _onKeyEvent;
|
||||
titleFocusNode.addListener(_onTitleFocusChanged);
|
||||
|
||||
titleFocusNode
|
||||
..onKeyEvent = _onKeyEvent
|
||||
..addListener(_onFocusChanged);
|
||||
|
||||
editorState.selectionNotifier.addListener(_onSelectionChanged);
|
||||
_requestFocusIfNeeded(widget.view, null);
|
||||
|
||||
editorContext.coverTitleFocusNode = titleFocusNode;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
editorContext.coverTitleFocusNode = null;
|
||||
editorState.selectionNotifier.removeListener(_onSelectionChanged);
|
||||
|
||||
titleTextController.removeListener(_onViewNameChanged);
|
||||
titleFocusNode
|
||||
..onKeyEvent = null
|
||||
..removeListener(_onFocusChanged);
|
||||
titleTextController.dispose();
|
||||
titleFocusNode.removeListener(_onTitleFocusChanged);
|
||||
titleFocusNode.dispose();
|
||||
|
||||
editorState.selectionNotifier.removeListener(_onSelectionChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onSelectionChanged() {
|
||||
// if title is focused and the selection is not null, clear the selection
|
||||
if (editorState.selection != null && isTitleFocused) {
|
||||
if (editorState.selection != null && titleFocusNode.hasFocus) {
|
||||
Log.info('title is focused, clear the editor selection');
|
||||
editorState.selection = null;
|
||||
}
|
||||
}
|
||||
|
||||
void _onTitleFocusChanged() {
|
||||
isTitleFocused = titleFocusNode.hasFocus;
|
||||
|
||||
if (titleFocusNode.hasFocus && editorState.selection != null) {
|
||||
Log.info('cover title got focus, clear the editor selection');
|
||||
editorState.selection = null;
|
||||
}
|
||||
|
||||
if (isTitleFocused) {
|
||||
Log.info('cover title got focus, disable keyboard service');
|
||||
editorState.service.keyboardService?.disable();
|
||||
} else {
|
||||
Log.info('cover title lost focus, enable keyboard service');
|
||||
editorState.service.keyboardService?.enable();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fontStyle = Theme.of(context)
|
||||
@ -175,6 +153,21 @@ class _InnerCoverTitleState extends State<_InnerCoverTitle> {
|
||||
}
|
||||
}
|
||||
|
||||
void _onFocusChanged() {
|
||||
if (titleFocusNode.hasFocus) {
|
||||
if (editorState.selection != null) {
|
||||
Log.info('cover title got focus, clear the editor selection');
|
||||
editorState.selection = null;
|
||||
}
|
||||
|
||||
Log.info('cover title got focus, disable keyboard service');
|
||||
editorState.service.keyboardService?.disable();
|
||||
} else {
|
||||
Log.info('cover title lost focus, enable keyboard service');
|
||||
editorState.service.keyboardService?.enable();
|
||||
}
|
||||
}
|
||||
|
||||
void _onViewNameChanged() {
|
||||
Debounce.debounce(
|
||||
'update view name',
|
||||
|
||||
@ -103,8 +103,6 @@ class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
|
||||
late final ViewListener viewListener;
|
||||
int retryCount = 0;
|
||||
|
||||
final titleTextController = TextEditingController();
|
||||
final titleFocusNode = FocusNode();
|
||||
final isCoverTitleHovered = ValueNotifier<bool>(false);
|
||||
|
||||
late final gestureInterceptor = SelectionGestureInterceptor(
|
||||
@ -120,7 +118,6 @@ class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
|
||||
viewIcon = value.isNotEmpty ? value : icon ?? '';
|
||||
cover = widget.view.cover;
|
||||
view = widget.view;
|
||||
titleTextController.text = view.name;
|
||||
widget.node.addListener(_reload);
|
||||
widget.editorState.service.selectionService
|
||||
.registerGestureInterceptor(gestureInterceptor);
|
||||
@ -128,9 +125,6 @@ class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
|
||||
viewListener = ViewListener(viewId: widget.view.id)
|
||||
..start(
|
||||
onViewUpdated: (view) {
|
||||
if (titleTextController.text != view.name) {
|
||||
titleTextController.text = view.name;
|
||||
}
|
||||
setState(() {
|
||||
viewIcon = view.icon.value;
|
||||
cover = view.cover;
|
||||
@ -144,8 +138,6 @@ class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
|
||||
void dispose() {
|
||||
viewListener.stop();
|
||||
widget.node.removeListener(_reload);
|
||||
titleTextController.dispose();
|
||||
titleFocusNode.dispose();
|
||||
isCoverTitleHovered.dispose();
|
||||
widget.editorState.service.selectionService
|
||||
.unregisterGestureInterceptor(_interceptorKey);
|
||||
|
||||
@ -6,9 +6,14 @@ import 'package:flutter/widgets.dart';
|
||||
/// so we need to use the shared context to get the focus node.
|
||||
///
|
||||
class SharedEditorContext {
|
||||
SharedEditorContext();
|
||||
SharedEditorContext() : _coverTitleFocusNode = FocusNode();
|
||||
|
||||
// The focus node of the cover title.
|
||||
// It's null when the cover title is not focused.
|
||||
FocusNode? coverTitleFocusNode;
|
||||
final FocusNode _coverTitleFocusNode;
|
||||
|
||||
FocusNode get coverTitleFocusNode => _coverTitleFocusNode;
|
||||
|
||||
void dispose() {
|
||||
_coverTitleFocusNode.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,8 +7,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
|
||||
class FlowyFormTextInput extends StatelessWidget {
|
||||
static EdgeInsets kDefaultTextInputPadding =
|
||||
EdgeInsets.only(bottom: Insets.sm, top: 4);
|
||||
static EdgeInsets kDefaultTextInputPadding = const EdgeInsets.only(bottom: 2);
|
||||
|
||||
final String? label;
|
||||
final bool? autoFocus;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user