|
@@ -14,43 +14,56 @@ import 'package:flutter/services.dart';
|
|
final List<PopupListItem> _popupListItems = [
|
|
final List<PopupListItem> _popupListItems = [
|
|
PopupListItem(
|
|
PopupListItem(
|
|
text: 'Text',
|
|
text: 'Text',
|
|
|
|
+ keywords: ['text'],
|
|
icon: _popupListIcon('text'),
|
|
icon: _popupListIcon('text'),
|
|
- handler: (editorState) => formatText(editorState),
|
|
|
|
|
|
+ handler: (editorState) {
|
|
|
|
+ insertTextNodeAfterSelection(editorState, {});
|
|
|
|
+ },
|
|
),
|
|
),
|
|
PopupListItem(
|
|
PopupListItem(
|
|
text: 'Heading 1',
|
|
text: 'Heading 1',
|
|
|
|
+ keywords: ['h1', 'heading 1'],
|
|
icon: _popupListIcon('h1'),
|
|
icon: _popupListIcon('h1'),
|
|
- handler: (editorState) => formatHeading(editorState, StyleKey.h1),
|
|
|
|
|
|
+ handler: (editorState) =>
|
|
|
|
+ insertHeadingAfterSelection(editorState, StyleKey.h1),
|
|
),
|
|
),
|
|
PopupListItem(
|
|
PopupListItem(
|
|
text: 'Heading 2',
|
|
text: 'Heading 2',
|
|
|
|
+ keywords: ['h2', 'heading 2'],
|
|
icon: _popupListIcon('h2'),
|
|
icon: _popupListIcon('h2'),
|
|
- handler: (editorState) => formatHeading(editorState, StyleKey.h2),
|
|
|
|
|
|
+ handler: (editorState) =>
|
|
|
|
+ insertHeadingAfterSelection(editorState, StyleKey.h2),
|
|
),
|
|
),
|
|
PopupListItem(
|
|
PopupListItem(
|
|
text: 'Heading 3',
|
|
text: 'Heading 3',
|
|
|
|
+ keywords: ['h3', 'heading 3'],
|
|
icon: _popupListIcon('h3'),
|
|
icon: _popupListIcon('h3'),
|
|
- handler: (editorState) => formatHeading(editorState, StyleKey.h3),
|
|
|
|
|
|
+ handler: (editorState) =>
|
|
|
|
+ insertHeadingAfterSelection(editorState, StyleKey.h3),
|
|
),
|
|
),
|
|
PopupListItem(
|
|
PopupListItem(
|
|
- text: 'Bullets',
|
|
|
|
|
|
+ text: 'Bulleted List',
|
|
|
|
+ keywords: ['bulleted list'],
|
|
icon: _popupListIcon('bullets'),
|
|
icon: _popupListIcon('bullets'),
|
|
- handler: (editorState) => formatBulletedList(editorState),
|
|
|
|
|
|
+ handler: (editorState) => insertBulletedListAfterSelection(editorState),
|
|
),
|
|
),
|
|
|
|
+ // PopupListItem(
|
|
|
|
+ // text: 'Numbered list',
|
|
|
|
+ // keywords: ['numbered list'],
|
|
|
|
+ // icon: _popupListIcon('number'),
|
|
|
|
+ // handler: (editorState) => debugPrint('Not implement yet!'),
|
|
|
|
+ // ),
|
|
PopupListItem(
|
|
PopupListItem(
|
|
- text: 'Numbered list',
|
|
|
|
- icon: _popupListIcon('number'),
|
|
|
|
- handler: (editorState) => debugPrint('Not implement yet!'),
|
|
|
|
- ),
|
|
|
|
- PopupListItem(
|
|
|
|
- text: 'Checkboxes',
|
|
|
|
|
|
+ text: 'To-do List',
|
|
|
|
+ keywords: ['checkbox', 'todo'],
|
|
icon: _popupListIcon('checkbox'),
|
|
icon: _popupListIcon('checkbox'),
|
|
- handler: (editorState) => formatCheckbox(editorState),
|
|
|
|
|
|
+ handler: (editorState) => insertCheckboxAfterSelection(editorState),
|
|
),
|
|
),
|
|
];
|
|
];
|
|
|
|
|
|
OverlayEntry? _popupListOverlay;
|
|
OverlayEntry? _popupListOverlay;
|
|
EditorState? _editorState;
|
|
EditorState? _editorState;
|
|
|
|
+bool _selectionChangeBySlash = false;
|
|
FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
|
|
FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
|
|
if (event.logicalKey != LogicalKeyboardKey.slash) {
|
|
if (event.logicalKey != LogicalKeyboardKey.slash) {
|
|
return KeyEventResult.ignored;
|
|
return KeyEventResult.ignored;
|
|
@@ -69,21 +82,19 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
|
|
if (selection == null || context == null || selectable == null) {
|
|
if (selection == null || context == null || selectable == null) {
|
|
return KeyEventResult.ignored;
|
|
return KeyEventResult.ignored;
|
|
}
|
|
}
|
|
-
|
|
|
|
- final rect = selectable.getCursorRectInPosition(selection.start);
|
|
|
|
- if (rect == null) {
|
|
|
|
|
|
+ final selectionRects = editorState.service.selectionService.selectionRects;
|
|
|
|
+ if (selectionRects.isEmpty) {
|
|
return KeyEventResult.ignored;
|
|
return KeyEventResult.ignored;
|
|
}
|
|
}
|
|
- final offset = selectable.localToGlobal(rect.topLeft);
|
|
|
|
-
|
|
|
|
TransactionBuilder(editorState)
|
|
TransactionBuilder(editorState)
|
|
..replaceText(textNode, selection.start.offset,
|
|
..replaceText(textNode, selection.start.offset,
|
|
- selection.end.offset - selection.start.offset, '/')
|
|
|
|
|
|
+ selection.end.offset - selection.start.offset, event.character ?? '')
|
|
..commit();
|
|
..commit();
|
|
|
|
|
|
_editorState = editorState;
|
|
_editorState = editorState;
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
- showPopupList(context, editorState, offset);
|
|
|
|
|
|
+ _selectionChangeBySlash = false;
|
|
|
|
+ showPopupList(context, editorState, selectionRects.first.bottomRight);
|
|
});
|
|
});
|
|
|
|
|
|
return KeyEventResult.handled;
|
|
return KeyEventResult.handled;
|
|
@@ -94,8 +105,8 @@ void showPopupList(
|
|
_popupListOverlay?.remove();
|
|
_popupListOverlay?.remove();
|
|
_popupListOverlay = OverlayEntry(
|
|
_popupListOverlay = OverlayEntry(
|
|
builder: (context) => Positioned(
|
|
builder: (context) => Positioned(
|
|
- top: offset.dy + 15.0,
|
|
|
|
- left: offset.dx + 5.0,
|
|
|
|
|
|
+ top: offset.dy,
|
|
|
|
+ left: offset.dx,
|
|
child: PopupListWidget(
|
|
child: PopupListWidget(
|
|
editorState: editorState,
|
|
editorState: editorState,
|
|
items: _popupListItems,
|
|
items: _popupListItems,
|
|
@@ -117,6 +128,15 @@ void clearPopupList() {
|
|
if (_popupListOverlay == null || _editorState == null) {
|
|
if (_popupListOverlay == null || _editorState == null) {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
|
|
+ final selection =
|
|
|
|
+ _editorState?.service.selectionService.currentSelection.value;
|
|
|
|
+ if (selection == null) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ if (_selectionChangeBySlash) {
|
|
|
|
+ _selectionChangeBySlash = false;
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
_popupListOverlay?.remove();
|
|
_popupListOverlay?.remove();
|
|
_popupListOverlay = null;
|
|
_popupListOverlay = null;
|
|
|
|
|
|
@@ -142,21 +162,55 @@ class PopupListWidget extends StatefulWidget {
|
|
}
|
|
}
|
|
|
|
|
|
class _PopupListWidgetState extends State<PopupListWidget> {
|
|
class _PopupListWidgetState extends State<PopupListWidget> {
|
|
- final focusNode = FocusNode(debugLabel: 'popup_list_widget');
|
|
|
|
- var selectedIndex = 0;
|
|
|
|
|
|
+ final _focusNode = FocusNode(debugLabel: 'popup_list_widget');
|
|
|
|
+ int _selectedIndex = 0;
|
|
|
|
+ List<PopupListItem> _items = [];
|
|
|
|
+
|
|
|
|
+ int _maxKeywordLength = 0;
|
|
|
|
+
|
|
|
|
+ String __keyword = '';
|
|
|
|
+ String get _keyword => __keyword;
|
|
|
|
+ set _keyword(String keyword) {
|
|
|
|
+ __keyword = keyword;
|
|
|
|
+
|
|
|
|
+ final items = widget.items
|
|
|
|
+ .where((item) =>
|
|
|
|
+ item.keywords.any((keyword) => keyword.contains(_keyword)))
|
|
|
|
+ .toList(growable: false);
|
|
|
|
+ if (items.isNotEmpty) {
|
|
|
|
+ var maxKeywordLength = 0;
|
|
|
|
+ for (var item in _items) {
|
|
|
|
+ for (var keyword in item.keywords) {
|
|
|
|
+ maxKeywordLength = max(maxKeywordLength, keyword.length);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ _maxKeywordLength = maxKeywordLength;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (keyword.length >= _maxKeywordLength + 2) {
|
|
|
|
+ clearPopupList();
|
|
|
|
+ } else {
|
|
|
|
+ setState(() {
|
|
|
|
+ _selectedIndex = 0;
|
|
|
|
+ _items = items;
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
|
|
@override
|
|
@override
|
|
void initState() {
|
|
void initState() {
|
|
super.initState();
|
|
super.initState();
|
|
|
|
|
|
|
|
+ _items = widget.items;
|
|
|
|
+
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
- focusNode.requestFocus();
|
|
|
|
|
|
+ _focusNode.requestFocus();
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
@override
|
|
@override
|
|
void dispose() {
|
|
void dispose() {
|
|
- focusNode.dispose();
|
|
|
|
|
|
+ _focusNode.dispose();
|
|
|
|
|
|
super.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|
|
@@ -164,7 +218,7 @@ class _PopupListWidgetState extends State<PopupListWidget> {
|
|
@override
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
Widget build(BuildContext context) {
|
|
return Focus(
|
|
return Focus(
|
|
- focusNode: focusNode,
|
|
|
|
|
|
+ focusNode: _focusNode,
|
|
onKey: _onKey,
|
|
onKey: _onKey,
|
|
child: Container(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
decoration: BoxDecoration(
|
|
@@ -178,9 +232,26 @@ class _PopupListWidgetState extends State<PopupListWidget> {
|
|
],
|
|
],
|
|
borderRadius: BorderRadius.circular(6.0),
|
|
borderRadius: BorderRadius.circular(6.0),
|
|
),
|
|
),
|
|
- child: Row(
|
|
|
|
- crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
- children: _buildColumns(widget.items, selectedIndex),
|
|
|
|
|
|
+ child: _items.isEmpty
|
|
|
|
+ ? _buildNoResultsWidget(context)
|
|
|
|
+ : Row(
|
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
+ children: _buildColumns(_items, _selectedIndex),
|
|
|
|
+ ),
|
|
|
|
+ ),
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ Widget _buildNoResultsWidget(BuildContext context) {
|
|
|
|
+ return const Align(
|
|
|
|
+ alignment: Alignment.centerLeft,
|
|
|
|
+ child: Material(
|
|
|
|
+ child: Padding(
|
|
|
|
+ padding: EdgeInsets.all(12.0),
|
|
|
|
+ child: Text(
|
|
|
|
+ 'No results',
|
|
|
|
+ style: TextStyle(color: Colors.grey),
|
|
|
|
+ ),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
);
|
|
@@ -214,26 +285,43 @@ class _PopupListWidgetState extends State<PopupListWidget> {
|
|
}
|
|
}
|
|
|
|
|
|
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
|
|
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
|
|
|
|
+ debugPrint('slash on key $event');
|
|
if (event is! RawKeyDownEvent) {
|
|
if (event is! RawKeyDownEvent) {
|
|
return KeyEventResult.ignored;
|
|
return KeyEventResult.ignored;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ final arrowKeys = [
|
|
|
|
+ LogicalKeyboardKey.arrowLeft,
|
|
|
|
+ LogicalKeyboardKey.arrowRight,
|
|
|
|
+ LogicalKeyboardKey.arrowUp,
|
|
|
|
+ LogicalKeyboardKey.arrowDown
|
|
|
|
+ ];
|
|
|
|
+
|
|
if (event.logicalKey == LogicalKeyboardKey.enter) {
|
|
if (event.logicalKey == LogicalKeyboardKey.enter) {
|
|
- if (0 <= selectedIndex && selectedIndex < widget.items.length) {
|
|
|
|
- _deleteSlash();
|
|
|
|
- widget.items[selectedIndex].handler(widget.editorState);
|
|
|
|
|
|
+ if (0 <= _selectedIndex && _selectedIndex < _items.length) {
|
|
|
|
+ _deleteLastCharacters(length: _keyword.length + 1);
|
|
|
|
+ _items[_selectedIndex].handler(widget.editorState);
|
|
return KeyEventResult.handled;
|
|
return KeyEventResult.handled;
|
|
}
|
|
}
|
|
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
|
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
|
clearPopupList();
|
|
clearPopupList();
|
|
return KeyEventResult.handled;
|
|
return KeyEventResult.handled;
|
|
} else if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
|
} else if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
|
- clearPopupList();
|
|
|
|
- _deleteSlash();
|
|
|
|
|
|
+ if (_keyword.isEmpty) {
|
|
|
|
+ clearPopupList();
|
|
|
|
+ } else {
|
|
|
|
+ _keyword = _keyword.substring(0, _keyword.length - 1);
|
|
|
|
+ }
|
|
|
|
+ _deleteLastCharacters();
|
|
|
|
+ return KeyEventResult.handled;
|
|
|
|
+ } else if (event.character != null &&
|
|
|
|
+ !arrowKeys.contains(event.logicalKey)) {
|
|
|
|
+ _keyword += event.character!;
|
|
|
|
+ _insertText(event.character!);
|
|
return KeyEventResult.handled;
|
|
return KeyEventResult.handled;
|
|
}
|
|
}
|
|
|
|
|
|
- var newSelectedIndex = selectedIndex;
|
|
|
|
|
|
+ var newSelectedIndex = _selectedIndex;
|
|
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
|
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
|
newSelectedIndex -= widget.maxItemInRow;
|
|
newSelectedIndex -= widget.maxItemInRow;
|
|
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
|
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
|
@@ -243,26 +331,44 @@ class _PopupListWidgetState extends State<PopupListWidget> {
|
|
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
|
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
|
newSelectedIndex += 1;
|
|
newSelectedIndex += 1;
|
|
}
|
|
}
|
|
- if (newSelectedIndex != selectedIndex) {
|
|
|
|
|
|
+ if (newSelectedIndex != _selectedIndex) {
|
|
setState(() {
|
|
setState(() {
|
|
- selectedIndex = max(0, min(widget.items.length - 1, newSelectedIndex));
|
|
|
|
|
|
+ _selectedIndex = max(0, min(_items.length - 1, newSelectedIndex));
|
|
});
|
|
});
|
|
return KeyEventResult.handled;
|
|
return KeyEventResult.handled;
|
|
}
|
|
}
|
|
return KeyEventResult.ignored;
|
|
return KeyEventResult.ignored;
|
|
}
|
|
}
|
|
|
|
|
|
- void _deleteSlash() {
|
|
|
|
|
|
+ void _deleteLastCharacters({int length = 1}) {
|
|
final selection =
|
|
final selection =
|
|
widget.editorState.service.selectionService.currentSelection.value;
|
|
widget.editorState.service.selectionService.currentSelection.value;
|
|
final nodes =
|
|
final nodes =
|
|
widget.editorState.service.selectionService.currentSelectedNodes;
|
|
widget.editorState.service.selectionService.currentSelectedNodes;
|
|
if (selection != null && nodes.length == 1) {
|
|
if (selection != null && nodes.length == 1) {
|
|
|
|
+ _selectionChangeBySlash = true;
|
|
TransactionBuilder(widget.editorState)
|
|
TransactionBuilder(widget.editorState)
|
|
..deleteText(
|
|
..deleteText(
|
|
nodes.first as TextNode,
|
|
nodes.first as TextNode,
|
|
- selection.start.offset - 1,
|
|
|
|
- 1,
|
|
|
|
|
|
+ selection.start.offset - length,
|
|
|
|
+ length,
|
|
|
|
+ )
|
|
|
|
+ ..commit();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ void _insertText(String text) {
|
|
|
|
+ final selection =
|
|
|
|
+ widget.editorState.service.selectionService.currentSelection.value;
|
|
|
|
+ final nodes =
|
|
|
|
+ widget.editorState.service.selectionService.currentSelectedNodes;
|
|
|
|
+ if (selection != null && nodes.length == 1) {
|
|
|
|
+ _selectionChangeBySlash = true;
|
|
|
|
+ TransactionBuilder(widget.editorState)
|
|
|
|
+ ..insertText(
|
|
|
|
+ nodes.first as TextNode,
|
|
|
|
+ selection.end.offset,
|
|
|
|
+ text,
|
|
)
|
|
)
|
|
..commit();
|
|
..commit();
|
|
}
|
|
}
|
|
@@ -318,12 +424,14 @@ class _PopupListItemWidget extends StatelessWidget {
|
|
class PopupListItem {
|
|
class PopupListItem {
|
|
PopupListItem({
|
|
PopupListItem({
|
|
required this.text,
|
|
required this.text,
|
|
|
|
+ required this.keywords,
|
|
this.message = '',
|
|
this.message = '',
|
|
required this.icon,
|
|
required this.icon,
|
|
required this.handler,
|
|
required this.handler,
|
|
});
|
|
});
|
|
|
|
|
|
final String text;
|
|
final String text;
|
|
|
|
+ final List<String> keywords;
|
|
final String message;
|
|
final String message;
|
|
final Widget icon;
|
|
final Widget icon;
|
|
final void Function(EditorState editorState) handler;
|
|
final void Function(EditorState editorState) handler;
|