mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
commit
6dfed4a7b0
@ -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();
|
||||
|
@ -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'),
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user