Skip to content

Commit fbf7c9c

Browse files
authored
Merge pull request #532 from AppFlowy-IO/feat/grid_keyboard_shortcut
Feat/grid keyboard shortcut
2 parents 523caca + a9a7523 commit fbf7c9c

File tree

13 files changed

+251
-23
lines changed

13 files changed

+251
-23
lines changed

frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_bloc.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
2424
url: cellData?.url ?? "",
2525
));
2626
},
27+
updateURL: (String url) {
28+
cellContext.saveCellData(url, deduplicate: true);
29+
},
2730
);
2831
},
2932
);
@@ -53,6 +56,7 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
5356
@freezed
5457
class URLCellEvent with _$URLCellEvent {
5558
const factory URLCellEvent.initial() = _InitialCell;
59+
const factory URLCellEvent.updateURL(String url) = _UpdateURL;
5660
const factory URLCellEvent.didReceiveCellUpdate(URLCellData? cell) = _DidReceiveCellUpdate;
5761
}
5862

frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import 'layout/sizes.dart';
1515
import 'widgets/row/grid_row.dart';
1616
import 'widgets/footer/grid_footer.dart';
1717
import 'widgets/header/grid_header.dart';
18+
import 'widgets/shortcuts.dart';
1819
import 'widgets/toolbar/grid_toolbar.dart';
1920

2021
class GridPage extends StatefulWidget {
@@ -40,7 +41,7 @@ class _GridPageState extends State<GridPage> {
4041
return state.loadingState.map(
4142
loading: (_) => const Center(child: CircularProgressIndicator.adaptive()),
4243
finish: (result) => result.successOrFail.fold(
43-
(_) => const FlowyGrid(),
44+
(_) => const GridShortcuts(child: FlowyGrid()),
4445
(err) => FlowyErrorPage(err.toString()),
4546
),
4647
);

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ abstract class GridCellAccessory implements Widget {
1818

1919
typedef AccessoryBuilder = List<GridCellAccessory> Function(GridCellAccessoryBuildContext buildContext);
2020

21-
abstract class AccessoryWidget extends Widget {
22-
const AccessoryWidget({Key? key}) : super(key: key);
21+
abstract class CellAccessory extends Widget {
22+
const CellAccessory({Key? key}) : super(key: key);
2323

2424
// The hover will show if the onFocus's value is true
2525
ValueNotifier<bool>? get isFocus;
@@ -28,7 +28,7 @@ abstract class AccessoryWidget extends Widget {
2828
}
2929

3030
class AccessoryHover extends StatefulWidget {
31-
final AccessoryWidget child;
31+
final CellAccessory child;
3232
final EdgeInsets contentPadding;
3333
const AccessoryHover({
3434
required this.child,

frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_service.dart';
22
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show FieldType;
3+
import 'package:flutter/services.dart';
34
import 'package:flutter/widgets.dart';
45
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart';
56
import 'package:flowy_infra/theme.dart';
@@ -8,6 +9,7 @@ import 'package:provider/provider.dart';
89
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
910
import 'package:styled_widget/styled_widget.dart';
1011
import 'cell_accessory.dart';
12+
import 'cell_shortcuts.dart';
1113
import 'checkbox_cell.dart';
1214
import 'date_cell/date_cell.dart';
1315
import 'number_cell.dart';
@@ -48,7 +50,7 @@ class BlankCell extends StatelessWidget {
4850
}
4951
}
5052

51-
abstract class GridCellWidget extends StatefulWidget implements AccessoryWidget, CellContainerFocustable {
53+
abstract class GridCellWidget extends StatefulWidget implements CellAccessory, CellFocustable, CellShortcuts {
5254
GridCellWidget({Key? key}) : super(key: key);
5355

5456
@override
@@ -58,38 +60,55 @@ abstract class GridCellWidget extends StatefulWidget implements AccessoryWidget,
5860
List<GridCellAccessory> Function(GridCellAccessoryBuildContext buildContext)? get accessoryBuilder => null;
5961

6062
@override
61-
final GridCellRequestBeginFocus requestBeginFocus = GridCellRequestBeginFocus();
63+
final GridCellFocusListener beginFocus = GridCellFocusListener();
64+
65+
@override
66+
final Map<CellKeyboardKey, CellKeyboardAction> shortcutHandlers = {};
6267
}
6368

6469
abstract class GridCellState<T extends GridCellWidget> extends State<T> {
6570
@override
6671
void initState() {
67-
widget.requestBeginFocus.setListener(() => requestBeginFocus());
72+
widget.beginFocus.setListener(() => requestBeginFocus());
73+
widget.shortcutHandlers[CellKeyboardKey.onCopy] = () => onCopy();
74+
widget.shortcutHandlers[CellKeyboardKey.onInsert] = () {
75+
Clipboard.getData("text/plain").then((data) {
76+
final s = data?.text;
77+
if (s is String) {
78+
onInsert(s);
79+
}
80+
});
81+
};
6882
super.initState();
6983
}
7084

7185
@override
7286
void didUpdateWidget(covariant T oldWidget) {
7387
if (oldWidget != this) {
74-
widget.requestBeginFocus.setListener(() => requestBeginFocus());
88+
widget.beginFocus.setListener(() => requestBeginFocus());
7589
}
7690
super.didUpdateWidget(oldWidget);
7791
}
7892

7993
@override
8094
void dispose() {
81-
widget.requestBeginFocus.removeAllListener();
95+
widget.beginFocus.removeAllListener();
8296
super.dispose();
8397
}
8498

8599
void requestBeginFocus();
100+
101+
String? onCopy() => null;
102+
103+
void onInsert(String value) {}
86104
}
87105

88106
abstract class GridFocusNodeCellState<T extends GridCellWidget> extends GridCellState<T> {
89107
SingleListenrFocusNode focusNode = SingleListenrFocusNode();
90108

91109
@override
92110
void initState() {
111+
widget.shortcutHandlers[CellKeyboardKey.onEnter] = () => focusNode.unfocus();
93112
_listenOnFocusNodeChanged();
94113
super.initState();
95114
}
@@ -104,6 +123,7 @@ abstract class GridFocusNodeCellState<T extends GridCellWidget> extends GridCell
104123

105124
@override
106125
void dispose() {
126+
widget.shortcutHandlers.clear();
107127
focusNode.removeAllListener();
108128
focusNode.dispose();
109129
super.dispose();
@@ -127,7 +147,7 @@ abstract class GridFocusNodeCellState<T extends GridCellWidget> extends GridCell
127147
Future<void> focusChanged() async {}
128148
}
129149

130-
class GridCellRequestBeginFocus extends ChangeNotifier {
150+
class GridCellFocusListener extends ChangeNotifier {
131151
VoidCallback? _listener;
132152

133153
void setListener(VoidCallback listener) {
@@ -194,9 +214,8 @@ class CellStateNotifier extends ChangeNotifier {
194214
bool get onEnter => _onEnter;
195215
}
196216

197-
abstract class CellContainerFocustable {
198-
// Listen on the requestBeginFocus if the
199-
GridCellRequestBeginFocus get requestBeginFocus;
217+
abstract class CellFocustable {
218+
GridCellFocusListener get beginFocus;
200219
}
201220

202221
class CellContainer extends StatelessWidget {
@@ -220,7 +239,7 @@ class CellContainer extends StatelessWidget {
220239
child: Selector<CellStateNotifier, bool>(
221240
selector: (context, notifier) => notifier.isFocus,
222241
builder: (context, isFocus, _) {
223-
Widget container = Center(child: child);
242+
Widget container = Center(child: GridCellShortcuts(child: child));
224243
child.isFocus.addListener(() {
225244
Provider.of<CellStateNotifier>(context, listen: false).isFocus = child.isFocus.value;
226245
});
@@ -235,7 +254,7 @@ class CellContainer extends StatelessWidget {
235254

236255
return GestureDetector(
237256
behavior: HitTestBehavior.translucent,
238-
onTap: () => child.requestBeginFocus.notify(),
257+
onTap: () => child.beginFocus.notify(),
239258
child: Container(
240259
constraints: BoxConstraints(maxWidth: width, minHeight: 46),
241260
decoration: _makeBoxDecoration(context, isFocus),
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter/services.dart';
3+
4+
typedef CellKeyboardAction = dynamic Function();
5+
6+
enum CellKeyboardKey {
7+
onEnter,
8+
onCopy,
9+
onInsert,
10+
}
11+
12+
abstract class CellShortcuts extends Widget {
13+
const CellShortcuts({Key? key}) : super(key: key);
14+
15+
Map<CellKeyboardKey, CellKeyboardAction> get shortcutHandlers;
16+
}
17+
18+
class GridCellShortcuts extends StatelessWidget {
19+
final CellShortcuts child;
20+
const GridCellShortcuts({required this.child, Key? key}) : super(key: key);
21+
22+
@override
23+
Widget build(BuildContext context) {
24+
return Shortcuts(
25+
shortcuts: {
26+
LogicalKeySet(LogicalKeyboardKey.enter): const GridCellEnterIdent(),
27+
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyC): const GridCellCopyIntent(),
28+
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyV): const GridCellInsertIntent(),
29+
},
30+
child: Actions(
31+
actions: {
32+
GridCellEnterIdent: GridCellEnterAction(child: child),
33+
GridCellCopyIntent: GridCellCopyAction(child: child),
34+
GridCellInsertIntent: GridCellInsertAction(child: child),
35+
},
36+
child: child,
37+
),
38+
);
39+
}
40+
}
41+
42+
class GridCellEnterIdent extends Intent {
43+
const GridCellEnterIdent();
44+
}
45+
46+
class GridCellEnterAction extends Action<GridCellEnterIdent> {
47+
final CellShortcuts child;
48+
GridCellEnterAction({required this.child});
49+
50+
@override
51+
void invoke(covariant GridCellEnterIdent intent) {
52+
final callback = child.shortcutHandlers[CellKeyboardKey.onEnter];
53+
if (callback != null) {
54+
callback();
55+
}
56+
}
57+
}
58+
59+
class GridCellCopyIntent extends Intent {
60+
const GridCellCopyIntent();
61+
}
62+
63+
class GridCellCopyAction extends Action<GridCellCopyIntent> {
64+
final CellShortcuts child;
65+
GridCellCopyAction({required this.child});
66+
67+
@override
68+
void invoke(covariant GridCellCopyIntent intent) {
69+
final callback = child.shortcutHandlers[CellKeyboardKey.onCopy];
70+
if (callback == null) {
71+
return;
72+
}
73+
74+
final s = callback();
75+
if (s is String) {
76+
Clipboard.setData(ClipboardData(text: s));
77+
}
78+
}
79+
}
80+
81+
class GridCellInsertIntent extends Intent {
82+
const GridCellInsertIntent();
83+
}
84+
85+
class GridCellInsertAction extends Action<GridCellInsertIntent> {
86+
final CellShortcuts child;
87+
GridCellInsertAction({required this.child});
88+
89+
@override
90+
void invoke(covariant GridCellInsertIntent intent) {
91+
final callback = child.shortcutHandlers[CellKeyboardKey.onInsert];
92+
if (callback != null) {
93+
callback();
94+
}
95+
}
96+
}

frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/checkbox_cell.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ class _CheckboxCellState extends GridCellState<CheckboxCell> {
2424
void initState() {
2525
final cellContext = widget.cellContextBuilder.build();
2626
_cellBloc = getIt<CheckboxCellBloc>(param1: cellContext)..add(const CheckboxCellEvent.initial());
27-
2827
super.initState();
2928
}
3029

@@ -59,4 +58,13 @@ class _CheckboxCellState extends GridCellState<CheckboxCell> {
5958
void requestBeginFocus() {
6059
_cellBloc.add(const CheckboxCellEvent.select());
6160
}
61+
62+
@override
63+
String? onCopy() {
64+
if (_cellBloc.state.isSelected) {
65+
return "Yes";
66+
} else {
67+
return "No";
68+
}
69+
}
6270
}

frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/date_cell.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ class DateCell extends GridCellWidget {
3535
}
3636

3737
@override
38-
State<DateCell> createState() => _DateCellState();
38+
GridCellState<DateCell> createState() => _DateCellState();
3939
}
4040

41-
class _DateCellState extends State<DateCell> {
41+
class _DateCellState extends GridCellState<DateCell> {
4242
late DateCellBloc _cellBloc;
4343

4444
@override
@@ -89,4 +89,10 @@ class _DateCellState extends State<DateCell> {
8989
_cellBloc.close();
9090
super.dispose();
9191
}
92+
93+
@override
94+
void requestBeginFocus() {}
95+
96+
@override
97+
String? onCopy() => _cellBloc.state.dateStr;
9298
}

frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/number_cell.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import 'dart:async';
2-
32
import 'package:app_flowy/startup/startup.dart';
43
import 'package:app_flowy/workspace/application/grid/prelude.dart';
54
import 'package:flutter/material.dart';
@@ -81,4 +80,14 @@ class _NumberCellState extends GridFocusNodeCellState<NumberCell> {
8180
String contentFromState(NumberCellState state) {
8281
return state.content.fold((l) => l, (r) => "");
8382
}
83+
84+
@override
85+
String? onCopy() {
86+
return _cellBloc.state.content.fold((content) => content, (r) => null);
87+
}
88+
89+
@override
90+
void onInsert(String value) {
91+
_cellBloc.add(NumberCellEvent.updateCell(value));
92+
}
8493
}

frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,12 @@ class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
9292
});
9393
}
9494
}
95+
96+
@override
97+
String? onCopy() => _cellBloc.state.content;
98+
99+
@override
100+
void onInsert(String value) {
101+
_cellBloc.add(TextCellEvent.updateText(value));
102+
}
95103
}

frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/cell_editor.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,21 @@ import 'package:flutter_bloc/flutter_bloc.dart';
88

99
class URLCellEditor extends StatefulWidget with FlowyOverlayDelegate {
1010
final GridURLCellContext cellContext;
11-
const URLCellEditor({required this.cellContext, Key? key}) : super(key: key);
11+
final VoidCallback completed;
12+
const URLCellEditor({required this.cellContext, required this.completed, Key? key}) : super(key: key);
1213

1314
@override
1415
State<URLCellEditor> createState() => _URLCellEditorState();
1516

1617
static void show(
1718
BuildContext context,
1819
GridURLCellContext cellContext,
20+
VoidCallback completed,
1921
) {
2022
FlowyOverlay.of(context).remove(identifier());
2123
final editor = URLCellEditor(
2224
cellContext: cellContext,
25+
completed: completed,
2326
);
2427

2528
//
@@ -46,6 +49,11 @@ class URLCellEditor extends StatefulWidget with FlowyOverlayDelegate {
4649
bool asBarrier() {
4750
return true;
4851
}
52+
53+
@override
54+
void didRemove() {
55+
completed();
56+
}
4957
}
5058

5159
class _URLCellEditorState extends State<URLCellEditor> {

0 commit comments

Comments
 (0)