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

Feat/grid keyboard shortcut
This commit is contained in:
Nathan.fooo 2022-06-04 15:01:34 +08:00 committed by GitHub
commit fbf7c9c9b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 251 additions and 23 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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();
}
}
}

View File

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

View File

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

View File

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/workspace/application/grid/prelude.dart'; import 'package:app_flowy/workspace/application/grid/prelude.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -81,4 +80,14 @@ class _NumberCellState extends GridFocusNodeCellState<NumberCell> {
String contentFromState(NumberCellState state) { String contentFromState(NumberCellState state) {
return state.content.fold((l) => l, (r) => ""); 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));
}
} }

View File

@ -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));
}
} }

View File

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

View File

@ -131,10 +131,13 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
Future<void> _openUrlOrEdit(String url) async { Future<void> _openUrlOrEdit(String url) async {
final uri = Uri.parse(url); final uri = Uri.parse(url);
if (url.isNotEmpty && await canLaunchUrl(uri)) { if (url.isNotEmpty && await canLaunchUrl(uri)) {
widget.isFocus.value = false;
await launchUrl(uri); await launchUrl(uri);
} else { } else {
final cellContext = widget.cellContextBuilder.build() as GridURLCellContext; 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() { void requestBeginFocus() {
_openUrlOrEdit(_cellBloc.state.url); _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 { class _EditURLAccessory extends StatelessWidget with GridCellAccessory {
@ -161,7 +172,7 @@ class _EditURLAccessory extends StatelessWidget with GridCellAccessory {
@override @override
void onTap() { void onTap() {
URLCellEditor.show(anchorContext, cellContext); URLCellEditor.show(anchorContext, cellContext, () {});
} }
} }

View File

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

View File

@ -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;
}
}