feat: implement selectable popup list widget

This commit is contained in:
Lucas.Xu 2022-08-03 22:12:09 +08:00
parent 1166d03b75
commit 58d656d9f4
3 changed files with 157 additions and 39 deletions

View File

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/editor_state.dart';
import 'package:flowy_editor/infra/flowy_svg.dart'; import 'package:flowy_editor/infra/flowy_svg.dart';
@ -47,9 +49,10 @@ final List<PopupListItem> _popupListItems = [
), ),
]; ];
OverlayEntry? popupListOverlay; OverlayEntry? _popupListOverlay;
EditorState? _editorState;
FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
if (event.logicalKey != LogicalKeyboardKey.slash && !event.isMetaPressed) { if (event.logicalKey != LogicalKeyboardKey.slash) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
@ -80,11 +83,11 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
..commit(); ..commit();
} }
popupListOverlay?.remove(); _popupListOverlay?.remove();
popupListOverlay = OverlayEntry( _popupListOverlay = OverlayEntry(
builder: (context) => Positioned( builder: (context) => Positioned(
top: offset.dy + 15.0, top: offset.dy + 15.0,
left: offset.dx, left: offset.dx + 5.0,
child: PopupListWidget( child: PopupListWidget(
editorState: editorState, editorState: editorState,
items: _popupListItems, items: _popupListItems,
@ -92,19 +95,24 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
), ),
); );
Overlay.of(context)?.insert(popupListOverlay!); Overlay.of(context)?.insert(_popupListOverlay!);
editorState.service.selectionService.currentSelectedNodes editorState.service.selectionService.currentSelectedNodes
.removeListener(clearPopupListOverlay); .removeListener(clearPopupListOverlay);
editorState.service.selectionService.currentSelectedNodes editorState.service.selectionService.currentSelectedNodes
.addListener(clearPopupListOverlay); .addListener(clearPopupListOverlay);
// editorState.service.keyboardService?.disable();
_editorState = editorState;
return KeyEventResult.handled; return KeyEventResult.handled;
}; };
void clearPopupListOverlay() { void clearPopupListOverlay() {
popupListOverlay?.remove(); _popupListOverlay?.remove();
popupListOverlay = null; _popupListOverlay = null;
_editorState?.service.keyboardService?.enable();
_editorState = null;
} }
class PopupListWidget extends StatefulWidget { class PopupListWidget extends StatefulWidget {
@ -112,7 +120,7 @@ class PopupListWidget extends StatefulWidget {
Key? key, Key? key,
required this.editorState, required this.editorState,
required this.items, required this.items,
this.maxItemInRow = 8, this.maxItemInRow = 5,
}) : super(key: key); }) : super(key: key);
final EditorState editorState; final EditorState editorState;
@ -124,32 +132,57 @@ class PopupListWidget extends StatefulWidget {
} }
class _PopupListWidgetState extends State<PopupListWidget> { class _PopupListWidgetState extends State<PopupListWidget> {
final focusNode = FocusNode(debugLabel: 'popup_list_widget');
var selectedIndex = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
focusNode.requestFocus();
});
}
@override
void dispose() {
focusNode.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( // TODO: Is there a better way to get focus?
decoration: BoxDecoration(
color: Colors.white, return Focus(
boxShadow: [ focusNode: focusNode,
BoxShadow( onKey: _onKey,
blurRadius: 5, child: Container(
spreadRadius: 1, decoration: BoxDecoration(
color: Colors.black.withOpacity(0.1), color: Colors.white,
), boxShadow: [
], BoxShadow(
borderRadius: BorderRadius.circular(6.0), blurRadius: 5,
), spreadRadius: 1,
child: Row( color: Colors.black.withOpacity(0.1),
crossAxisAlignment: CrossAxisAlignment.start, ),
children: _buildColumns(widget.items), ],
borderRadius: BorderRadius.circular(6.0),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildColumns(widget.items, selectedIndex),
),
), ),
); );
} }
List<Widget> _buildColumns(List<PopupListItem> items) { List<Widget> _buildColumns(List<PopupListItem> items, int selectedIndex) {
List<Widget> columns = []; List<Widget> columns = [];
List<Widget> itemWidgets = []; List<Widget> itemWidgets = [];
for (var i = 0; i < items.length; i++) { for (var i = 0; i < items.length; i++) {
if (i != 0 && i % (widget.maxItemInRow - 1) == 0) { if (i != 0 && i % (widget.maxItemInRow) == 0) {
columns.add(Column( columns.add(Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: itemWidgets, children: itemWidgets,
@ -157,7 +190,10 @@ class _PopupListWidgetState extends State<PopupListWidget> {
itemWidgets = []; itemWidgets = [];
} }
itemWidgets.add(_PopupListItemWidget( itemWidgets.add(_PopupListItemWidget(
editorState: widget.editorState, item: items[i])); editorState: widget.editorState,
item: items[i],
highlight: selectedIndex == i,
));
} }
if (itemWidgets.isNotEmpty) { if (itemWidgets.isNotEmpty) {
columns.add(Column( columns.add(Column(
@ -168,35 +204,78 @@ class _PopupListWidgetState extends State<PopupListWidget> {
} }
return columns; return columns;
} }
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
if (event is! RawKeyDownEvent) {
return KeyEventResult.ignored;
}
if (event.logicalKey == LogicalKeyboardKey.enter) {
widget.items[selectedIndex].handler(widget.editorState);
return KeyEventResult.handled;
}
var newSelectedIndex = selectedIndex;
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
newSelectedIndex -= widget.maxItemInRow;
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
newSelectedIndex += widget.maxItemInRow;
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
newSelectedIndex -= 1;
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
newSelectedIndex += 1;
}
if (newSelectedIndex != selectedIndex) {
setState(() {
selectedIndex = max(0, min(widget.items.length - 1, newSelectedIndex));
});
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
} }
class _PopupListItemWidget extends StatelessWidget { class _PopupListItemWidget extends StatelessWidget {
const _PopupListItemWidget({ const _PopupListItemWidget({
Key? key, Key? key,
required this.highlight,
required this.item, required this.item,
required this.editorState, required this.editorState,
}) : super(key: key); }) : super(key: key);
final EditorState editorState; final EditorState editorState;
final PopupListItem item; final PopupListItem item;
final bool highlight;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: const EdgeInsets.fromLTRB(8.0, 5.0, 8.0, 5.0), padding: const EdgeInsets.fromLTRB(8.0, 5.0, 8.0, 5.0),
child: TextButton.icon( child: SizedBox(
icon: item.icon, width: 140,
label: Text( child: TextButton.icon(
item.text, icon: item.icon,
textAlign: TextAlign.left, style: ButtonStyle(
style: const TextStyle( alignment: Alignment.centerLeft,
color: Colors.black, overlayColor: MaterialStateProperty.all(
fontSize: 14.0, const Color(0xFFE0F8FF),
),
backgroundColor: highlight
? MaterialStateProperty.all(const Color(0xFFE0F8FF))
: MaterialStateProperty.all(Colors.transparent),
), ),
label: Text(
item.text,
textAlign: TextAlign.left,
style: const TextStyle(
color: Colors.black,
fontSize: 14.0,
),
),
onPressed: () {
item.handler(editorState);
},
), ),
onPressed: () {
item.handler(editorState);
},
), ),
); );
} }

View File

@ -3,6 +3,11 @@ import 'package:flutter/services.dart';
import '../editor_state.dart'; import '../editor_state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
mixin FlowyKeyboardService<T extends StatefulWidget> on State<T> {
void enable();
void disable();
}
typedef FlowyKeyEventHandler = KeyEventResult Function( typedef FlowyKeyEventHandler = KeyEventResult Function(
EditorState editorState, EditorState editorState,
RawKeyEvent event, RawKeyEvent event,
@ -25,9 +30,12 @@ class FlowyKeyboard extends StatefulWidget {
State<FlowyKeyboard> createState() => _FlowyKeyboardState(); State<FlowyKeyboard> createState() => _FlowyKeyboardState();
} }
class _FlowyKeyboardState extends State<FlowyKeyboard> { class _FlowyKeyboardState extends State<FlowyKeyboard>
with FlowyKeyboardService {
final FocusNode focusNode = FocusNode(debugLabel: 'flowy_keyboard_service'); final FocusNode focusNode = FocusNode(debugLabel: 'flowy_keyboard_service');
bool isFocus = true;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Focus( return Focus(
@ -38,7 +46,30 @@ class _FlowyKeyboardState extends State<FlowyKeyboard> {
); );
} }
@override
void dispose() {
focusNode.dispose();
super.dispose();
}
@override
void enable() {
isFocus = true;
focusNode.requestFocus();
}
@override
void disable() {
isFocus = false;
focusNode.unfocus();
}
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) { KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
if (!isFocus) {
return KeyEventResult.ignored;
}
debugPrint('on keyboard event $event'); debugPrint('on keyboard event $event');
if (event is! RawKeyDownEvent) { if (event is! RawKeyDownEvent) {

View File

@ -1,3 +1,4 @@
import 'package:flowy_editor/service/keyboard_service.dart';
import 'package:flowy_editor/service/render_plugin_service.dart'; import 'package:flowy_editor/service/render_plugin_service.dart';
import 'package:flowy_editor/service/toolbar_service.dart'; import 'package:flowy_editor/service/toolbar_service.dart';
import 'package:flowy_editor/service/selection_service.dart'; import 'package:flowy_editor/service/selection_service.dart';
@ -14,6 +15,13 @@ class FlowyService {
// keyboard service // keyboard service
final keyboardServiceKey = GlobalKey(debugLabel: 'flowy_keyboard_service'); final keyboardServiceKey = GlobalKey(debugLabel: 'flowy_keyboard_service');
FlowyKeyboardService? get keyboardService {
if (keyboardServiceKey.currentState != null &&
keyboardServiceKey.currentState is FlowyKeyboardService) {
return keyboardServiceKey.currentState! as FlowyKeyboardService;
}
return null;
}
// input service // input service
final inputServiceKey = GlobalKey(debugLabel: 'flowy_input_service'); final inputServiceKey = GlobalKey(debugLabel: 'flowy_input_service');