mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: implement selectable popup list widget
This commit is contained in:
parent
1166d03b75
commit
58d656d9f4
@ -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);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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');
|
||||||
|
Loading…
Reference in New Issue
Block a user