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 ?? "",
));
},
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;
}

View File

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

View File

@ -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,

View File

@ -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),

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

View File

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

View File

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

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 {
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> {

View File

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

View File

@ -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),

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