feat: implement command + K to trigger link menu

This commit is contained in:
Lucas.Xu
2022-08-22 17:51:47 +08:00
parent 1f90f30274
commit 886c1f00e5
4 changed files with 49 additions and 12 deletions

View File

@ -21,12 +21,21 @@ class LinkMenu extends StatefulWidget {
class _LinkMenuState extends State<LinkMenu> { class _LinkMenuState extends State<LinkMenu> {
final _textEditingController = TextEditingController(); final _textEditingController = TextEditingController();
final _focusNode = FocusNode();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_textEditingController.text = widget.linkText ?? ''; _textEditingController.text = widget.linkText ?? '';
_focusNode.requestFocus();
}
@override
void dispose() {
_focusNode.dispose();
super.dispose();
} }
@override @override
@ -88,7 +97,7 @@ class _LinkMenuState extends State<LinkMenu> {
Widget _buildInput() { Widget _buildInput() {
return TextField( return TextField(
autofocus: true, focusNode: _focusNode,
style: const TextStyle(fontSize: 14.0), style: const TextStyle(fontSize: 14.0),
textAlign: TextAlign.left, textAlign: TextAlign.left,
controller: _textEditingController, controller: _textEditingController,
@ -112,11 +121,7 @@ class _LinkMenuState extends State<LinkMenu> {
required VoidCallback onPressed, required VoidCallback onPressed,
}) { }) {
return TextButton.icon( return TextButton.icon(
icon: FlowySvg( icon: FlowySvg(name: iconName),
name: iconName,
width: 20.0,
height: 20.0,
),
style: TextButton.styleFrom( style: TextButton.styleFrom(
minimumSize: const Size.fromHeight(40), minimumSize: const Size.fromHeight(40),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,

View File

@ -13,12 +13,14 @@ typedef ToolbarShowValidator = bool Function(EditorState editorState);
class ToolbarItem { class ToolbarItem {
ToolbarItem({ ToolbarItem({
required this.id,
required this.icon, required this.icon,
this.tooltipsMessage = '', this.tooltipsMessage = '',
required this.validator, required this.validator,
required this.handler, required this.handler,
}); });
final String id;
final Widget icon; final Widget icon;
final String tooltipsMessage; final String tooltipsMessage;
final ToolbarShowValidator validator; final ToolbarShowValidator validator;
@ -26,6 +28,7 @@ class ToolbarItem {
factory ToolbarItem.divider() { factory ToolbarItem.divider() {
return ToolbarItem( return ToolbarItem(
id: 'divider',
icon: const FlowySvg(name: 'toolbar/divider'), icon: const FlowySvg(name: 'toolbar/divider'),
validator: (editorState) => true, validator: (editorState) => true,
handler: (editorState, context) {}, handler: (editorState, context) {},
@ -35,18 +38,21 @@ class ToolbarItem {
List<ToolbarItem> defaultToolbarItems = [ List<ToolbarItem> defaultToolbarItems = [
ToolbarItem( ToolbarItem(
id: 'appflowy.toolbar.h1',
tooltipsMessage: 'Heading 1', tooltipsMessage: 'Heading 1',
icon: const FlowySvg(name: 'toolbar/h1'), icon: const FlowySvg(name: 'toolbar/h1'),
validator: _onlyShowInSingleTextSelection, validator: _onlyShowInSingleTextSelection,
handler: (editorState, context) => formatHeading(editorState, StyleKey.h1), handler: (editorState, context) => formatHeading(editorState, StyleKey.h1),
), ),
ToolbarItem( ToolbarItem(
id: 'appflowy.toolbar.h2',
tooltipsMessage: 'Heading 2', tooltipsMessage: 'Heading 2',
icon: const FlowySvg(name: 'toolbar/h2'), icon: const FlowySvg(name: 'toolbar/h2'),
validator: _onlyShowInSingleTextSelection, validator: _onlyShowInSingleTextSelection,
handler: (editorState, context) => formatHeading(editorState, StyleKey.h2), handler: (editorState, context) => formatHeading(editorState, StyleKey.h2),
), ),
ToolbarItem( ToolbarItem(
id: 'appflowy.toolbar.h3',
tooltipsMessage: 'Heading 3', tooltipsMessage: 'Heading 3',
icon: const FlowySvg(name: 'toolbar/h3'), icon: const FlowySvg(name: 'toolbar/h3'),
validator: _onlyShowInSingleTextSelection, validator: _onlyShowInSingleTextSelection,
@ -54,24 +60,28 @@ List<ToolbarItem> defaultToolbarItems = [
), ),
ToolbarItem.divider(), ToolbarItem.divider(),
ToolbarItem( ToolbarItem(
id: 'appflowy.toolbar.bold',
tooltipsMessage: 'Bold', tooltipsMessage: 'Bold',
icon: const FlowySvg(name: 'toolbar/bold'), icon: const FlowySvg(name: 'toolbar/bold'),
validator: _showInTextSelection, validator: _showInTextSelection,
handler: (editorState, context) => formatBold(editorState), handler: (editorState, context) => formatBold(editorState),
), ),
ToolbarItem( ToolbarItem(
id: 'appflowy.toolbar.italic',
tooltipsMessage: 'Italic', tooltipsMessage: 'Italic',
icon: const FlowySvg(name: 'toolbar/italic'), icon: const FlowySvg(name: 'toolbar/italic'),
validator: _showInTextSelection, validator: _showInTextSelection,
handler: (editorState, context) => formatItalic(editorState), handler: (editorState, context) => formatItalic(editorState),
), ),
ToolbarItem( ToolbarItem(
id: 'appflowy.toolbar.underline',
tooltipsMessage: 'Underline', tooltipsMessage: 'Underline',
icon: const FlowySvg(name: 'toolbar/underline'), icon: const FlowySvg(name: 'toolbar/underline'),
validator: _showInTextSelection, validator: _showInTextSelection,
handler: (editorState, context) => formatUnderline(editorState), handler: (editorState, context) => formatUnderline(editorState),
), ),
ToolbarItem( ToolbarItem(
id: 'appflowy.toolbar.strikethrough',
tooltipsMessage: 'Strikethrough', tooltipsMessage: 'Strikethrough',
icon: const FlowySvg(name: 'toolbar/strikethrough'), icon: const FlowySvg(name: 'toolbar/strikethrough'),
validator: _showInTextSelection, validator: _showInTextSelection,
@ -79,12 +89,14 @@ List<ToolbarItem> defaultToolbarItems = [
), ),
ToolbarItem.divider(), ToolbarItem.divider(),
ToolbarItem( ToolbarItem(
id: 'appflowy.toolbar.quote',
tooltipsMessage: 'Quote', tooltipsMessage: 'Quote',
icon: const FlowySvg(name: 'toolbar/quote'), icon: const FlowySvg(name: 'toolbar/quote'),
validator: _onlyShowInSingleTextSelection, validator: _onlyShowInSingleTextSelection,
handler: (editorState, context) => formatQuote(editorState), handler: (editorState, context) => formatQuote(editorState),
), ),
ToolbarItem( ToolbarItem(
id: 'appflowy.toolbar.bulleted_list',
tooltipsMessage: 'Bulleted list', tooltipsMessage: 'Bulleted list',
icon: const FlowySvg(name: 'toolbar/bulleted_list'), icon: const FlowySvg(name: 'toolbar/bulleted_list'),
validator: _onlyShowInSingleTextSelection, validator: _onlyShowInSingleTextSelection,
@ -92,12 +104,14 @@ List<ToolbarItem> defaultToolbarItems = [
), ),
ToolbarItem.divider(), ToolbarItem.divider(),
ToolbarItem( ToolbarItem(
id: 'appflowy.toolbar.link',
tooltipsMessage: 'Link', tooltipsMessage: 'Link',
icon: const FlowySvg(name: 'toolbar/link'), icon: const FlowySvg(name: 'toolbar/link'),
validator: _onlyShowInSingleTextSelection, validator: _onlyShowInSingleTextSelection,
handler: (editorState, context) => _showLinkMenu(editorState, context), handler: (editorState, context) => _showLinkMenu(editorState, context),
), ),
ToolbarItem( ToolbarItem(
id: 'appflowy.toolbar.highlight',
tooltipsMessage: 'Highlight', tooltipsMessage: 'Highlight',
icon: const FlowySvg(name: 'toolbar/highlight'), icon: const FlowySvg(name: 'toolbar/highlight'),
validator: _showInTextSelection, validator: _showInTextSelection,
@ -152,9 +166,7 @@ void _showLinkMenu(EditorState editorState, BuildContext context) {
linkText: linkText, linkText: linkText,
onSubmitted: (text) { onSubmitted: (text) {
TransactionBuilder(editorState) TransactionBuilder(editorState)
..formatText(node, index, length, { ..formatText(node, index, length, {StyleKey.href: text})
StyleKey.href: text,
})
..commit(); ..commit();
_dismissLinkMenu(); _dismissLinkMenu();
}, },
@ -164,9 +176,7 @@ void _showLinkMenu(EditorState editorState, BuildContext context) {
}, },
onRemoveLink: () { onRemoveLink: () {
TransactionBuilder(editorState) TransactionBuilder(editorState)
..formatText(node, index, length, { ..formatText(node, index, length, {StyleKey.href: null})
StyleKey.href: null,
})
..commit(); ..commit();
_dismissLinkMenu(); _dismissLinkMenu();
}, },
@ -177,6 +187,7 @@ void _showLinkMenu(EditorState editorState, BuildContext context) {
Overlay.of(context)?.insert(_linkMenuOverlay!); Overlay.of(context)?.insert(_linkMenuOverlay!);
editorState.service.scrollService?.disable(); editorState.service.scrollService?.disable();
editorState.service.keyboardService?.disable();
editorState.service.selectionService.currentSelection editorState.service.selectionService.currentSelection
.addListener(_dismissLinkMenu); .addListener(_dismissLinkMenu);
} }
@ -186,6 +197,7 @@ void _dismissLinkMenu() {
_linkMenuOverlay = null; _linkMenuOverlay = null;
_editorState?.service.scrollService?.enable(); _editorState?.service.scrollService?.enable();
_editorState?.service.keyboardService?.enable();
_editorState?.service.selectionService.currentSelection _editorState?.service.selectionService.currentSelection
.removeListener(_dismissLinkMenu); .removeListener(_dismissLinkMenu);
_editorState = null; _editorState = null;

View File

@ -36,6 +36,12 @@ AppFlowyKeyEventHandler updateTextStyleByCommandXHandler =
event.isShiftPressed) { event.isShiftPressed) {
formatHighlight(editorState); formatHighlight(editorState);
return KeyEventResult.handled; return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.keyK) {
if (editorState.service.toolbarService
?.triggerHandler('appflowy.toolbar.link') ==
true) {
return KeyEventResult.handled;
}
} }
return KeyEventResult.ignored; return KeyEventResult.ignored;

View File

@ -11,6 +11,9 @@ abstract class FlowyToolbarService {
/// Hide the toolbar widget. /// Hide the toolbar widget.
void hide(); void hide();
/// Trigger the specified handler.
bool triggerHandler(String id);
} }
class FlowyToolbar extends StatefulWidget { class FlowyToolbar extends StatefulWidget {
@ -55,6 +58,17 @@ class _FlowyToolbarState extends State<FlowyToolbar>
_toolbarOverlay = null; _toolbarOverlay = null;
} }
@override
bool triggerHandler(String id) {
final items = defaultToolbarItems.where((item) => item.id == id);
if (items.length != 1) {
assert(items.length == 1, 'The toolbar item\'s id must be unique');
return false;
}
items.first.handler(widget.editorState, context);
return true;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(