Browse Source

Merge pull request #532 from AppFlowy-IO/feat/grid_keyboard_shortcut

Feat/grid keyboard shortcut
Nathan.fooo 2 years ago
parent
commit
fbf7c9c9b8

+ 4 - 0
frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_bloc.dart

@@ -24,6 +24,9 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
               url: cellData?.url ?? "",
             ));
           },
+          updateURL: (String url) {
+            cellContext.saveCellData(url, deduplicate: true);
+          },
         );
       },
     );
@@ -53,6 +56,7 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
 @freezed
 class URLCellEvent with _$URLCellEvent {
   const factory URLCellEvent.initial() = _InitialCell;
+  const factory URLCellEvent.updateURL(String url) = _UpdateURL;
   const factory URLCellEvent.didReceiveCellUpdate(URLCellData? cell) = _DidReceiveCellUpdate;
 }
 

+ 2 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart

@@ -15,6 +15,7 @@ import 'layout/sizes.dart';
 import 'widgets/row/grid_row.dart';
 import 'widgets/footer/grid_footer.dart';
 import 'widgets/header/grid_header.dart';
+import 'widgets/shortcuts.dart';
 import 'widgets/toolbar/grid_toolbar.dart';
 
 class GridPage extends StatefulWidget {
@@ -40,7 +41,7 @@ class _GridPageState extends State<GridPage> {
           return state.loadingState.map(
             loading: (_) => const Center(child: CircularProgressIndicator.adaptive()),
             finish: (result) => result.successOrFail.fold(
-              (_) => const FlowyGrid(),
+              (_) => const GridShortcuts(child: FlowyGrid()),
               (err) => FlowyErrorPage(err.toString()),
             ),
           );

+ 3 - 3
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_accessory.dart

@@ -18,8 +18,8 @@ abstract class GridCellAccessory implements Widget {
 
 typedef AccessoryBuilder = List<GridCellAccessory> Function(GridCellAccessoryBuildContext buildContext);
 
-abstract class AccessoryWidget extends Widget {
-  const AccessoryWidget({Key? key}) : super(key: key);
+abstract class CellAccessory extends Widget {
+  const CellAccessory({Key? key}) : super(key: key);
 
   // The hover will show if the onFocus's value is true
   ValueNotifier<bool>? get isFocus;
@@ -28,7 +28,7 @@ abstract class AccessoryWidget extends Widget {
 }
 
 class AccessoryHover extends StatefulWidget {
-  final AccessoryWidget child;
+  final CellAccessory child;
   final EdgeInsets contentPadding;
   const AccessoryHover({
     required this.child,

+ 30 - 11
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart

@@ -1,5 +1,6 @@
 import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_service.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show FieldType;
+import 'package:flutter/services.dart';
 import 'package:flutter/widgets.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart';
 import 'package:flowy_infra/theme.dart';
@@ -8,6 +9,7 @@ import 'package:provider/provider.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
 import 'package:styled_widget/styled_widget.dart';
 import 'cell_accessory.dart';
+import 'cell_shortcuts.dart';
 import 'checkbox_cell.dart';
 import 'date_cell/date_cell.dart';
 import 'number_cell.dart';
@@ -48,7 +50,7 @@ class BlankCell extends StatelessWidget {
   }
 }
 
-abstract class GridCellWidget extends StatefulWidget implements AccessoryWidget, CellContainerFocustable {
+abstract class GridCellWidget extends StatefulWidget implements CellAccessory, CellFocustable, CellShortcuts {
   GridCellWidget({Key? key}) : super(key: key);
 
   @override
@@ -58,31 +60,47 @@ abstract class GridCellWidget extends StatefulWidget implements AccessoryWidget,
   List<GridCellAccessory> Function(GridCellAccessoryBuildContext buildContext)? get accessoryBuilder => null;
 
   @override
-  final GridCellRequestBeginFocus requestBeginFocus = GridCellRequestBeginFocus();
+  final GridCellFocusListener beginFocus = GridCellFocusListener();
+
+  @override
+  final Map<CellKeyboardKey, CellKeyboardAction> shortcutHandlers = {};
 }
 
 abstract class GridCellState<T extends GridCellWidget> extends State<T> {
   @override
   void initState() {
-    widget.requestBeginFocus.setListener(() => requestBeginFocus());
+    widget.beginFocus.setListener(() => requestBeginFocus());
+    widget.shortcutHandlers[CellKeyboardKey.onCopy] = () => onCopy();
+    widget.shortcutHandlers[CellKeyboardKey.onInsert] = () {
+      Clipboard.getData("text/plain").then((data) {
+        final s = data?.text;
+        if (s is String) {
+          onInsert(s);
+        }
+      });
+    };
     super.initState();
   }
 
   @override
   void didUpdateWidget(covariant T oldWidget) {
     if (oldWidget != this) {
-      widget.requestBeginFocus.setListener(() => requestBeginFocus());
+      widget.beginFocus.setListener(() => requestBeginFocus());
     }
     super.didUpdateWidget(oldWidget);
   }
 
   @override
   void dispose() {
-    widget.requestBeginFocus.removeAllListener();
+    widget.beginFocus.removeAllListener();
     super.dispose();
   }
 
   void requestBeginFocus();
+
+  String? onCopy() => null;
+
+  void onInsert(String value) {}
 }
 
 abstract class GridFocusNodeCellState<T extends GridCellWidget> extends GridCellState<T> {
@@ -90,6 +108,7 @@ abstract class GridFocusNodeCellState<T extends GridCellWidget> extends GridCell
 
   @override
   void initState() {
+    widget.shortcutHandlers[CellKeyboardKey.onEnter] = () => focusNode.unfocus();
     _listenOnFocusNodeChanged();
     super.initState();
   }
@@ -104,6 +123,7 @@ abstract class GridFocusNodeCellState<T extends GridCellWidget> extends GridCell
 
   @override
   void dispose() {
+    widget.shortcutHandlers.clear();
     focusNode.removeAllListener();
     focusNode.dispose();
     super.dispose();
@@ -127,7 +147,7 @@ abstract class GridFocusNodeCellState<T extends GridCellWidget> extends GridCell
   Future<void> focusChanged() async {}
 }
 
-class GridCellRequestBeginFocus extends ChangeNotifier {
+class GridCellFocusListener extends ChangeNotifier {
   VoidCallback? _listener;
 
   void setListener(VoidCallback listener) {
@@ -194,9 +214,8 @@ class CellStateNotifier extends ChangeNotifier {
   bool get onEnter => _onEnter;
 }
 
-abstract class CellContainerFocustable {
-  // Listen on the requestBeginFocus if the
-  GridCellRequestBeginFocus get requestBeginFocus;
+abstract class CellFocustable {
+  GridCellFocusListener get beginFocus;
 }
 
 class CellContainer extends StatelessWidget {
@@ -220,7 +239,7 @@ class CellContainer extends StatelessWidget {
       child: Selector<CellStateNotifier, bool>(
         selector: (context, notifier) => notifier.isFocus,
         builder: (context, isFocus, _) {
-          Widget container = Center(child: child);
+          Widget container = Center(child: GridCellShortcuts(child: child));
           child.isFocus.addListener(() {
             Provider.of<CellStateNotifier>(context, listen: false).isFocus = child.isFocus.value;
           });
@@ -235,7 +254,7 @@ class CellContainer extends StatelessWidget {
 
           return GestureDetector(
             behavior: HitTestBehavior.translucent,
-            onTap: () => child.requestBeginFocus.notify(),
+            onTap: () => child.beginFocus.notify(),
             child: Container(
               constraints: BoxConstraints(maxWidth: width, minHeight: 46),
               decoration: _makeBoxDecoration(context, isFocus),

+ 96 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_shortcuts.dart

@@ -0,0 +1,96 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+typedef CellKeyboardAction = dynamic Function();
+
+enum CellKeyboardKey {
+  onEnter,
+  onCopy,
+  onInsert,
+}
+
+abstract class CellShortcuts extends Widget {
+  const CellShortcuts({Key? key}) : super(key: key);
+
+  Map<CellKeyboardKey, CellKeyboardAction> get shortcutHandlers;
+}
+
+class GridCellShortcuts extends StatelessWidget {
+  final CellShortcuts child;
+  const GridCellShortcuts({required this.child, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Shortcuts(
+      shortcuts: {
+        LogicalKeySet(LogicalKeyboardKey.enter): const GridCellEnterIdent(),
+        LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyC): const GridCellCopyIntent(),
+        LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyV): const GridCellInsertIntent(),
+      },
+      child: Actions(
+        actions: {
+          GridCellEnterIdent: GridCellEnterAction(child: child),
+          GridCellCopyIntent: GridCellCopyAction(child: child),
+          GridCellInsertIntent: GridCellInsertAction(child: child),
+        },
+        child: child,
+      ),
+    );
+  }
+}
+
+class GridCellEnterIdent extends Intent {
+  const GridCellEnterIdent();
+}
+
+class GridCellEnterAction extends Action<GridCellEnterIdent> {
+  final CellShortcuts child;
+  GridCellEnterAction({required this.child});
+
+  @override
+  void invoke(covariant GridCellEnterIdent intent) {
+    final callback = child.shortcutHandlers[CellKeyboardKey.onEnter];
+    if (callback != null) {
+      callback();
+    }
+  }
+}
+
+class GridCellCopyIntent extends Intent {
+  const GridCellCopyIntent();
+}
+
+class GridCellCopyAction extends Action<GridCellCopyIntent> {
+  final CellShortcuts child;
+  GridCellCopyAction({required this.child});
+
+  @override
+  void invoke(covariant GridCellCopyIntent intent) {
+    final callback = child.shortcutHandlers[CellKeyboardKey.onCopy];
+    if (callback == null) {
+      return;
+    }
+
+    final s = callback();
+    if (s is String) {
+      Clipboard.setData(ClipboardData(text: s));
+    }
+  }
+}
+
+class GridCellInsertIntent extends Intent {
+  const GridCellInsertIntent();
+}
+
+class GridCellInsertAction extends Action<GridCellInsertIntent> {
+  final CellShortcuts child;
+  GridCellInsertAction({required this.child});
+
+  @override
+  void invoke(covariant GridCellInsertIntent intent) {
+    final callback = child.shortcutHandlers[CellKeyboardKey.onInsert];
+    if (callback != null) {
+      callback();
+    }
+  }
+}

+ 9 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/checkbox_cell.dart

@@ -24,7 +24,6 @@ class _CheckboxCellState extends GridCellState<CheckboxCell> {
   void initState() {
     final cellContext = widget.cellContextBuilder.build();
     _cellBloc = getIt<CheckboxCellBloc>(param1: cellContext)..add(const CheckboxCellEvent.initial());
-
     super.initState();
   }
 
@@ -59,4 +58,13 @@ class _CheckboxCellState extends GridCellState<CheckboxCell> {
   void requestBeginFocus() {
     _cellBloc.add(const CheckboxCellEvent.select());
   }
+
+  @override
+  String? onCopy() {
+    if (_cellBloc.state.isSelected) {
+      return "Yes";
+    } else {
+      return "No";
+    }
+  }
 }

+ 8 - 2
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/date_cell.dart

@@ -35,10 +35,10 @@ class DateCell extends GridCellWidget {
   }
 
   @override
-  State<DateCell> createState() => _DateCellState();
+  GridCellState<DateCell> createState() => _DateCellState();
 }
 
-class _DateCellState extends State<DateCell> {
+class _DateCellState extends GridCellState<DateCell> {
   late DateCellBloc _cellBloc;
 
   @override
@@ -89,4 +89,10 @@ class _DateCellState extends State<DateCell> {
     _cellBloc.close();
     super.dispose();
   }
+
+  @override
+  void requestBeginFocus() {}
+
+  @override
+  String? onCopy() => _cellBloc.state.dateStr;
 }

+ 10 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/number_cell.dart

@@ -1,5 +1,4 @@
 import 'dart:async';
-
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/grid/prelude.dart';
 import 'package:flutter/material.dart';
@@ -81,4 +80,14 @@ class _NumberCellState extends GridFocusNodeCellState<NumberCell> {
   String contentFromState(NumberCellState state) {
     return state.content.fold((l) => l, (r) => "");
   }
+
+  @override
+  String? onCopy() {
+    return _cellBloc.state.content.fold((content) => content, (r) => null);
+  }
+
+  @override
+  void onInsert(String value) {
+    _cellBloc.add(NumberCellEvent.updateCell(value));
+  }
 }

+ 8 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart

@@ -92,4 +92,12 @@ class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
       });
     }
   }
+
+  @override
+  String? onCopy() => _cellBloc.state.content;
+
+  @override
+  void onInsert(String value) {
+    _cellBloc.add(TextCellEvent.updateText(value));
+  }
 }

+ 9 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/cell_editor.dart

@@ -8,7 +8,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 
 class URLCellEditor extends StatefulWidget with FlowyOverlayDelegate {
   final GridURLCellContext cellContext;
-  const URLCellEditor({required this.cellContext, Key? key}) : super(key: key);
+  final VoidCallback completed;
+  const URLCellEditor({required this.cellContext, required this.completed, Key? key}) : super(key: key);
 
   @override
   State<URLCellEditor> createState() => _URLCellEditorState();
@@ -16,10 +17,12 @@ class URLCellEditor extends StatefulWidget with FlowyOverlayDelegate {
   static void show(
     BuildContext context,
     GridURLCellContext cellContext,
+    VoidCallback completed,
   ) {
     FlowyOverlay.of(context).remove(identifier());
     final editor = URLCellEditor(
       cellContext: cellContext,
+      completed: completed,
     );
 
     //
@@ -46,6 +49,11 @@ class URLCellEditor extends StatefulWidget with FlowyOverlayDelegate {
   bool asBarrier() {
     return true;
   }
+
+  @override
+  void didRemove() {
+    completed();
+  }
 }
 
 class _URLCellEditorState extends State<URLCellEditor> {

+ 13 - 2
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/url_cell.dart

@@ -131,10 +131,13 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
   Future<void> _openUrlOrEdit(String url) async {
     final uri = Uri.parse(url);
     if (url.isNotEmpty && await canLaunchUrl(uri)) {
+      widget.isFocus.value = false;
       await launchUrl(uri);
     } else {
       final cellContext = widget.cellContextBuilder.build() as GridURLCellContext;
-      URLCellEditor.show(context, cellContext);
+      URLCellEditor.show(context, cellContext, () {
+        widget.isFocus.value = false;
+      });
     }
   }
 
@@ -142,6 +145,14 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
   void requestBeginFocus() {
     _openUrlOrEdit(_cellBloc.state.url);
   }
+
+  @override
+  String? onCopy() => _cellBloc.state.content;
+
+  @override
+  void onInsert(String value) {
+    _cellBloc.add(URLCellEvent.updateURL(value));
+  }
 }
 
 class _EditURLAccessory extends StatelessWidget with GridCellAccessory {
@@ -161,7 +172,7 @@ class _EditURLAccessory extends StatelessWidget with GridCellAccessory {
 
   @override
   void onTap() {
-    URLCellEditor.show(anchorContext, cellContext);
+    URLCellEditor.show(anchorContext, cellContext, () {});
   }
 }
 

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_detail.dart

@@ -154,7 +154,7 @@ class _RowDetailCell extends StatelessWidget {
 
     final gesture = GestureDetector(
       behavior: HitTestBehavior.translucent,
-      onTap: () => cell.requestBeginFocus.notify(),
+      onTap: () => cell.beginFocus.notify(),
       child: AccessoryHover(
         child: cell,
         contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12),

+ 58 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/shortcuts.dart

@@ -0,0 +1,58 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+class GridShortcuts extends StatelessWidget {
+  final Widget child;
+  const GridShortcuts({required this.child, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Shortcuts(
+      shortcuts: bindKeys([]),
+      child: Actions(
+        dispatcher: LoggingActionDispatcher(),
+        actions: const {},
+        child: child,
+      ),
+    );
+  }
+}
+
+Map<ShortcutActivator, Intent> bindKeys(List<LogicalKeyboardKey> keys) {
+  return {for (var key in keys) LogicalKeySet(key): KeyboardKeyIdent(key)};
+}
+
+Map<Type, Action<Intent>> bindActions() {
+  return {
+    KeyboardKeyIdent: KeyboardBindingAction(),
+  };
+}
+
+class KeyboardKeyIdent extends Intent {
+  final KeyboardKey key;
+
+  const KeyboardKeyIdent(this.key);
+}
+
+class KeyboardBindingAction extends Action<KeyboardKeyIdent> {
+  KeyboardBindingAction();
+
+  @override
+  void invoke(covariant KeyboardKeyIdent intent) {
+    // print(intent);
+  }
+}
+
+class LoggingActionDispatcher extends ActionDispatcher {
+  @override
+  Object? invokeAction(
+    covariant Action<Intent> action,
+    covariant Intent intent, [
+    BuildContext? context,
+  ]) {
+    // print('Action invoked: $action($intent) from $context');
+    super.invokeAction(action, intent, context);
+
+    return null;
+  }
+}