Merge pull request #866 from AppFlowy-IO/feat/shortcut_escape_to_exit_overlay

chore: add shortcut to exit the FlowyOverlay
This commit is contained in:
Nathan.fooo 2022-08-17 16:30:29 +08:00 committed by GitHub
commit 6dfed4a7b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 135 additions and 43 deletions

View File

@ -24,14 +24,16 @@ class GridCellShortcuts extends StatelessWidget {
return Shortcuts(
shortcuts: {
LogicalKeySet(LogicalKeyboardKey.enter): const GridCellEnterIdent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyC): const GridCellCopyIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyV): const GridCellInsertIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyC):
const GridCellCopyIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyV):
const GridCellPasteIntent(),
},
child: Actions(
actions: {
GridCellEnterIdent: GridCellEnterAction(child: child),
GridCellCopyIntent: GridCellCopyAction(child: child),
GridCellInsertIntent: GridCellInsertAction(child: child),
GridCellPasteIntent: GridCellPasteAction(child: child),
},
child: child,
),
@ -78,16 +80,16 @@ class GridCellCopyAction extends Action<GridCellCopyIntent> {
}
}
class GridCellInsertIntent extends Intent {
const GridCellInsertIntent();
class GridCellPasteIntent extends Intent {
const GridCellPasteIntent();
}
class GridCellInsertAction extends Action<GridCellInsertIntent> {
class GridCellPasteAction extends Action<GridCellPasteIntent> {
final CellShortcuts child;
GridCellInsertAction({required this.child});
GridCellPasteAction({required this.child});
@override
void invoke(covariant GridCellInsertIntent intent) {
void invoke(covariant GridCellPasteIntent intent) {
final callback = child.shortcutHandlers[CellKeyboardKey.onInsert];
if (callback != null) {
callback();

View File

@ -53,7 +53,8 @@ class OverlayScreen extends StatelessWidget {
title: const Text('Overlay Demo'),
),
body: ChangeNotifierProvider(
create: (context) => OverlayDemoConfiguration(AnchorDirection.rightWithTopAligned, OverlapBehaviour.stretch),
create: (context) => OverlayDemoConfiguration(
AnchorDirection.rightWithTopAligned, OverlapBehaviour.stretch),
child: Builder(builder: (providerContext) {
return Center(
child: ConstrainedBox(
@ -77,7 +78,8 @@ class OverlayScreen extends StatelessWidget {
child: GestureDetector(
// ignore: avoid_print
onTapDown: (_) => print('Hello Flutter'),
child: const Center(child: FlutterLogo(size: 100)),
child:
const Center(child: FlutterLogo(size: 100)),
),
),
),
@ -90,26 +92,38 @@ class OverlayScreen extends StatelessWidget {
),
const SizedBox(height: 24.0),
DropdownButton<AnchorDirection>(
value: providerContext.watch<OverlayDemoConfiguration>().anchorDirection,
value: providerContext
.watch<OverlayDemoConfiguration>()
.anchorDirection,
onChanged: (AnchorDirection? newValue) {
if (newValue != null) {
providerContext.read<OverlayDemoConfiguration>().anchorDirection = newValue;
providerContext
.read<OverlayDemoConfiguration>()
.anchorDirection = newValue;
}
},
items: AnchorDirection.values.map((AnchorDirection classType) {
return DropdownMenuItem<AnchorDirection>(value: classType, child: Text(classType.toString()));
items:
AnchorDirection.values.map((AnchorDirection classType) {
return DropdownMenuItem<AnchorDirection>(
value: classType, child: Text(classType.toString()));
}).toList(),
),
const SizedBox(height: 24.0),
DropdownButton<OverlapBehaviour>(
value: providerContext.watch<OverlayDemoConfiguration>().overlapBehaviour,
value: providerContext
.watch<OverlayDemoConfiguration>()
.overlapBehaviour,
onChanged: (OverlapBehaviour? newValue) {
if (newValue != null) {
providerContext.read<OverlayDemoConfiguration>().overlapBehaviour = newValue;
providerContext
.read<OverlayDemoConfiguration>()
.overlapBehaviour = newValue;
}
},
items: OverlapBehaviour.values.map((OverlapBehaviour classType) {
return DropdownMenuItem<OverlapBehaviour>(value: classType, child: Text(classType.toString()));
items: OverlapBehaviour.values
.map((OverlapBehaviour classType) {
return DropdownMenuItem<OverlapBehaviour>(
value: classType, child: Text(classType.toString()));
}).toList(),
),
const SizedBox(height: 24.0),
@ -127,15 +141,20 @@ class OverlayScreen extends StatelessWidget {
child: GestureDetector(
// ignore: avoid_print
onTapDown: (_) => print('Hello Flutter'),
child: const Center(child: FlutterLogo(size: 50)),
child: const Center(
child: FlutterLogo(size: 50)),
),
),
),
identifier: 'overlay_anchored_card',
delegate: null,
anchorContext: buttonContext,
anchorDirection: providerContext.read<OverlayDemoConfiguration>().anchorDirection,
overlapBehaviour: providerContext.read<OverlayDemoConfiguration>().overlapBehaviour,
anchorDirection: providerContext
.read<OverlayDemoConfiguration>()
.anchorDirection,
overlapBehaviour: providerContext
.read<OverlayDemoConfiguration>()
.overlapBehaviour,
);
},
child: const Text('Show Anchored Overlay'),
@ -155,7 +174,8 @@ class OverlayScreen extends StatelessWidget {
child: GestureDetector(
// ignore: avoid_print
onTapDown: (_) => debugPrint('Hello Flutter'),
child: const Center(child: FlutterLogo(size: 100)),
child:
const Center(child: FlutterLogo(size: 100)),
),
),
),
@ -163,8 +183,12 @@ class OverlayScreen extends StatelessWidget {
delegate: null,
anchorPosition: Offset(0, windowSize.height - 200),
anchorSize: Size.zero,
anchorDirection: providerContext.read<OverlayDemoConfiguration>().anchorDirection,
overlapBehaviour: providerContext.read<OverlayDemoConfiguration>().overlapBehaviour,
anchorDirection: providerContext
.read<OverlayDemoConfiguration>()
.anchorDirection,
overlapBehaviour: providerContext
.read<OverlayDemoConfiguration>()
.overlapBehaviour,
);
},
child: const Text('Show Positioned Overlay'),
@ -176,18 +200,24 @@ class OverlayScreen extends StatelessWidget {
ListOverlay.showWithAnchor(
context,
itemBuilder: (_, index) => Card(
margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
margin: const EdgeInsets.symmetric(
vertical: 8.0, horizontal: 12.0),
elevation: 0,
child: Text(
'Option $index',
style: const TextStyle(fontSize: 20.0, color: Colors.black),
style: const TextStyle(
fontSize: 20.0, color: Colors.black),
),
),
itemCount: 10,
identifier: 'overlay_list_menu',
anchorContext: buttonContext,
anchorDirection: providerContext.read<OverlayDemoConfiguration>().anchorDirection,
overlapBehaviour: providerContext.read<OverlayDemoConfiguration>().overlapBehaviour,
anchorDirection: providerContext
.read<OverlayDemoConfiguration>()
.anchorDirection,
overlapBehaviour: providerContext
.read<OverlayDemoConfiguration>()
.overlapBehaviour,
width: 200.0,
height: 200.0,
);
@ -201,13 +231,28 @@ class OverlayScreen extends StatelessWidget {
onPressed: () {
OptionOverlay.showWithAnchor(
context,
items: <String>['Alpha', 'Beta', 'Charlie', 'Delta', 'Echo', 'Foxtrot', 'Golf', 'Hotel'],
onHover: (value, index) => debugPrint('Did hover option $index, value $value'),
onTap: (value, index) => debugPrint('Did tap option $index, value $value'),
items: <String>[
'Alpha',
'Beta',
'Charlie',
'Delta',
'Echo',
'Foxtrot',
'Golf',
'Hotel'
],
onHover: (value, index) => debugPrint(
'Did hover option $index, value $value'),
onTap: (value, index) =>
debugPrint('Did tap option $index, value $value'),
identifier: 'overlay_options',
anchorContext: buttonContext,
anchorDirection: providerContext.read<OverlayDemoConfiguration>().anchorDirection,
overlapBehaviour: providerContext.read<OverlayDemoConfiguration>().overlapBehaviour,
anchorDirection: providerContext
.read<OverlayDemoConfiguration>()
.anchorDirection,
overlapBehaviour: providerContext
.read<OverlayDemoConfiguration>()
.overlapBehaviour,
);
},
child: const Text('Show Options Overlay'),

View File

@ -3,6 +3,7 @@
import 'dart:ui';
import 'package:flowy_infra_ui/src/flowy_overlay/layout.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
export './overlay_container.dart';
/// Specifies how overlay are anchored to the SourceWidget
@ -59,7 +60,8 @@ class FlowyOverlayStyle {
final Color barrierColor;
bool blur;
FlowyOverlayStyle({this.barrierColor = Colors.transparent, this.blur = false});
FlowyOverlayStyle(
{this.barrierColor = Colors.transparent, this.blur = false});
}
final GlobalKey<FlowyOverlayState> _key = GlobalKey<FlowyOverlayState>();
@ -82,7 +84,8 @@ class FlowyOverlay extends StatefulWidget {
final Widget child;
static FlowyOverlayState of(BuildContext context, {bool rootOverlay = false}) {
static FlowyOverlayState of(BuildContext context,
{bool rootOverlay = false}) {
FlowyOverlayState? state = maybeOf(context, rootOverlay: rootOverlay);
assert(() {
if (state == null) {
@ -95,7 +98,8 @@ class FlowyOverlay extends StatefulWidget {
return state!;
}
static FlowyOverlayState? maybeOf(BuildContext context, {bool rootOverlay = false}) {
static FlowyOverlayState? maybeOf(BuildContext context,
{bool rootOverlay = false}) {
FlowyOverlayState? state;
if (rootOverlay) {
state = context.findRootAncestorStateOfType<FlowyOverlayState>();
@ -113,20 +117,29 @@ class OverlayItem {
Widget widget;
String identifier;
FlowyOverlayDelegate? delegate;
FocusNode focusNode;
OverlayItem({
required this.widget,
required this.identifier,
required this.focusNode,
this.delegate,
});
void dispose() {
focusNode.dispose();
}
}
class FlowyOverlayState extends State<FlowyOverlay> {
final List<OverlayItem> _overlayList = [];
FlowyOverlayStyle style = FlowyOverlayStyle();
final Map<ShortcutActivator, void Function(String)>
_keyboardShortcutBindings = {};
/// Insert a overlay widget which frame is set by the widget, not the component.
/// Be sure to specify the offset and size using a anchorable widget (like `Postition`, `CompositedTransformFollower`)
/// Be sure to specify the offset and size using a anchorable widget (like `Position`, `CompositedTransformFollower`)
void insertCustom({
required Widget widget,
required String identifier,
@ -192,9 +205,12 @@ class FlowyOverlayState extends State<FlowyOverlay> {
void remove(String identifier) {
setState(() {
final index = _overlayList.indexWhere((item) => item.identifier == identifier);
final index =
_overlayList.indexWhere((item) => item.identifier == identifier);
if (index != -1) {
_overlayList.removeAt(index).delegate?.didRemove();
final OverlayItem item = _overlayList.removeAt(index);
item.delegate?.didRemove();
item.dispose();
}
});
}
@ -210,6 +226,7 @@ class FlowyOverlayState extends State<FlowyOverlay> {
_overlayList.remove(firstItem);
if (firstItem.delegate != null) {
firstItem.delegate!.didRemove();
firstItem.dispose();
if (firstItem.delegate!.asBarrier()) {
return;
}
@ -220,6 +237,7 @@ class FlowyOverlayState extends State<FlowyOverlay> {
return;
} else {
element.delegate?.didRemove();
element.dispose();
_overlayList.remove(element);
}
}
@ -247,7 +265,7 @@ class FlowyOverlayState extends State<FlowyOverlay> {
debugPrint("Show overlay: $identifier");
Widget overlay = widget;
final offset = anchorOffset ?? Offset.zero;
final focusNode = FocusNode();
if (shouldAnchor) {
assert(
anchorPosition != null || anchorContext != null,
@ -259,7 +277,7 @@ class FlowyOverlayState extends State<FlowyOverlay> {
RenderObject renderObject = anchorContext.findRenderObject()!;
assert(
renderObject is RenderBox,
'Unexpect non-RenderBox render object caught.',
'Unexpected non-RenderBox render object caught.',
);
final renderBox = renderObject as RenderBox;
targetAnchorPosition = renderBox.localToGlobal(Offset.zero);
@ -271,13 +289,28 @@ class FlowyOverlayState extends State<FlowyOverlay> {
targetAnchorSize.width,
targetAnchorSize.height,
);
overlay = CustomSingleChildLayout(
delegate: OverlayLayoutDelegate(
anchorRect: anchorRect,
anchorDirection: anchorDirection ?? AnchorDirection.rightWithTopAligned,
anchorDirection:
anchorDirection ?? AnchorDirection.rightWithTopAligned,
overlapBehaviour: overlapBehaviour ?? OverlapBehaviour.stretch,
),
child: widget,
child: Focus(
focusNode: focusNode,
onKey: (node, event) {
KeyEventResult result = KeyEventResult.ignored;
for (final ShortcutActivator activator
in _keyboardShortcutBindings.keys) {
if (activator.accepts(event, RawKeyboard.instance)) {
_keyboardShortcutBindings[activator]!.call(identifier);
result = KeyEventResult.handled;
}
}
return result;
},
child: widget),
);
}
@ -285,15 +318,27 @@ class FlowyOverlayState extends State<FlowyOverlay> {
_overlayList.add(OverlayItem(
widget: overlay,
identifier: identifier,
focusNode: focusNode,
delegate: delegate,
));
});
}
@override
void initState() {
_keyboardShortcutBindings.addAll({
LogicalKeySet(LogicalKeyboardKey.escape): (identifier) {
remove(identifier);
},
});
super.initState();
}
@override
Widget build(BuildContext context) {
final overlays = _overlayList.map((item) {
var widget = item.widget;
item.focusNode.requestFocus();
if (item.delegate?.asBarrier() ?? false) {
widget = Container(
color: style.barrierColor,