mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-12-17 10:14:47 +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 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
import 'document_block_option_test.dart' as document_block_option_test;
|
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'
|
import 'document_inline_page_reference_test.dart'
|
||||||
as document_inline_page_reference_test;
|
as document_inline_page_reference_test;
|
||||||
import 'document_more_actions_test.dart' as document_more_actions_test;
|
import 'document_more_actions_test.dart' as document_more_actions_test;
|
||||||
@ -22,5 +23,6 @@ void main() {
|
|||||||
document_with_file_test.main();
|
document_with_file_test.main();
|
||||||
document_shortcuts_test.main();
|
document_shortcuts_test.main();
|
||||||
document_block_option_test.main();
|
document_block_option_test.main();
|
||||||
|
document_find_menu_test.main();
|
||||||
document_toolbar_test.main();
|
document_toolbar_test.main();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -161,7 +161,16 @@ class _DocumentPageState extends State<DocumentPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Provider(
|
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(
|
child: EditorTransactionService(
|
||||||
viewId: widget.view.id,
|
viewId: widget.view.id,
|
||||||
editorState: state.editorState!,
|
editorState: state.editorState!,
|
||||||
|
|||||||
@ -485,6 +485,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
|
|||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: FindAndReplaceMenuWidget(
|
child: FindAndReplaceMenuWidget(
|
||||||
|
showReplaceMenu: showReplaceMenu,
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
onDismiss: onDismiss,
|
onDismiss: onDismiss,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,62 +1,90 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.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 {
|
class FindAndReplaceMenuWidget extends StatefulWidget {
|
||||||
const FindAndReplaceMenuWidget({
|
const FindAndReplaceMenuWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onDismiss,
|
required this.onDismiss,
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
|
required this.showReplaceMenu,
|
||||||
});
|
});
|
||||||
|
|
||||||
final EditorState editorState;
|
final EditorState editorState;
|
||||||
final VoidCallback onDismiss;
|
final VoidCallback onDismiss;
|
||||||
|
|
||||||
|
/// Whether to show the replace menu initially
|
||||||
|
final bool showReplaceMenu;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FindAndReplaceMenuWidget> createState() =>
|
State<FindAndReplaceMenuWidget> createState() =>
|
||||||
_FindAndReplaceMenuWidgetState();
|
_FindAndReplaceMenuWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FindAndReplaceMenuWidgetState extends State<FindAndReplaceMenuWidget> {
|
class _FindAndReplaceMenuWidgetState extends State<FindAndReplaceMenuWidget> {
|
||||||
bool showReplaceMenu = false;
|
late bool showReplaceMenu = widget.showReplaceMenu;
|
||||||
|
|
||||||
|
final findFocusNode = FocusNode();
|
||||||
|
final replaceFocusNode = FocusNode();
|
||||||
|
|
||||||
late SearchServiceV3 searchService = SearchServiceV3(
|
late SearchServiceV3 searchService = SearchServiceV3(
|
||||||
editorState: widget.editorState,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return TextFieldTapRegion(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Padding(
|
children: [
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
Padding(
|
||||||
child: FindMenu(
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
onDismiss: widget.onDismiss,
|
child: FindMenu(
|
||||||
editorState: widget.editorState,
|
onDismiss: widget.onDismiss,
|
||||||
searchService: searchService,
|
editorState: widget.editorState,
|
||||||
onShowReplace: (value) => setState(
|
searchService: searchService,
|
||||||
() => showReplaceMenu = value,
|
focusNode: findFocusNode,
|
||||||
|
showReplaceMenu: showReplaceMenu,
|
||||||
|
onToggleShowReplace: () => setState(() {
|
||||||
|
showReplaceMenu = !showReplaceMenu;
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (showReplaceMenu)
|
||||||
showReplaceMenu
|
Padding(
|
||||||
? Padding(
|
padding: const EdgeInsets.only(
|
||||||
padding: const EdgeInsets.only(
|
bottom: 8.0,
|
||||||
bottom: 8.0,
|
),
|
||||||
),
|
child: ReplaceMenu(
|
||||||
child: ReplaceMenu(
|
editorState: widget.editorState,
|
||||||
editorState: widget.editorState,
|
searchService: searchService,
|
||||||
searchService: searchService,
|
focusNode: replaceFocusNode,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
: const SizedBox.shrink(),
|
],
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,29 +92,30 @@ class _FindAndReplaceMenuWidgetState extends State<FindAndReplaceMenuWidget> {
|
|||||||
class FindMenu extends StatefulWidget {
|
class FindMenu extends StatefulWidget {
|
||||||
const FindMenu({
|
const FindMenu({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onDismiss,
|
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
required this.searchService,
|
required this.searchService,
|
||||||
required this.onShowReplace,
|
required this.showReplaceMenu,
|
||||||
|
required this.focusNode,
|
||||||
|
required this.onDismiss,
|
||||||
|
required this.onToggleShowReplace,
|
||||||
});
|
});
|
||||||
|
|
||||||
final EditorState editorState;
|
final EditorState editorState;
|
||||||
final VoidCallback onDismiss;
|
|
||||||
final SearchServiceV3 searchService;
|
final SearchServiceV3 searchService;
|
||||||
final void Function(bool value) onShowReplace;
|
|
||||||
|
final bool showReplaceMenu;
|
||||||
|
final FocusNode focusNode;
|
||||||
|
|
||||||
|
final VoidCallback onDismiss;
|
||||||
|
final void Function() onToggleShowReplace;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FindMenu> createState() => _FindMenuState();
|
State<FindMenu> createState() => _FindMenuState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FindMenuState extends State<FindMenu> {
|
class _FindMenuState extends State<FindMenu> {
|
||||||
late final FocusNode findTextFieldFocusNode;
|
final textController = TextEditingController();
|
||||||
|
|
||||||
final findTextEditingController = TextEditingController();
|
|
||||||
|
|
||||||
String queriedPattern = '';
|
|
||||||
|
|
||||||
bool showReplaceMenu = false;
|
|
||||||
bool caseSensitive = false;
|
bool caseSensitive = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -96,11 +125,7 @@ class _FindMenuState extends State<FindMenu> {
|
|||||||
widget.searchService.matchWrappers.addListener(_setState);
|
widget.searchService.matchWrappers.addListener(_setState);
|
||||||
widget.searchService.currentSelectedIndex.addListener(_setState);
|
widget.searchService.currentSelectedIndex.addListener(_setState);
|
||||||
|
|
||||||
findTextEditingController.addListener(_searchPattern);
|
textController.addListener(_searchPattern);
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
findTextFieldFocusNode.requestFocus();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -108,9 +133,7 @@ class _FindMenuState extends State<FindMenu> {
|
|||||||
widget.searchService.matchWrappers.removeListener(_setState);
|
widget.searchService.matchWrappers.removeListener(_setState);
|
||||||
widget.searchService.currentSelectedIndex.removeListener(_setState);
|
widget.searchService.currentSelectedIndex.removeListener(_setState);
|
||||||
widget.searchService.dispose();
|
widget.searchService.dispose();
|
||||||
findTextEditingController.removeListener(_searchPattern);
|
textController.dispose();
|
||||||
findTextEditingController.dispose();
|
|
||||||
findTextFieldFocusNode.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,42 +147,36 @@ class _FindMenuState extends State<FindMenu> {
|
|||||||
const HSpace(4.0),
|
const HSpace(4.0),
|
||||||
// expand/collapse button
|
// expand/collapse button
|
||||||
_FindAndReplaceIcon(
|
_FindAndReplaceIcon(
|
||||||
icon: showReplaceMenu
|
icon: widget.showReplaceMenu
|
||||||
? FlowySvgs.drop_menu_show_s
|
? FlowySvgs.drop_menu_show_s
|
||||||
: FlowySvgs.drop_menu_hide_s,
|
: FlowySvgs.drop_menu_hide_s,
|
||||||
tooltipText: '',
|
tooltipText: '',
|
||||||
onPressed: () {
|
onPressed: widget.onToggleShowReplace,
|
||||||
widget.onShowReplace(!showReplaceMenu);
|
|
||||||
setState(
|
|
||||||
() => showReplaceMenu = !showReplaceMenu,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const HSpace(4.0),
|
const HSpace(4.0),
|
||||||
// find text input
|
// find text input
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 150,
|
width: 200,
|
||||||
height: 30,
|
height: 30,
|
||||||
child: FlowyFormTextInput(
|
child: TextField(
|
||||||
onFocusCreated: (focusNode) {
|
key: const Key('findTextField'),
|
||||||
findTextFieldFocusNode = focusNode;
|
focusNode: widget.focusNode,
|
||||||
},
|
controller: textController,
|
||||||
onEditingComplete: () {
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
onSubmitted: (_) {
|
||||||
widget.searchService.navigateToMatch();
|
widget.searchService.navigateToMatch();
|
||||||
|
|
||||||
// after update selection or navigate to match, the editor
|
// after update selection or navigate to match, the editor
|
||||||
// will request focus, here's a workaround to request the
|
// will request focus, here's a workaround to request the
|
||||||
// focus back to the findTextField
|
// focus back to the text field
|
||||||
Future.delayed(const Duration(milliseconds: 50), () {
|
Future.delayed(
|
||||||
if (context.mounted) {
|
const Duration(milliseconds: 50),
|
||||||
FocusScope.of(context).requestFocus(
|
() => widget.focusNode.requestFocus(),
|
||||||
findTextFieldFocusNode,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
controller: findTextEditingController,
|
decoration: _buildInputDecoration(
|
||||||
hintText: LocaleKeys.findAndReplace_find.tr(),
|
LocaleKeys.findAndReplace_find.tr(),
|
||||||
textAlign: TextAlign.left,
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// the count of matches
|
// the count of matches
|
||||||
@ -210,11 +227,8 @@ class _FindMenuState extends State<FindMenu> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _searchPattern() {
|
void _searchPattern() {
|
||||||
if (findTextEditingController.text.isEmpty) {
|
widget.searchService.findAndHighlight(textController.text);
|
||||||
return;
|
_setState();
|
||||||
}
|
|
||||||
widget.searchService.findAndHighlight(findTextEditingController.text);
|
|
||||||
setState(() => queriedPattern = findTextEditingController.text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setState() {
|
void _setState() {
|
||||||
@ -227,27 +241,24 @@ class ReplaceMenu extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
required this.searchService,
|
required this.searchService,
|
||||||
this.localizations,
|
required this.focusNode,
|
||||||
});
|
});
|
||||||
|
|
||||||
final EditorState editorState;
|
final EditorState editorState;
|
||||||
|
|
||||||
/// The localizations of the find and replace menu
|
|
||||||
final FindReplaceLocalizations? localizations;
|
|
||||||
|
|
||||||
final SearchServiceV3 searchService;
|
final SearchServiceV3 searchService;
|
||||||
|
|
||||||
|
final FocusNode focusNode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ReplaceMenu> createState() => _ReplaceMenuState();
|
State<ReplaceMenu> createState() => _ReplaceMenuState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ReplaceMenuState extends State<ReplaceMenu> {
|
class _ReplaceMenuState extends State<ReplaceMenu> {
|
||||||
late final FocusNode replaceTextFieldFocusNode;
|
final textController = TextEditingController();
|
||||||
final replaceTextEditingController = TextEditingController();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
replaceTextEditingController.dispose();
|
textController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,31 +269,26 @@ class _ReplaceMenuState extends State<ReplaceMenu> {
|
|||||||
// placeholder for aligning the replace menu
|
// placeholder for aligning the replace menu
|
||||||
const HSpace(30),
|
const HSpace(30),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 150,
|
width: 200,
|
||||||
height: 30,
|
height: 30,
|
||||||
child: FlowyFormTextInput(
|
child: TextField(
|
||||||
onFocusCreated: (focusNode) {
|
key: const Key('replaceTextField'),
|
||||||
replaceTextFieldFocusNode = focusNode;
|
focusNode: widget.focusNode,
|
||||||
|
controller: textController,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
onSubmitted: (_) {
|
||||||
|
_replaceSelectedWord();
|
||||||
|
|
||||||
|
Future.delayed(
|
||||||
|
const Duration(milliseconds: 50),
|
||||||
|
() => widget.focusNode.requestFocus(),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onEditingComplete: () {
|
decoration: _buildInputDecoration(
|
||||||
widget.searchService.navigateToMatch();
|
LocaleKeys.findAndReplace_replace.tr(),
|
||||||
// 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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const HSpace(4.0),
|
|
||||||
_FindAndReplaceIcon(
|
_FindAndReplaceIcon(
|
||||||
onPressed: _replaceSelectedWord,
|
onPressed: _replaceSelectedWord,
|
||||||
iconBuilder: (_) => const Icon(
|
iconBuilder: (_) => const Icon(
|
||||||
@ -299,7 +305,7 @@ class _ReplaceMenuState extends State<ReplaceMenu> {
|
|||||||
),
|
),
|
||||||
tooltipText: LocaleKeys.findAndReplace_replaceAll.tr(),
|
tooltipText: LocaleKeys.findAndReplace_replaceAll.tr(),
|
||||||
onPressed: () => widget.searchService.replaceAllMatches(
|
onPressed: () => widget.searchService.replaceAllMatches(
|
||||||
replaceTextEditingController.text,
|
textController.text,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -307,7 +313,7 @@ class _ReplaceMenuState extends State<ReplaceMenu> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _replaceSelectedWord() {
|
void _replaceSelectedWord() {
|
||||||
widget.searchService.replaceSelectedWord(replaceTextEditingController.text);
|
widget.searchService.replaceSelectedWord(textController.text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -333,10 +339,20 @@ class _FindAndReplaceIcon extends StatelessWidget {
|
|||||||
height: 24,
|
height: 24,
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
icon: iconBuilder?.call(context) ??
|
icon: iconBuilder?.call(context) ??
|
||||||
(icon != null ? FlowySvg(icon!) : const Placeholder()),
|
(icon != null
|
||||||
|
? FlowySvg(icon!, color: Theme.of(context).iconTheme.color)
|
||||||
|
: const Placeholder()),
|
||||||
tooltipText: tooltipText,
|
tooltipText: tooltipText,
|
||||||
isSelected: isSelected,
|
isSelected: isSelected,
|
||||||
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
|
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> {
|
class _InnerCoverTitleState extends State<_InnerCoverTitle> {
|
||||||
final titleTextController = TextEditingController();
|
final titleTextController = TextEditingController();
|
||||||
final titleFocusNode = FocusNode();
|
|
||||||
|
|
||||||
late final editorContext = context.read<SharedEditorContext>();
|
late final editorContext = context.read<SharedEditorContext>();
|
||||||
late final editorState = context.read<EditorState>();
|
late final editorState = context.read<EditorState>();
|
||||||
bool isTitleFocused = false;
|
late final titleFocusNode = editorContext.coverTitleFocusNode;
|
||||||
int lineCount = 1;
|
int lineCount = 1;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -58,53 +57,32 @@ class _InnerCoverTitleState extends State<_InnerCoverTitle> {
|
|||||||
|
|
||||||
titleTextController.text = widget.view.name;
|
titleTextController.text = widget.view.name;
|
||||||
titleTextController.addListener(_onViewNameChanged);
|
titleTextController.addListener(_onViewNameChanged);
|
||||||
titleFocusNode.onKeyEvent = _onKeyEvent;
|
|
||||||
titleFocusNode.addListener(_onTitleFocusChanged);
|
titleFocusNode
|
||||||
|
..onKeyEvent = _onKeyEvent
|
||||||
|
..addListener(_onFocusChanged);
|
||||||
|
|
||||||
editorState.selectionNotifier.addListener(_onSelectionChanged);
|
editorState.selectionNotifier.addListener(_onSelectionChanged);
|
||||||
_requestFocusIfNeeded(widget.view, null);
|
|
||||||
|
|
||||||
editorContext.coverTitleFocusNode = titleFocusNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
editorContext.coverTitleFocusNode = null;
|
titleFocusNode
|
||||||
editorState.selectionNotifier.removeListener(_onSelectionChanged);
|
..onKeyEvent = null
|
||||||
|
..removeListener(_onFocusChanged);
|
||||||
titleTextController.removeListener(_onViewNameChanged);
|
|
||||||
titleTextController.dispose();
|
titleTextController.dispose();
|
||||||
titleFocusNode.removeListener(_onTitleFocusChanged);
|
editorState.selectionNotifier.removeListener(_onSelectionChanged);
|
||||||
titleFocusNode.dispose();
|
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSelectionChanged() {
|
void _onSelectionChanged() {
|
||||||
// if title is focused and the selection is not null, clear the selection
|
// 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');
|
Log.info('title is focused, clear the editor selection');
|
||||||
editorState.selection = null;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final fontStyle = Theme.of(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() {
|
void _onViewNameChanged() {
|
||||||
Debounce.debounce(
|
Debounce.debounce(
|
||||||
'update view name',
|
'update view name',
|
||||||
|
|||||||
@ -103,8 +103,6 @@ class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
|
|||||||
late final ViewListener viewListener;
|
late final ViewListener viewListener;
|
||||||
int retryCount = 0;
|
int retryCount = 0;
|
||||||
|
|
||||||
final titleTextController = TextEditingController();
|
|
||||||
final titleFocusNode = FocusNode();
|
|
||||||
final isCoverTitleHovered = ValueNotifier<bool>(false);
|
final isCoverTitleHovered = ValueNotifier<bool>(false);
|
||||||
|
|
||||||
late final gestureInterceptor = SelectionGestureInterceptor(
|
late final gestureInterceptor = SelectionGestureInterceptor(
|
||||||
@ -120,7 +118,6 @@ class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
|
|||||||
viewIcon = value.isNotEmpty ? value : icon ?? '';
|
viewIcon = value.isNotEmpty ? value : icon ?? '';
|
||||||
cover = widget.view.cover;
|
cover = widget.view.cover;
|
||||||
view = widget.view;
|
view = widget.view;
|
||||||
titleTextController.text = view.name;
|
|
||||||
widget.node.addListener(_reload);
|
widget.node.addListener(_reload);
|
||||||
widget.editorState.service.selectionService
|
widget.editorState.service.selectionService
|
||||||
.registerGestureInterceptor(gestureInterceptor);
|
.registerGestureInterceptor(gestureInterceptor);
|
||||||
@ -128,9 +125,6 @@ class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
|
|||||||
viewListener = ViewListener(viewId: widget.view.id)
|
viewListener = ViewListener(viewId: widget.view.id)
|
||||||
..start(
|
..start(
|
||||||
onViewUpdated: (view) {
|
onViewUpdated: (view) {
|
||||||
if (titleTextController.text != view.name) {
|
|
||||||
titleTextController.text = view.name;
|
|
||||||
}
|
|
||||||
setState(() {
|
setState(() {
|
||||||
viewIcon = view.icon.value;
|
viewIcon = view.icon.value;
|
||||||
cover = view.cover;
|
cover = view.cover;
|
||||||
@ -144,8 +138,6 @@ class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
viewListener.stop();
|
viewListener.stop();
|
||||||
widget.node.removeListener(_reload);
|
widget.node.removeListener(_reload);
|
||||||
titleTextController.dispose();
|
|
||||||
titleFocusNode.dispose();
|
|
||||||
isCoverTitleHovered.dispose();
|
isCoverTitleHovered.dispose();
|
||||||
widget.editorState.service.selectionService
|
widget.editorState.service.selectionService
|
||||||
.unregisterGestureInterceptor(_interceptorKey);
|
.unregisterGestureInterceptor(_interceptorKey);
|
||||||
|
|||||||
@ -6,9 +6,14 @@ import 'package:flutter/widgets.dart';
|
|||||||
/// so we need to use the shared context to get the focus node.
|
/// so we need to use the shared context to get the focus node.
|
||||||
///
|
///
|
||||||
class SharedEditorContext {
|
class SharedEditorContext {
|
||||||
SharedEditorContext();
|
SharedEditorContext() : _coverTitleFocusNode = FocusNode();
|
||||||
|
|
||||||
// The focus node of the cover title.
|
// The focus node of the cover title.
|
||||||
// It's null when the cover title is not focused.
|
final FocusNode _coverTitleFocusNode;
|
||||||
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';
|
import 'package:flowy_infra/size.dart';
|
||||||
|
|
||||||
class FlowyFormTextInput extends StatelessWidget {
|
class FlowyFormTextInput extends StatelessWidget {
|
||||||
static EdgeInsets kDefaultTextInputPadding =
|
static EdgeInsets kDefaultTextInputPadding = const EdgeInsets.only(bottom: 2);
|
||||||
EdgeInsets.only(bottom: Insets.sm, top: 4);
|
|
||||||
|
|
||||||
final String? label;
|
final String? label;
|
||||||
final bool? autoFocus;
|
final bool? autoFocus;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user