Pārlūkot izejas kodu

chore: add shortcut to exit the FlowyOverlay

appflowy 2 gadi atpakaļ
vecāks
revīzija
6c2dda1667

+ 10 - 8
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<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();

+ 70 - 25
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<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'),

+ 55 - 10
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<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,