diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_shortcuts.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_shortcuts.dart index f4f222f219..f44a4b5663 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_shortcuts.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_shortcuts.dart @@ -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 { } } -class GridCellInsertIntent extends Intent { - const GridCellInsertIntent(); +class GridCellPasteIntent extends Intent { + const GridCellPasteIntent(); } -class GridCellInsertAction extends Action { +class GridCellPasteAction extends Action { 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(); diff --git a/frontend/app_flowy/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart b/frontend/app_flowy/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart index 24274999db..cea870a017 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart @@ -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( - value: providerContext.watch().anchorDirection, + value: providerContext + .watch() + .anchorDirection, onChanged: (AnchorDirection? newValue) { if (newValue != null) { - providerContext.read().anchorDirection = newValue; + providerContext + .read() + .anchorDirection = newValue; } }, - items: AnchorDirection.values.map((AnchorDirection classType) { - return DropdownMenuItem(value: classType, child: Text(classType.toString())); + items: + AnchorDirection.values.map((AnchorDirection classType) { + return DropdownMenuItem( + value: classType, child: Text(classType.toString())); }).toList(), ), const SizedBox(height: 24.0), DropdownButton( - value: providerContext.watch().overlapBehaviour, + value: providerContext + .watch() + .overlapBehaviour, onChanged: (OverlapBehaviour? newValue) { if (newValue != null) { - providerContext.read().overlapBehaviour = newValue; + providerContext + .read() + .overlapBehaviour = newValue; } }, - items: OverlapBehaviour.values.map((OverlapBehaviour classType) { - return DropdownMenuItem(value: classType, child: Text(classType.toString())); + items: OverlapBehaviour.values + .map((OverlapBehaviour classType) { + return DropdownMenuItem( + 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().anchorDirection, - overlapBehaviour: providerContext.read().overlapBehaviour, + anchorDirection: providerContext + .read() + .anchorDirection, + overlapBehaviour: providerContext + .read() + .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().anchorDirection, - overlapBehaviour: providerContext.read().overlapBehaviour, + anchorDirection: providerContext + .read() + .anchorDirection, + overlapBehaviour: providerContext + .read() + .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().anchorDirection, - overlapBehaviour: providerContext.read().overlapBehaviour, + anchorDirection: providerContext + .read() + .anchorDirection, + overlapBehaviour: providerContext + .read() + .overlapBehaviour, width: 200.0, height: 200.0, ); @@ -201,13 +231,28 @@ class OverlayScreen extends StatelessWidget { onPressed: () { OptionOverlay.showWithAnchor( context, - items: ['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: [ + '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().anchorDirection, - overlapBehaviour: providerContext.read().overlapBehaviour, + anchorDirection: providerContext + .read() + .anchorDirection, + overlapBehaviour: providerContext + .read() + .overlapBehaviour, ); }, child: const Text('Show Options Overlay'), diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart index 7ed3c04673..ad04dc25c2 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart @@ -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 _key = GlobalKey(); @@ -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(); @@ -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 { final List _overlayList = []; FlowyOverlayStyle style = FlowyOverlayStyle(); + final Map + _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 { 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 { _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 { return; } else { element.delegate?.didRemove(); + element.dispose(); _overlayList.remove(element); } } @@ -247,7 +265,7 @@ class FlowyOverlayState extends State { 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 { 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 { 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 { _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,