mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #532 from AppFlowy-IO/feat/grid_keyboard_shortcut
Feat/grid keyboard shortcut
This commit is contained in:
commit
fbf7c9c9b8
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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()),
|
||||
),
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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, () {});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user