Merge pull request #472 from AppFlowy-IO/feat_row_expand

Feat: auto expand row height
This commit is contained in:
Nathan.fooo 2022-04-30 23:17:08 +08:00 committed by GitHub
commit 2848ecb5ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 411 additions and 308 deletions

View File

@ -365,4 +365,11 @@ class GridCell with _$GridCell {
required Field field, required Field field,
Cell? cell, Cell? cell,
}) = _GridCell; }) = _GridCell;
// ignore: unused_element
const GridCell._();
String cellId() {
return rowId + field.id + "${field.fieldType}";
}
} }

View File

@ -1,5 +1,7 @@
import 'dart:collection'; import 'dart:collection';
import 'package:app_flowy/workspace/application/grid/cell/cell_service.dart'; import 'package:app_flowy/workspace/application/grid/cell/cell_service.dart';
import 'package:equatable/equatable.dart';
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show Field;
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async'; import 'dart:async';
@ -28,7 +30,13 @@ class RowBloc extends Bloc<RowEvent, RowState> {
_rowService.createRow(); _rowService.createRow();
}, },
didReceiveCellDatas: (_DidReceiveCellDatas value) async { didReceiveCellDatas: (_DidReceiveCellDatas value) async {
emit(state.copyWith(cellDataMap: value.cellData)); final fields = value.gridCellMap.values.map((e) => CellSnapshot(e.field)).toList();
final snapshots = UnmodifiableListView(fields);
emit(state.copyWith(
gridCellMap: value.gridCellMap,
snapshots: snapshots,
changeReason: value.reason,
));
}, },
); );
}, },
@ -47,7 +55,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
Future<void> _startListening() async { Future<void> _startListening() async {
_rowListenFn = _rowCache.addRowListener( _rowListenFn = _rowCache.addRowListener(
rowId: state.rowData.rowId, rowId: state.rowData.rowId,
onUpdated: (cellDatas) => add(RowEvent.didReceiveCellDatas(cellDatas)), onUpdated: (cellDatas, reason) => add(RowEvent.didReceiveCellDatas(cellDatas, reason)),
listenWhen: () => !isClosed, listenWhen: () => !isClosed,
); );
} }
@ -57,18 +65,35 @@ class RowBloc extends Bloc<RowEvent, RowState> {
class RowEvent with _$RowEvent { class RowEvent with _$RowEvent {
const factory RowEvent.initial() = _InitialRow; const factory RowEvent.initial() = _InitialRow;
const factory RowEvent.createRow() = _CreateRow; const factory RowEvent.createRow() = _CreateRow;
const factory RowEvent.didReceiveCellDatas(GridCellMap cellData) = _DidReceiveCellDatas; const factory RowEvent.didReceiveCellDatas(GridCellMap gridCellMap, GridRowChangeReason reason) =
_DidReceiveCellDatas;
} }
@freezed @freezed
class RowState with _$RowState { class RowState with _$RowState {
const factory RowState({ const factory RowState({
required GridRow rowData, required GridRow rowData,
required GridCellMap cellDataMap, required GridCellMap gridCellMap,
required UnmodifiableListView<CellSnapshot> snapshots,
GridRowChangeReason? changeReason,
}) = _RowState; }) = _RowState;
factory RowState.initial(GridRow rowData, GridCellMap cellDataMap) => RowState( factory RowState.initial(GridRow rowData, GridCellMap cellDataMap) => RowState(
rowData: rowData, rowData: rowData,
cellDataMap: cellDataMap, gridCellMap: cellDataMap,
snapshots: UnmodifiableListView(cellDataMap.values.map((e) => CellSnapshot(e.field)).toList()),
); );
} }
class CellSnapshot extends Equatable {
final Field _field;
const CellSnapshot(Field field) : _field = field;
@override
List<Object?> get props => [
_field.id,
_field.fieldType,
_field.visibility,
];
}

View File

@ -42,7 +42,7 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
Future<void> _startListening() async { Future<void> _startListening() async {
_rowListenFn = _rowCache.addRowListener( _rowListenFn = _rowCache.addRowListener(
rowId: rowData.rowId, rowId: rowData.rowId,
onUpdated: (cellDatas) => add(RowDetailEvent.didReceiveCellDatas(cellDatas.values.toList())), onUpdated: (cellDatas, reason) => add(RowDetailEvent.didReceiveCellDatas(cellDatas.values.toList())),
listenWhen: () => !isClosed, listenWhen: () => !isClosed,
); );
} }

View File

@ -83,7 +83,7 @@ class GridRowCache {
RowUpdateCallback addRowListener({ RowUpdateCallback addRowListener({
required String rowId, required String rowId,
void Function(GridCellMap)? onUpdated, void Function(GridCellMap, GridRowChangeReason)? onUpdated,
bool Function()? listenWhen, bool Function()? listenWhen,
}) { }) {
listenrHandler() async { listenrHandler() async {
@ -99,7 +99,7 @@ class GridRowCache {
final row = _rowsNotifier.rowDataWithId(rowId); final row = _rowsNotifier.rowDataWithId(rowId);
if (row != null) { if (row != null) {
final GridCellMap cellDataMap = _makeGridCells(rowId, row); final GridCellMap cellDataMap = _makeGridCells(rowId, row);
onUpdated(cellDataMap); onUpdated(cellDataMap, _rowsNotifier._changeReason);
} }
} }
@ -339,7 +339,7 @@ class GridRow with _$GridRow {
const factory GridRow({ const factory GridRow({
required String gridId, required String gridId,
required String rowId, required String rowId,
required List<Field> fields, required UnmodifiableListView<Field> fields,
required double height, required double height,
Row? data, Row? data,
}) = _GridRow; }) = _GridRow;

View File

@ -9,7 +9,7 @@ class GridSize {
static double get leadingHeaderPadding => 50 * scale; static double get leadingHeaderPadding => 50 * scale;
static double get trailHeaderPadding => 140 * scale; static double get trailHeaderPadding => 140 * scale;
static double get headerContainerPadding => 0 * scale; static double get headerContainerPadding => 0 * scale;
static double get cellHPadding => 10 * scale; static double get cellHPadding => 8 * scale;
static double get cellVPadding => 8 * scale; static double get cellVPadding => 8 * scale;
static double get typeOptionItemHeight => 32 * scale; static double get typeOptionItemHeight => 32 * scale;
static double get typeOptionSeparatorHeight => 6 * scale; static double get typeOptionSeparatorHeight => 6 * scale;

View File

@ -2,6 +2,12 @@ import 'package:app_flowy/workspace/application/grid/cell/cell_service.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/hover.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/widgets.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';
import 'package:flutter/material.dart';
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 'checkbox_cell.dart'; import 'checkbox_cell.dart';
import 'date_cell.dart'; import 'date_cell.dart';
import 'number_cell.dart'; import 'number_cell.dart';
@ -9,7 +15,7 @@ import 'selection_cell/selection_cell.dart';
import 'text_cell.dart'; import 'text_cell.dart';
GridCellWidget buildGridCellWidget(GridCell gridCell, GridCellCache cellCache, {GridCellStyle? style}) { GridCellWidget buildGridCellWidget(GridCell gridCell, GridCellCache cellCache, {GridCellStyle? style}) {
final key = ValueKey(gridCell.rowId + gridCell.field.id); final key = ValueKey(gridCell.cellId());
final cellContextBuilder = GridCellContextBuilder(gridCell: gridCell, cellCache: cellCache); final cellContextBuilder = GridCellContextBuilder(gridCell: gridCell, cellCache: cellCache);
@ -51,9 +57,157 @@ abstract class GridCellWidget extends HoverWidget {
} }
class GridCellRequestFocusNotifier extends ChangeNotifier { class GridCellRequestFocusNotifier extends ChangeNotifier {
VoidCallback? _listener;
@override
void addListener(VoidCallback listener) {
if (_listener != null) {
removeListener(_listener!);
}
_listener = listener;
super.addListener(listener);
}
void removeAllListener() {
if (_listener != null) {
removeListener(_listener!);
}
}
void notify() { void notify() {
notifyListeners(); notifyListeners();
} }
} }
abstract class GridCellStyle {} abstract class GridCellStyle {}
class CellSingleFocusNode extends FocusNode {
VoidCallback? _listener;
void setSingleListener(VoidCallback listener) {
if (_listener != null) {
removeListener(_listener!);
}
_listener = listener;
super.addListener(listener);
}
void removeSingleListener() {
if (_listener != null) {
removeListener(_listener!);
}
}
}
class CellStateNotifier extends ChangeNotifier {
bool _isFocus = false;
bool _onEnter = false;
set isFocus(bool value) {
if (_isFocus != value) {
_isFocus = value;
notifyListeners();
}
}
set onEnter(bool value) {
if (_onEnter != value) {
_onEnter = value;
notifyListeners();
}
}
bool get isFocus => _isFocus;
bool get onEnter => _onEnter;
}
class CellContainer extends StatelessWidget {
final GridCellWidget child;
final Widget? expander;
final double width;
final RegionStateNotifier rowStateNotifier;
const CellContainer({
Key? key,
required this.child,
required this.width,
required this.rowStateNotifier,
this.expander,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProxyProvider<RegionStateNotifier, CellStateNotifier>(
create: (_) => CellStateNotifier(),
update: (_, row, cell) => cell!..onEnter = row.onEnter,
child: Selector<CellStateNotifier, bool>(
selector: (context, notifier) => notifier.isFocus,
builder: (context, isFocus, _) {
Widget container = Center(child: child);
child.onFocus.addListener(() {
Provider.of<CellStateNotifier>(context, listen: false).isFocus = child.onFocus.value;
});
if (expander != null) {
container = _CellEnterRegion(child: container, expander: expander!);
}
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => child.requestFocus.notify(),
child: Container(
constraints: BoxConstraints(maxWidth: width, minHeight: 46),
decoration: _makeBoxDecoration(context, isFocus),
padding: GridSize.cellContentInsets,
child: container,
),
);
},
),
);
}
BoxDecoration _makeBoxDecoration(BuildContext context, bool isFocus) {
final theme = context.watch<AppTheme>();
if (isFocus) {
final borderSide = BorderSide(color: theme.main1, width: 1.0);
return BoxDecoration(border: Border.fromBorderSide(borderSide));
} else {
final borderSide = BorderSide(color: theme.shader5, width: 1.0);
return BoxDecoration(border: Border(right: borderSide, bottom: borderSide));
}
}
}
class _CellEnterRegion extends StatelessWidget {
final Widget child;
final Widget expander;
const _CellEnterRegion({required this.child, required this.expander, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Selector<CellStateNotifier, bool>(
selector: (context, notifier) => notifier.onEnter,
builder: (context, onEnter, _) {
List<Widget> children = [child];
if (onEnter) {
children.add(expander.positioned(right: 0));
}
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = true,
onExit: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = false,
child: Stack(
alignment: AlignmentDirectional.center,
fit: StackFit.expand,
// alignment: AlignmentDirectional.centerEnd,
children: children,
),
);
},
);
}
}

View File

@ -1,115 +0,0 @@
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
import 'cell_builder.dart';
class CellStateNotifier extends ChangeNotifier {
bool _isFocus = false;
bool _onEnter = false;
set isFocus(bool value) {
if (_isFocus != value) {
_isFocus = value;
notifyListeners();
}
}
set onEnter(bool value) {
if (_onEnter != value) {
_onEnter = value;
notifyListeners();
}
}
bool get isFocus => _isFocus;
bool get onEnter => _onEnter;
}
class CellContainer extends StatelessWidget {
final GridCellWidget child;
final Widget? expander;
final double width;
final RegionStateNotifier rowStateNotifier;
const CellContainer({
Key? key,
required this.child,
required this.width,
required this.rowStateNotifier,
this.expander,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProxyProvider<RegionStateNotifier, CellStateNotifier>(
create: (_) => CellStateNotifier(),
update: (_, row, cell) => cell!..onEnter = row.onEnter,
child: Selector<CellStateNotifier, bool>(
selector: (context, notifier) => notifier.isFocus,
builder: (context, isFocus, _) {
Widget container = Center(child: child);
child.onFocus.addListener(() {
Provider.of<CellStateNotifier>(context, listen: false).isFocus = child.onFocus.value;
});
if (expander != null) {
container = _CellEnterRegion(child: container, expander: expander!);
}
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => child.requestFocus.notify(),
child: Container(
constraints: BoxConstraints(maxWidth: width),
decoration: _makeBoxDecoration(context, isFocus),
padding: GridSize.cellContentInsets,
child: container,
),
);
},
),
);
}
BoxDecoration _makeBoxDecoration(BuildContext context, bool isFocus) {
final theme = context.watch<AppTheme>();
if (isFocus) {
final borderSide = BorderSide(color: theme.main1, width: 1.0);
return BoxDecoration(border: Border.fromBorderSide(borderSide));
} else {
final borderSide = BorderSide(color: theme.shader4, width: 0.4);
return BoxDecoration(border: Border(right: borderSide, bottom: borderSide));
}
}
}
class _CellEnterRegion extends StatelessWidget {
final Widget child;
final Widget expander;
const _CellEnterRegion({required this.child, required this.expander, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Selector<CellStateNotifier, bool>(
selector: (context, notifier) => notifier.onEnter,
builder: (context, onEnter, _) {
List<Widget> children = [Expanded(child: child)];
if (onEnter) {
children.add(expander);
}
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = true,
onExit: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = false,
child: Row(
// alignment: AlignmentDirectional.centerEnd,
children: children,
),
);
},
);
}
}

View File

@ -57,6 +57,7 @@ class _CheckboxCellState extends State<CheckboxCell> {
@override @override
Future<void> dispose() async { Future<void> dispose() async {
widget.requestFocus.removeAllListener();
_cellBloc.close(); _cellBloc.close();
super.dispose(); super.dispose();
} }

View File

@ -22,8 +22,7 @@ class NumberCell extends GridCellWidget {
class _NumberCellState extends State<NumberCell> { class _NumberCellState extends State<NumberCell> {
late NumberCellBloc _cellBloc; late NumberCellBloc _cellBloc;
late TextEditingController _controller; late TextEditingController _controller;
late FocusNode _focusNode; late CellSingleFocusNode _focusNode;
VoidCallback? _focusListener;
Timer? _delayOperation; Timer? _delayOperation;
@override @override
@ -31,11 +30,8 @@ class _NumberCellState extends State<NumberCell> {
final cellContext = widget.cellContextBuilder.build(); final cellContext = widget.cellContextBuilder.build();
_cellBloc = getIt<NumberCellBloc>(param1: cellContext)..add(const NumberCellEvent.initial()); _cellBloc = getIt<NumberCellBloc>(param1: cellContext)..add(const NumberCellEvent.initial());
_controller = TextEditingController(text: _cellBloc.state.content); _controller = TextEditingController(text: _cellBloc.state.content);
_focusNode = FocusNode(); _focusNode = CellSingleFocusNode();
_focusNode.addListener(() { _listenFocusNode();
widget.onFocus.value = _focusNode.hasFocus;
focusChanged();
});
super.initState(); super.initState();
} }
@ -55,7 +51,7 @@ class _NumberCellState extends State<NumberCell> {
controller: _controller, controller: _controller,
focusNode: _focusNode, focusNode: _focusNode,
onEditingComplete: () => _focusNode.unfocus(), onEditingComplete: () => _focusNode.unfocus(),
maxLines: 1, maxLines: null,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
decoration: const InputDecoration( decoration: const InputDecoration(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
@ -70,15 +66,22 @@ class _NumberCellState extends State<NumberCell> {
@override @override
Future<void> dispose() async { Future<void> dispose() async {
if (_focusListener != null) { widget.requestFocus.removeAllListener();
widget.requestFocus.removeListener(_focusListener!);
}
_delayOperation?.cancel(); _delayOperation?.cancel();
_cellBloc.close(); _cellBloc.close();
_focusNode.removeSingleListener();
_focusNode.dispose(); _focusNode.dispose();
super.dispose(); super.dispose();
} }
@override
void didUpdateWidget(covariant NumberCell oldWidget) {
if (oldWidget != widget) {
_listenFocusNode();
}
super.didUpdateWidget(oldWidget);
}
Future<void> focusChanged() async { Future<void> focusChanged() async {
if (mounted) { if (mounted) {
_delayOperation?.cancel(); _delayOperation?.cancel();
@ -95,18 +98,19 @@ class _NumberCellState extends State<NumberCell> {
} }
} }
void _listenCellRequestFocus(BuildContext context) { void _listenFocusNode() {
if (_focusListener != null) { widget.onFocus.value = _focusNode.hasFocus;
widget.requestFocus.removeListener(_focusListener!); _focusNode.setSingleListener(() {
widget.onFocus.value = _focusNode.hasFocus;
focusChanged();
});
} }
focusListener() { void _listenCellRequestFocus(BuildContext context) {
widget.requestFocus.addListener(() {
if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) { if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) {
FocusScope.of(context).requestFocus(_focusNode); FocusScope.of(context).requestFocus(_focusNode);
} }
} });
_focusListener = focusListener;
widget.requestFocus.addListener(focusListener);
} }
} }

View File

@ -1,5 +1,4 @@
export 'cell_builder.dart'; export 'cell_builder.dart';
export 'cell_container.dart';
export 'text_cell.dart'; export 'text_cell.dart';
export 'number_cell.dart'; export 'number_cell.dart';
export 'date_cell.dart'; export 'date_cell.dart';

View File

@ -66,15 +66,25 @@ class SelectOptionTag extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return ChoiceChip(
decoration: BoxDecoration( pressElevation: 1,
color: option.color.make(context), label: FlowyText.medium(option.name, fontSize: 12),
shape: BoxShape.rectangle, selectedColor: option.color.make(context),
borderRadius: BorderRadius.circular(8.0), backgroundColor: option.color.make(context),
), labelPadding: const EdgeInsets.symmetric(horizontal: 6),
child: Center(child: FlowyText.medium(option.name, fontSize: 12)), selected: true,
margin: const EdgeInsets.symmetric(horizontal: 3.0), onSelected: (_) {},
padding: const EdgeInsets.symmetric(horizontal: 6.0),
); );
// return Container(
// decoration: BoxDecoration(
// color: option.color.make(context),
// shape: BoxShape.rectangle,
// borderRadius: BorderRadius.circular(8.0),
// ),
// child: Center(child: FlowyText.medium(option.name, fontSize: 12)),
// margin: const EdgeInsets.symmetric(horizontal: 3.0),
// padding: const EdgeInsets.symmetric(horizontal: 6.0),
// );
} }
} }

View File

@ -5,6 +5,7 @@ import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
// ignore: unused_import // ignore: unused_import
import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -44,51 +45,29 @@ class _SingleSelectCellState extends State<SingleSelectCell> {
@override @override
void initState() { void initState() {
// Log.trace("init widget $hashCode"); final cellContext = widget.cellContextBuilder.build() as GridSelectOptionCellContext;
final cellContext = _buildCellContext();
_cellBloc = getIt<SelectionCellBloc>(param1: cellContext)..add(const SelectionCellEvent.initial()); _cellBloc = getIt<SelectionCellBloc>(param1: cellContext)..add(const SelectionCellEvent.initial());
super.initState(); super.initState();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
// Log.trace("build widget $hashCode");
return BlocProvider.value( return BlocProvider.value(
value: _cellBloc, value: _cellBloc,
child: BlocBuilder<SelectionCellBloc, SelectionCellState>( child: BlocBuilder<SelectionCellBloc, SelectionCellState>(
builder: (context, state) { builder: (context, state) {
List<Widget> children = []; return _SelectOptionCell(
children.addAll(state.selectedOptions.map((option) => SelectOptionTag(option: option)).toList()); selectOptions: state.selectedOptions,
cellStyle: widget.cellStyle,
if (children.isEmpty && widget.cellStyle != null) { onFocus: (value) => widget.onFocus.value = value,
children.add(FlowyText.medium(widget.cellStyle!.placeholder, fontSize: 14, color: theme.shader3)); cellContextBuilder: widget.cellContextBuilder);
}
return SizedBox.expand(
child: InkWell(
onTap: () {
widget.onFocus.value = true;
SelectOptionCellEditor.show(
context,
_buildCellContext(),
() => widget.onFocus.value = false,
);
},
child: ClipRRect(child: Row(children: children)),
),
);
}, },
), ),
); );
} }
GridSelectOptionCellContext _buildCellContext() {
return widget.cellContextBuilder.build() as GridSelectOptionCellContext;
}
@override @override
Future<void> dispose() async { Future<void> dispose() async {
// Log.trace("dispose widget $hashCode");
_cellBloc.close(); _cellBloc.close();
super.dispose(); super.dispose();
} }
@ -120,7 +99,7 @@ class _MultiSelectCellState extends State<MultiSelectCell> {
@override @override
void initState() { void initState() {
final cellContext = _buildCellContext(); final cellContext = widget.cellContextBuilder.build() as GridSelectOptionCellContext;
_cellBloc = getIt<SelectionCellBloc>(param1: cellContext)..add(const SelectionCellEvent.initial()); _cellBloc = getIt<SelectionCellBloc>(param1: cellContext)..add(const SelectionCellEvent.initial());
super.initState(); super.initState();
} }
@ -131,25 +110,11 @@ class _MultiSelectCellState extends State<MultiSelectCell> {
value: _cellBloc, value: _cellBloc,
child: BlocBuilder<SelectionCellBloc, SelectionCellState>( child: BlocBuilder<SelectionCellBloc, SelectionCellState>(
builder: (context, state) { builder: (context, state) {
List<Widget> children = state.selectedOptions.map((option) => SelectOptionTag(option: option)).toList(); return _SelectOptionCell(
selectOptions: state.selectedOptions,
if (children.isEmpty && widget.cellStyle != null) { cellStyle: widget.cellStyle,
children.add(FlowyText.medium(widget.cellStyle!.placeholder, fontSize: 14)); onFocus: (value) => widget.onFocus.value = value,
} cellContextBuilder: widget.cellContextBuilder);
return SizedBox.expand(
child: InkWell(
onTap: () {
widget.onFocus.value = true;
SelectOptionCellEditor.show(
context,
_buildCellContext(),
() => widget.onFocus.value = false,
);
},
child: ClipRRect(child: Row(children: children)),
),
);
}, },
), ),
); );
@ -160,8 +125,51 @@ class _MultiSelectCellState extends State<MultiSelectCell> {
_cellBloc.close(); _cellBloc.close();
super.dispose(); super.dispose();
} }
}
GridSelectOptionCellContext _buildCellContext() { class _SelectOptionCell extends StatelessWidget {
return widget.cellContextBuilder.build() as GridSelectOptionCellContext; final List<SelectOption> selectOptions;
final void Function(bool) onFocus;
final SelectOptionCellStyle? cellStyle;
final GridCellContextBuilder cellContextBuilder;
const _SelectOptionCell({
required this.selectOptions,
required this.onFocus,
required this.cellStyle,
required this.cellContextBuilder,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
final Widget child;
if (selectOptions.isEmpty && cellStyle != null) {
child = Align(
alignment: Alignment.centerLeft,
child: FlowyText.medium(cellStyle!.placeholder, fontSize: 14, color: theme.shader3),
);
} else {
final tags = selectOptions.map((option) => SelectOptionTag(option: option)).toList();
child = Align(
alignment: Alignment.centerLeft,
child: Wrap(children: tags, spacing: 4, runSpacing: 4),
);
}
return Stack(
alignment: AlignmentDirectional.center,
fit: StackFit.expand,
children: [
child,
InkWell(
onTap: () {
onFocus(true);
final cellContext = cellContextBuilder.build() as GridSelectOptionCellContext;
SelectOptionCellEditor.show(context, cellContext, () => onFocus(false));
},
),
],
);
} }
} }

View File

@ -184,11 +184,22 @@ class _SelectOptionCell extends StatelessWidget {
final theme = context.watch<AppTheme>(); final theme = context.watch<AppTheme>();
return SizedBox( return SizedBox(
height: GridSize.typeOptionItemHeight, height: GridSize.typeOptionItemHeight,
child: InkWell( child: Stack(
fit: StackFit.expand,
children: [
_body(theme, context),
InkWell(
onTap: () { onTap: () {
context.read<SelectOptionEditorBloc>().add(SelectOptionEditorEvent.selectOption(option.id)); context.read<SelectOptionEditorBloc>().add(SelectOptionEditorEvent.selectOption(option.id));
}, },
child: FlowyHover( ),
],
),
);
}
FlowyHover _body(AppTheme theme, BuildContext context) {
return FlowyHover(
style: HoverStyle(hoverColor: theme.hover), style: HoverStyle(hoverColor: theme.hover),
builder: (_, onHover) { builder: (_, onHover) {
List<Widget> children = [ List<Widget> children = [
@ -214,8 +225,6 @@ class _SelectOptionCell extends StatelessWidget {
child: Row(children: children), child: Row(children: children),
); );
}, },
),
),
); );
} }

View File

@ -94,7 +94,7 @@ class SelectOptionTextField extends StatelessWidget {
child: SingleChildScrollView( child: SingleChildScrollView(
controller: sc, controller: sc,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Row(children: children), child: Wrap(children: children, spacing: 4),
), ),
); );
} }

View File

@ -35,8 +35,7 @@ class GridTextCell extends GridCellWidget {
class _GridTextCellState extends State<GridTextCell> { class _GridTextCellState extends State<GridTextCell> {
late TextCellBloc _cellBloc; late TextCellBloc _cellBloc;
late TextEditingController _controller; late TextEditingController _controller;
late FocusNode _focusNode; late CellSingleFocusNode _focusNode;
VoidCallback? _focusListener;
Timer? _delayOperation; Timer? _delayOperation;
@override @override
@ -45,35 +44,29 @@ class _GridTextCellState extends State<GridTextCell> {
_cellBloc = getIt<TextCellBloc>(param1: cellContext); _cellBloc = getIt<TextCellBloc>(param1: cellContext);
_cellBloc.add(const TextCellEvent.initial()); _cellBloc.add(const TextCellEvent.initial());
_controller = TextEditingController(text: _cellBloc.state.content); _controller = TextEditingController(text: _cellBloc.state.content);
_focusNode = FocusNode(); _focusNode = CellSingleFocusNode();
_focusNode.addListener(() {
widget.onFocus.value = _focusNode.hasFocus;
focusChanged();
});
_listenFocusNode();
_listenRequestFocus(context);
super.initState(); super.initState();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_listenCellRequestFocus(context);
return BlocProvider.value( return BlocProvider.value(
value: _cellBloc, value: _cellBloc,
child: BlocConsumer<TextCellBloc, TextCellState>( child: BlocListener<TextCellBloc, TextCellState>(
listener: (context, state) { listener: (context, state) {
if (_controller.text != state.content) { if (_controller.text != state.content) {
_controller.text = state.content; _controller.text = state.content;
} }
}, },
buildWhen: (previous, current) => previous.content != current.content, child: TextField(
builder: (context, state) {
return TextField(
controller: _controller, controller: _controller,
focusNode: _focusNode, focusNode: _focusNode,
onChanged: (value) => focusChanged(), onChanged: (value) => focusChanged(),
onEditingComplete: () => _focusNode.unfocus(), onEditingComplete: () => _focusNode.unfocus(),
maxLines: 1, maxLines: null,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
@ -81,36 +74,44 @@ class _GridTextCellState extends State<GridTextCell> {
hintText: widget.cellStyle?.placeholder, hintText: widget.cellStyle?.placeholder,
isDense: true, isDense: true,
), ),
); ),
},
), ),
); );
} }
void _listenCellRequestFocus(BuildContext context) { @override
if (_focusListener != null) { Future<void> dispose() async {
widget.requestFocus.removeListener(_focusListener!); widget.requestFocus.removeAllListener();
} _delayOperation?.cancel();
_cellBloc.close();
_focusNode.removeSingleListener();
_focusNode.dispose();
focusListener() { super.dispose();
if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) {
FocusScope.of(context).requestFocus(_focusNode);
}
}
_focusListener = focusListener;
widget.requestFocus.addListener(focusListener);
} }
@override @override
Future<void> dispose() async { void didUpdateWidget(covariant GridTextCell oldWidget) {
if (_focusListener != null) { if (oldWidget != widget) {
widget.requestFocus.removeListener(_focusListener!); _listenFocusNode();
} }
_delayOperation?.cancel(); super.didUpdateWidget(oldWidget);
_cellBloc.close(); }
_focusNode.dispose();
super.dispose(); void _listenFocusNode() {
widget.onFocus.value = _focusNode.hasFocus;
_focusNode.setSingleListener(() {
widget.onFocus.value = _focusNode.hasFocus;
focusChanged();
});
}
void _listenRequestFocus(BuildContext context) {
widget.requestFocus.addListener(() {
if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) {
FocusScope.of(context).requestFocus(_focusNode);
}
});
} }
Future<void> focusChanged() async { Future<void> focusChanged() async {

View File

@ -4,6 +4,7 @@ import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/p
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -48,19 +49,13 @@ class _GridRowWidgetState extends State<GridRowWidget> {
child: BlocBuilder<RowBloc, RowState>( child: BlocBuilder<RowBloc, RowState>(
buildWhen: (p, c) => p.rowData.height != c.rowData.height, buildWhen: (p, c) => p.rowData.height != c.rowData.height,
builder: (context, state) { builder: (context, state) {
final children = [ return Row(
children: [
const _RowLeading(), const _RowLeading(),
_RowCells(cellCache: widget.cellCache, onExpand: () => onExpandCell(context)), Expanded(child: _RowCells(cellCache: widget.cellCache, onExpand: () => _expandRow(context))),
const _RowTrailing(), const _RowTrailing(),
]; ],
final child = Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: children,
); );
return SizedBox(height: 42, child: child);
}, },
), ),
), ),
@ -73,7 +68,7 @@ class _GridRowWidgetState extends State<GridRowWidget> {
super.dispose(); super.dispose();
} }
void onExpandCell(BuildContext context) { void _expandRow(BuildContext context) {
final page = RowDetailPage( final page = RowDetailPage(
rowData: widget.rowData, rowData: widget.rowData,
rowCache: widget.rowCache, rowCache: widget.rowCache,
@ -159,13 +154,15 @@ class _RowCells extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<RowBloc, RowState>( return BlocBuilder<RowBloc, RowState>(
buildWhen: (previous, current) => previous.cellDataMap.length != current.cellDataMap.length, buildWhen: (previous, current) => !listEquals(previous.snapshots, current.snapshots),
builder: (context, state) { builder: (context, state) {
return Row( return IntrinsicHeight(
mainAxisSize: MainAxisSize.min, child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.max,
children: _makeCells(context, state.cellDataMap), mainAxisAlignment: MainAxisAlignment.start,
); crossAxisAlignment: CrossAxisAlignment.stretch,
children: _makeCells(context, state.gridCellMap),
));
}, },
); );
} }
@ -209,11 +206,14 @@ class _CellExpander extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.watch<AppTheme>(); final theme = context.watch<AppTheme>();
return FlowyIconButton( return FittedBox(
width: 20, fit: BoxFit.contain,
child: FlowyIconButton(
onPressed: onExpand, onPressed: onExpand,
iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2), iconPadding: const EdgeInsets.fromLTRB(6, 6, 6, 6),
fillColor: theme.surface,
icon: svgWidget("grid/expander", color: theme.main1), icon: svgWidget("grid/expander", color: theme.main1),
),
); );
} }
} }