feat: optimize editor toolbar tooltip (#5889)

* chore: improve icon picker dark mode color

* feat: optimize editor toolbar tooltip style

* feat: customize markdown item tooltip message

* chore: find the tooltip by rich message

* feat: add hover effect in toolbar item

* feat: add hover effect in toolbar item

* chore: optimize hover color in light mode

* chore: fix integration test

* chore: optimize align & font toolbar item

* chore: fix integration test
This commit is contained in:
Lucas.Xu 2024-08-07 11:37:30 +08:00 committed by GitHub
parent a798b037db
commit e279ad1cc7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 293 additions and 233 deletions

View File

@ -33,7 +33,7 @@ void main() {
); );
// tap the inline math equation button // tap the inline math equation button
final inlineMathEquationButton = find.byTooltip( final inlineMathEquationButton = find.findFlowyTooltip(
LocaleKeys.document_plugins_createInlineMathEquation.tr(), LocaleKeys.document_plugins_createInlineMathEquation.tr(),
); );
await tester.tapButton(inlineMathEquationButton); await tester.tapButton(inlineMathEquationButton);
@ -78,7 +78,7 @@ void main() {
); );
// tap the inline math equation button // tap the inline math equation button
var inlineMathEquationButton = find.byTooltip( var inlineMathEquationButton = find.findFlowyTooltip(
LocaleKeys.document_plugins_createInlineMathEquation.tr(), LocaleKeys.document_plugins_createInlineMathEquation.tr(),
); );
await tester.tapButton(inlineMathEquationButton); await tester.tapButton(inlineMathEquationButton);
@ -93,11 +93,11 @@ void main() {
); );
// expect to the see the inline math equation button is highlighted // expect to the see the inline math equation button is highlighted
inlineMathEquationButton = find.byWidgetPredicate( inlineMathEquationButton = find.descendant(
(widget) => of: find.findFlowyTooltip(
widget is SVGIconItemWidget &&
widget.tooltip ==
LocaleKeys.document_plugins_createInlineMathEquation.tr(), LocaleKeys.document_plugins_createInlineMathEquation.tr(),
),
matching: find.byType(SVGIconItemWidget),
); );
expect( expect(
tester.widget<SVGIconItemWidget>(inlineMathEquationButton).isHighlight, tester.widget<SVGIconItemWidget>(inlineMathEquationButton).isHighlight,

View File

@ -1,113 +0,0 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/mock/mock_openai_repository.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
const service = TestWorkspaceService(TestWorkspace.aiWorkSpace);
group('integration tests for open-ai smart menu', () {
setUpAll(() async => service.setUpAll());
setUp(() async => service.setUp());
testWidgets('testing selection on open-ai smart menu replace',
(tester) async {
final appFlowyEditor = await setUpOpenAITesting(tester);
final editorState = appFlowyEditor.editorState;
editorState.service.selectionService.updateSelection(
Selection(
start: Position(path: [1], offset: 4),
end: Position(path: [1], offset: 10),
),
);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
await tester.pumpAndSettle();
expect(find.byType(ToolbarWidget), findsAtLeastNWidgets(1));
await tester.tap(find.byTooltip('AI Assistants'));
await tester.pumpAndSettle(const Duration(milliseconds: 500));
await tester.tap(find.text('Summarize'));
await tester.pumpAndSettle();
await tester
.tap(find.byType(FlowyRichTextButton, skipOffstage: false).first);
await tester.pumpAndSettle();
expect(
editorState.service.selectionService.currentSelection.value,
Selection(
start: Position(path: [1], offset: 4),
end: Position(path: [1], offset: 84),
),
);
});
testWidgets('testing selection on open-ai smart menu insert',
(tester) async {
final appFlowyEditor = await setUpOpenAITesting(tester);
final editorState = appFlowyEditor.editorState;
editorState.service.selectionService.updateSelection(
Selection(
start: Position(path: [1]),
end: Position(path: [1], offset: 5),
),
);
await tester.pumpAndSettle(const Duration(milliseconds: 500));
await tester.pumpAndSettle();
expect(find.byType(ToolbarWidget), findsAtLeastNWidgets(1));
await tester.tap(find.byTooltip('AI Assistants'));
await tester.pumpAndSettle(const Duration(milliseconds: 500));
await tester.tap(find.text('Summarize'));
await tester.pumpAndSettle();
await tester
.tap(find.byType(FlowyRichTextButton, skipOffstage: false).at(1));
await tester.pumpAndSettle();
expect(
editorState.service.selectionService.currentSelection.value,
Selection(
start: Position(path: [2]),
end: Position(path: [3]),
),
);
});
});
}
Future<AppFlowyEditor> setUpOpenAITesting(WidgetTester tester) async {
await tester.initializeAppFlowy();
await mockOpenAIRepository();
await simulateKeyDownEvent(LogicalKeyboardKey.controlLeft);
await simulateKeyDownEvent(LogicalKeyboardKey.backslash);
await tester.pumpAndSettle();
final Finder editor = find.byType(AppFlowyEditor);
await tester.tap(editor);
await tester.pumpAndSettle();
return tester.state(editor).widget as AppFlowyEditor;
}
Future<void> mockOpenAIRepository() async {
await getIt.unregister<AIRepository>();
getIt.registerFactoryAsync<AIRepository>(
() => Future.value(
MockOpenAIRepository(),
),
);
return;
}

View File

@ -1,9 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/env/cloud_env_test.dart'; import 'package:appflowy/env/cloud_env_test.dart';
import 'package:appflowy/startup/entry_point.dart'; import 'package:appflowy/startup/entry_point.dart';
@ -16,6 +13,8 @@ import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widget
import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:flowy_infra/uuid.dart'; import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@ -231,6 +230,16 @@ extension AppFlowyFinderTestBase on CommonFinders {
(widget) => widget is FlowyText && widget.text == text, (widget) => widget is FlowyText && widget.text == text,
); );
} }
Finder findFlowyTooltip(String richMessage, {bool skipOffstage = true}) {
return byWidgetPredicate(
(widget) =>
widget is FlowyTooltip &&
widget.richMessage != null &&
widget.richMessage!.toPlainText().contains(richMessage),
skipOffstage: skipOffstage,
);
}
} }
Future<void> useTestSupabaseCloud() async { Future<void> useTestSupabaseCloud() async {

View File

@ -3,7 +3,7 @@ import 'dart:ui';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart'; import 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';

View File

@ -1,5 +1,5 @@
import 'package:appflowy/plugins/base/emoji/emoji_picker_header.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker_header.dart';
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart'; import 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.dart';
import 'package:appflowy/shared/icon_emoji_picker/emoji_search_bar.dart'; import 'package:appflowy/shared/icon_emoji_picker/emoji_search_bar.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';

View File

@ -387,6 +387,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
editorState: editorState, editorState: editorState,
editorScrollController: editorScrollController, editorScrollController: editorScrollController,
textDirection: textDirection, textDirection: textDirection,
tooltipBuilder: (context, id, message, child) => widget.styleCustomizer
.buildToolbarItemTooltip(context, id, message, child,),
child: editor, child: editor,
), ),
); );

View File

@ -9,12 +9,13 @@ import 'package:flutter/material.dart';
const String leftAlignmentKey = 'left'; const String leftAlignmentKey = 'left';
const String centerAlignmentKey = 'center'; const String centerAlignmentKey = 'center';
const String rightAlignmentKey = 'right'; const String rightAlignmentKey = 'right';
const String kAlignToolbarItemId = 'editor.align';
final alignToolbarItem = ToolbarItem( final alignToolbarItem = ToolbarItem(
id: 'editor.align', id: kAlignToolbarItemId,
group: 4, group: 4,
isActive: onlyShowInTextType, isActive: onlyShowInTextType,
builder: (context, editorState, highlightColor, _) { builder: (context, editorState, highlightColor, _, tooltipBuilder) {
final selection = editorState.selection!; final selection = editorState.selection!;
final nodes = editorState.getNodesInSelection(selection); final nodes = editorState.getNodesInSelection(selection);
@ -37,19 +38,13 @@ final alignToolbarItem = ToolbarItem(
data = FlowySvgs.toolbar_align_right_s; data = FlowySvgs.toolbar_align_right_s;
} }
final child = FlowySvg( Widget child = FlowySvg(
data, data,
size: const Size.square(16), size: const Size.square(16),
color: isHighlight ? highlightColor : Colors.white, color: isHighlight ? highlightColor : Colors.white,
); );
return MouseRegion( child = _AlignmentButtons(
cursor: SystemMouseCursors.click,
child: FlowyTooltip(
message: LocaleKeys.document_plugins_optionAction_align.tr(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: _AlignmentButtons(
child: child, child: child,
onAlignChanged: (align) async { onAlignChanged: (align) async {
await editorState.updateNode( await editorState.updateNode(
@ -62,10 +57,18 @@ final alignToolbarItem = ToolbarItem(
), ),
); );
}, },
),
),
),
); );
if (tooltipBuilder != null) {
child = tooltipBuilder(
context,
kAlignToolbarItemId,
LocaleKeys.document_plugins_optionAction_align.tr(),
child,
);
}
return child;
}, },
); );
@ -83,13 +86,15 @@ class _AlignmentButtons extends StatefulWidget {
} }
class _AlignmentButtonsState extends State<_AlignmentButtons> { class _AlignmentButtonsState extends State<_AlignmentButtons> {
final controller = PopoverController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppFlowyPopover( return AppFlowyPopover(
windowPadding: const EdgeInsets.all(0), windowPadding: const EdgeInsets.all(0),
margin: const EdgeInsets.all(4), margin: const EdgeInsets.symmetric(vertical: 2.0),
direction: PopoverDirection.bottomWithCenterAligned, direction: PopoverDirection.bottomWithCenterAligned,
offset: const Offset(0, 10), offset: const Offset(0, 12),
decorationColor: Theme.of(context).colorScheme.onTertiary, decorationColor: Theme.of(context).colorScheme.onTertiary,
borderRadius: const BorderRadius.all(Radius.circular(4)), borderRadius: const BorderRadius.all(Radius.circular(4)),
popupBuilder: (_) { popupBuilder: (_) {
@ -99,7 +104,12 @@ class _AlignmentButtonsState extends State<_AlignmentButtons> {
onClose: () { onClose: () {
keepEditorFocusNotifier.decrease(); keepEditorFocusNotifier.decrease();
}, },
child: widget.child, child: FlowyButton(
useIntrinsicWidth: true,
text: widget.child,
hoverColor: Colors.grey.withOpacity(0.3),
onTap: () => controller.show(),
),
); );
} }
} }
@ -114,7 +124,7 @@ class _AlignButtons extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
height: 32, height: 28,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -156,11 +166,11 @@ class _AlignButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MouseRegion( return FlowyButton(
cursor: SystemMouseCursors.click, useIntrinsicWidth: true,
child: GestureDetector( hoverColor: Colors.grey.withOpacity(0.3),
onTap: onTap, onTap: onTap,
child: FlowyTooltip( text: FlowyTooltip(
message: tooltips, message: tooltips,
child: FlowySvg( child: FlowySvg(
icon, icon,
@ -168,7 +178,6 @@ class _AlignButton extends StatelessWidget {
color: Colors.white, color: Colors.white,
), ),
), ),
),
); );
} }
} }
@ -179,7 +188,7 @@ class _Divider extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(4),
child: Container( child: Container(
width: 1, width: 1,
color: Colors.grey, color: Colors.grey,

View File

@ -1,6 +1,3 @@
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
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/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
@ -9,6 +6,7 @@ import 'package:appflowy/util/font_family_extension.dart';
import 'package:appflowy/util/levenshtein.dart'; import 'package:appflowy/util/levenshtein.dart';
import 'package:appflowy/workspace/application/appearance_defaults.dart'; import 'package:appflowy/workspace/application/appearance_defaults.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart'; import 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart';
import 'package:appflowy/workspace/presentation/settings/shared/setting_value_dropdown.dart'; import 'package:appflowy/workspace/presentation/settings/shared/setting_value_dropdown.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
@ -20,22 +18,23 @@ import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
const kFontToolbarItemId = 'editor.font';
final customizeFontToolbarItem = ToolbarItem( final customizeFontToolbarItem = ToolbarItem(
id: 'editor.font', id: kFontToolbarItemId,
group: 4, group: 4,
isActive: onlyShowInTextType, isActive: onlyShowInTextType,
builder: (context, editorState, highlightColor, _) { builder: (context, editorState, highlightColor, _, tooltipBuilder) {
final selection = editorState.selection!; final selection = editorState.selection!;
final popoverController = PopoverController(); final popoverController = PopoverController();
final String? currentFontFamily = editorState final String? currentFontFamily = editorState
.getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.fontFamily); .getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.fontFamily);
return MouseRegion(
cursor: SystemMouseCursors.click, Widget child = FontFamilyDropDown(
child: FontFamilyDropDown(
currentFontFamily: currentFontFamily ?? '', currentFontFamily: currentFontFamily ?? '',
offset: const Offset(0, 12), offset: const Offset(0, 12),
popoverController: popoverController, popoverController: popoverController,
@ -57,19 +56,28 @@ final customizeFontToolbarItem = ToolbarItem(
await editorState await editorState
.formatDelta(selection, {AppFlowyRichTextKeys.fontFamily: null}); .formatDelta(selection, {AppFlowyRichTextKeys.fontFamily: null});
}, },
child: Padding( child: FlowyButton(
padding: const EdgeInsets.symmetric(horizontal: 4.0), useIntrinsicWidth: true,
child: FlowyTooltip( hoverColor: Colors.grey.withOpacity(0.3),
message: LocaleKeys.document_plugins_fonts.tr(), onTap: () => popoverController.show(),
child: const FlowySvg( text: const FlowySvg(
FlowySvgs.font_family_s, FlowySvgs.font_family_s,
size: Size.square(16.0), size: Size.square(16.0),
color: Colors.white, color: Colors.white,
), ),
), ),
),
),
); );
if (tooltipBuilder != null) {
child = tooltipBuilder(
context,
kFontToolbarItemId,
LocaleKeys.document_plugins_fonts.tr(),
child,
);
}
return child;
}, },
); );

View File

@ -5,11 +5,13 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
const _kInlineMathEquationToolbarItemId = 'editor.inline_math_equation';
final ToolbarItem inlineMathEquationItem = ToolbarItem( final ToolbarItem inlineMathEquationItem = ToolbarItem(
id: 'editor.inline_math_equation', id: _kInlineMathEquationToolbarItemId,
group: 2, group: 2,
isActive: onlyShowInSingleSelectionAndTextType, isActive: onlyShowInSingleSelectionAndTextType,
builder: (context, editorState, highlightColor, _) { builder: (context, editorState, highlightColor, _, tooltipBuilder) {
final selection = editorState.selection!; final selection = editorState.selection!;
final nodes = editorState.getNodesInSelection(selection); final nodes = editorState.getNodesInSelection(selection);
final isHighlight = nodes.allSatisfyInSelection(selection, (delta) { final isHighlight = nodes.allSatisfyInSelection(selection, (delta) {
@ -17,7 +19,7 @@ final ToolbarItem inlineMathEquationItem = ToolbarItem(
(attributes) => attributes[InlineMathEquationKeys.formula] != null, (attributes) => attributes[InlineMathEquationKeys.formula] != null,
); );
}); });
return SVGIconItemWidget( final child = SVGIconItemWidget(
iconBuilder: (_) => FlowySvg( iconBuilder: (_) => FlowySvg(
FlowySvgs.math_lg, FlowySvgs.math_lg,
size: const Size.square(16), size: const Size.square(16),
@ -25,7 +27,6 @@ final ToolbarItem inlineMathEquationItem = ToolbarItem(
), ),
isHighlight: isHighlight, isHighlight: isHighlight,
highlightColor: highlightColor, highlightColor: highlightColor,
tooltip: LocaleKeys.document_plugins_createInlineMathEquation.tr(),
onPressed: () async { onPressed: () async {
final selection = editorState.selection; final selection = editorState.selection;
if (selection == null || selection.isCollapsed) { if (selection == null || selection.isCollapsed) {
@ -71,5 +72,16 @@ final ToolbarItem inlineMathEquationItem = ToolbarItem(
await editorState.apply(transaction); await editorState.apply(transaction);
}, },
); );
if (tooltipBuilder != null) {
return tooltipBuilder(
context,
_kInlineMathEquationToolbarItemId,
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
child,
);
}
return child;
}, },
); );

View File

@ -11,12 +11,15 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
const _kSmartEditToolbarItemId = 'appflowy.editor.smart_edit';
final ToolbarItem smartEditItem = ToolbarItem( final ToolbarItem smartEditItem = ToolbarItem(
id: 'appflowy.editor.smart_edit', id: _kSmartEditToolbarItemId,
group: 0, group: 0,
isActive: onlyShowInSingleSelectionAndTextType, isActive: onlyShowInSingleSelectionAndTextType,
builder: (context, editorState, _, __) => SmartEditActionList( builder: (context, editorState, _, __, tooltipBuilder) => SmartEditActionList(
editorState: editorState, editorState: editorState,
tooltipBuilder: tooltipBuilder,
), ),
); );
@ -24,9 +27,11 @@ class SmartEditActionList extends StatefulWidget {
const SmartEditActionList({ const SmartEditActionList({
super.key, super.key,
required this.editorState, required this.editorState,
this.tooltipBuilder,
}); });
final EditorState editorState; final EditorState editorState;
final ToolbarTooltipBuilder? tooltipBuilder;
@override @override
State<SmartEditActionList> createState() => _SmartEditActionListState(); State<SmartEditActionList> createState() => _SmartEditActionListState();
@ -60,11 +65,8 @@ class _SmartEditActionListState extends State<SmartEditActionList> {
onClosed: () => keepEditorFocusNotifier.decrease(), onClosed: () => keepEditorFocusNotifier.decrease(),
buildChild: (controller) { buildChild: (controller) {
keepEditorFocusNotifier.increase(); keepEditorFocusNotifier.increase();
return FlowyIconButton( final child = FlowyIconButton(
hoverColor: Colors.transparent, hoverColor: Colors.transparent,
tooltipText: isAIEnabled
? LocaleKeys.document_plugins_smartEdit.tr()
: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),
preferBelow: false, preferBelow: false,
icon: const Icon( icon: const Icon(
Icons.lightbulb_outline, Icons.lightbulb_outline,
@ -83,6 +85,19 @@ class _SmartEditActionListState extends State<SmartEditActionList> {
} }
}, },
); );
if (widget.tooltipBuilder != null) {
return widget.tooltipBuilder!(
context,
_kSmartEditToolbarItemId,
isAIEnabled
? LocaleKeys.document_plugins_smartEdit.tr()
: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),
child,
);
}
return child;
}, },
onSelected: (action, controller) { onSelected: (action, controller) {
controller.close(); controller.close();

View File

@ -1,4 +1,7 @@
import 'dart:io';
import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/font_colors.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/font_colors.dart';
@ -13,7 +16,10 @@ import 'package:appflowy/workspace/application/settings/appearance/appearance_cu
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -394,4 +400,83 @@ class EditorStyleCustomizer {
after, after,
); );
} }
Widget buildToolbarItemTooltip(
BuildContext context,
String id,
String message,
Widget child,
) {
final tooltipMessage = _buildTooltipMessage(id, message);
child = FlowyTooltip(
richMessage: tooltipMessage,
preferBelow: false,
verticalOffset: 20,
child: child,
);
// the align/font toolbar item doesn't need the hover effect
final toolbarItemsWithoutHover = {
kFontToolbarItemId,
kAlignToolbarItemId,
};
if (!toolbarItemsWithoutHover.contains(id)) {
child = Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: FlowyHover(
style: HoverStyle(
hoverColor: Colors.grey.withOpacity(0.3),
),
child: child,
),
);
}
return child;
}
TextSpan _buildTooltipMessage(String id, String message) {
final markdownItemTooltips = {
'underline': (LocaleKeys.toolbar_underline.tr(), 'U'),
'bold': (LocaleKeys.toolbar_bold.tr(), 'B'),
'italic': (LocaleKeys.toolbar_italic.tr(), 'I'),
'strikethrough': (LocaleKeys.toolbar_strike.tr(), 'Shift+S'),
'code': (LocaleKeys.toolbar_inlineCode.tr(), 'E'),
};
final markdownItemIds = markdownItemTooltips.keys.toSet();
// the items without shortcuts
if (!markdownItemIds.contains(id)) {
return TextSpan(
text: message,
style: context.tooltipTextStyle(),
);
}
final tooltip = markdownItemTooltips[id];
if (tooltip == null) {
return TextSpan(
text: message,
style: context.tooltipTextStyle(),
);
}
final textSpan = TextSpan(
children: [
TextSpan(
text: '${tooltip.$1}\n',
style: context.tooltipTextStyle(),
),
TextSpan(
text: (Platform.isMacOS ? '⌘+' : 'Ctrl+\\') + tooltip.$2,
style: context
.tooltipTextStyle()
?.copyWith(color: Theme.of(context).hintColor),
),
],
);
return textSpan;
}
} }

View File

@ -0,0 +1,26 @@
import 'package:appflowy/util/theme_extension.dart';
import 'package:flutter/material.dart';
extension PickerColors on BuildContext {
Color get pickerTextColor {
return Theme.of(this).isLightMode
? const Color(0x80171717)
: Colors.white.withOpacity(0.5);
}
Color get pickerIconColor {
return Theme.of(this).isLightMode ? const Color(0xFF171717) : Colors.white;
}
Color get pickerSearchBarBorderColor {
return Theme.of(this).isLightMode
? const Color(0x1E171717)
: Colors.white.withOpacity(0.12);
}
Color get pickerButtonBoarderColor {
return Theme.of(this).isLightMode
? const Color(0x1E171717)
: Colors.white.withOpacity(0.12);
}
}

View File

@ -1,12 +1,14 @@
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/plugins/base/emoji/emoji_skin_tone.dart'; import 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.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:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'colors.dart';
typedef EmojiKeywordChangedCallback = void Function(String keyword); typedef EmojiKeywordChangedCallback = void Function(String keyword);
typedef EmojiSkinToneChanged = void Function(EmojiSkinTone skinTone); typedef EmojiSkinToneChanged = void Function(EmojiSkinTone skinTone);
@ -82,7 +84,7 @@ class _RandomEmojiButton extends StatelessWidget {
height: 36, height: 36,
decoration: ShapeDecoration( decoration: ShapeDecoration(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
side: const BorderSide(color: Color(0x1E171717)), side: BorderSide(color: context.pickerButtonBoarderColor),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
), ),
@ -141,7 +143,7 @@ class _SearchTextFieldState extends State<_SearchTextField> {
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: Theme.of(context).hintColor, color: Theme.of(context).hintColor,
), ),
enableBorderColor: const Color(0x1E171717), enableBorderColor: context.pickerSearchBarBorderColor,
controller: controller, controller: controller,
onChanged: widget.onKeywordChanged, onChanged: widget.onKeywordChanged,
prefixIcon: const Padding( prefixIcon: const Padding(

View File

@ -5,6 +5,8 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'colors.dart';
// use a temporary global value to store last selected skin tone // use a temporary global value to store last selected skin tone
EmojiSkinTone? lastSelectedEmojiSkinTone; EmojiSkinTone? lastSelectedEmojiSkinTone;
@ -68,7 +70,7 @@ class _FlowyEmojiSkinToneSelectorState
width: 36, width: 36,
height: 36, height: 36,
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: const Color(0x1E171717)), border: Border.all(color: context.pickerButtonBoarderColor),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: FlowyButton( child: FlowyButton(

View File

@ -13,6 +13,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart' hide Icon; import 'package:flutter/material.dart' hide Icon;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'colors.dart';
import 'icon_color_picker.dart'; import 'icon_color_picker.dart';
// cache the icon groups to avoid loading them multiple times // cache the icon groups to avoid loading them multiple times
@ -200,7 +201,7 @@ class _IconPickerState extends State<IconPicker> {
iconGroup.displayName, iconGroup.displayName,
fontSize: 12, fontSize: 12,
figmaLineHeight: 18.0, figmaLineHeight: 18.0,
color: const Color(0x80171717), color: context.pickerTextColor,
), ),
const VSpace(4.0), const VSpace(4.0),
Wrap( Wrap(
@ -252,7 +253,7 @@ class _Icon extends StatelessWidget {
child: FlowySvg.string( child: FlowySvg.string(
icon.content, icon.content,
size: const Size.square(20), size: const Size.square(20),
color: const Color(0xFF171717), color: context.pickerIconColor,
opacity: 0.7, opacity: 0.7,
), ),
), ),

View File

@ -6,6 +6,8 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'colors.dart';
typedef IconKeywordChangedCallback = void Function(String keyword); typedef IconKeywordChangedCallback = void Function(String keyword);
typedef EmojiSkinToneChanged = void Function(EmojiSkinTone skinTone); typedef EmojiSkinToneChanged = void Function(EmojiSkinTone skinTone);
@ -70,7 +72,7 @@ class _RandomIconButton extends StatelessWidget {
height: 36, height: 36,
decoration: ShapeDecoration( decoration: ShapeDecoration(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
side: const BorderSide(color: Color(0x1E171717)), side: BorderSide(color: context.pickerButtonBoarderColor),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
), ),
@ -123,7 +125,7 @@ class _SearchTextFieldState extends State<_SearchTextField> {
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: Theme.of(context).hintColor, color: Theme.of(context).hintColor,
), ),
enableBorderColor: const Color(0x1E171717), enableBorderColor: context.pickerSearchBarBorderColor,
controller: controller, controller: controller,
onChanged: widget.onKeywordChanged, onChanged: widget.onKeywordChanged,
prefixIcon: const Padding( prefixIcon: const Padding(

View File

@ -10,6 +10,7 @@ class FlowyTooltip extends StatelessWidget {
this.preferBelow, this.preferBelow,
this.showDuration, this.showDuration,
this.margin, this.margin,
this.verticalOffset,
this.child, this.child,
}); });
@ -19,6 +20,7 @@ class FlowyTooltip extends StatelessWidget {
final Duration? showDuration; final Duration? showDuration;
final EdgeInsetsGeometry? margin; final EdgeInsetsGeometry? margin;
final Widget? child; final Widget? child;
final double? verticalOffset;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -28,16 +30,14 @@ class FlowyTooltip extends StatelessWidget {
return Tooltip( return Tooltip(
margin: margin, margin: margin,
verticalOffset: 16.0, verticalOffset: verticalOffset ?? 16.0,
padding: const EdgeInsets.only( padding: const EdgeInsets.symmetric(
left: 12.0, horizontal: 12.0,
right: 12.0, vertical: 8.0,
top: 5.0,
bottom: 8.0,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.tooltipBackgroundColor(), color: context.tooltipBackgroundColor(),
borderRadius: BorderRadius.circular(12.0), borderRadius: BorderRadius.circular(10.0),
), ),
waitDuration: _tooltipWaitDuration, waitDuration: _tooltipWaitDuration,
message: message, message: message,
@ -50,7 +50,7 @@ class FlowyTooltip extends StatelessWidget {
} }
extension FlowyToolTipExtension on BuildContext { extension FlowyToolTipExtension on BuildContext {
double tooltipFontSize() => 13.0; double tooltipFontSize() => 14.0;
double tooltipHeight() => 18.0 / tooltipFontSize(); double tooltipHeight() => 18.0 / tooltipFontSize();
Color tooltipFontColor() => Theme.of(this).brightness == Brightness.light Color tooltipFontColor() => Theme.of(this).brightness == Brightness.light
? Colors.white ? Colors.white

View File

@ -53,8 +53,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: c064543 ref: "4536488faf458ab45e304c1715850d4d1ae517ee"
resolved-ref: c064543e0e40f862f1db2db36725d48465a8aea5 resolved-ref: "4536488faf458ab45e304c1715850d4d1ae517ee"
url: "https://github.com/AppFlowy-IO/appflowy-editor.git" url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git source: git
version: "3.1.0" version: "3.1.0"

View File

@ -199,7 +199,7 @@ dependency_overrides:
appflowy_editor: appflowy_editor:
git: git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: "c064543" ref: "4536488faf458ab45e304c1715850d4d1ae517ee"
appflowy_editor_plugins: appflowy_editor_plugins:
git: git: