mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #472 from AppFlowy-IO/feat_row_expand
Feat: auto expand row height
This commit is contained in:
commit
2848ecb5ba
@ -365,4 +365,11 @@ class GridCell with _$GridCell {
|
||||
required Field field,
|
||||
Cell? cell,
|
||||
}) = _GridCell;
|
||||
|
||||
// ignore: unused_element
|
||||
const GridCell._();
|
||||
|
||||
String cellId() {
|
||||
return rowId + field.id + "${field.fieldType}";
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'dart:collection';
|
||||
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:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
@ -28,7 +30,13 @@ class RowBloc extends Bloc<RowEvent, RowState> {
|
||||
_rowService.createRow();
|
||||
},
|
||||
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 {
|
||||
_rowListenFn = _rowCache.addRowListener(
|
||||
rowId: state.rowData.rowId,
|
||||
onUpdated: (cellDatas) => add(RowEvent.didReceiveCellDatas(cellDatas)),
|
||||
onUpdated: (cellDatas, reason) => add(RowEvent.didReceiveCellDatas(cellDatas, reason)),
|
||||
listenWhen: () => !isClosed,
|
||||
);
|
||||
}
|
||||
@ -57,18 +65,35 @@ class RowBloc extends Bloc<RowEvent, RowState> {
|
||||
class RowEvent with _$RowEvent {
|
||||
const factory RowEvent.initial() = _InitialRow;
|
||||
const factory RowEvent.createRow() = _CreateRow;
|
||||
const factory RowEvent.didReceiveCellDatas(GridCellMap cellData) = _DidReceiveCellDatas;
|
||||
const factory RowEvent.didReceiveCellDatas(GridCellMap gridCellMap, GridRowChangeReason reason) =
|
||||
_DidReceiveCellDatas;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class RowState with _$RowState {
|
||||
const factory RowState({
|
||||
required GridRow rowData,
|
||||
required GridCellMap cellDataMap,
|
||||
required GridCellMap gridCellMap,
|
||||
required UnmodifiableListView<CellSnapshot> snapshots,
|
||||
GridRowChangeReason? changeReason,
|
||||
}) = _RowState;
|
||||
|
||||
factory RowState.initial(GridRow rowData, GridCellMap cellDataMap) => RowState(
|
||||
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,
|
||||
];
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||
Future<void> _startListening() async {
|
||||
_rowListenFn = _rowCache.addRowListener(
|
||||
rowId: rowData.rowId,
|
||||
onUpdated: (cellDatas) => add(RowDetailEvent.didReceiveCellDatas(cellDatas.values.toList())),
|
||||
onUpdated: (cellDatas, reason) => add(RowDetailEvent.didReceiveCellDatas(cellDatas.values.toList())),
|
||||
listenWhen: () => !isClosed,
|
||||
);
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ class GridRowCache {
|
||||
|
||||
RowUpdateCallback addRowListener({
|
||||
required String rowId,
|
||||
void Function(GridCellMap)? onUpdated,
|
||||
void Function(GridCellMap, GridRowChangeReason)? onUpdated,
|
||||
bool Function()? listenWhen,
|
||||
}) {
|
||||
listenrHandler() async {
|
||||
@ -99,7 +99,7 @@ class GridRowCache {
|
||||
final row = _rowsNotifier.rowDataWithId(rowId);
|
||||
if (row != null) {
|
||||
final GridCellMap cellDataMap = _makeGridCells(rowId, row);
|
||||
onUpdated(cellDataMap);
|
||||
onUpdated(cellDataMap, _rowsNotifier._changeReason);
|
||||
}
|
||||
}
|
||||
|
||||
@ -339,7 +339,7 @@ class GridRow with _$GridRow {
|
||||
const factory GridRow({
|
||||
required String gridId,
|
||||
required String rowId,
|
||||
required List<Field> fields,
|
||||
required UnmodifiableListView<Field> fields,
|
||||
required double height,
|
||||
Row? data,
|
||||
}) = _GridRow;
|
||||
|
@ -9,7 +9,7 @@ class GridSize {
|
||||
static double get leadingHeaderPadding => 50 * scale;
|
||||
static double get trailHeaderPadding => 140 * 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 typeOptionItemHeight => 32 * scale;
|
||||
static double get typeOptionSeparatorHeight => 6 * scale;
|
||||
|
@ -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_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show FieldType;
|
||||
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 'date_cell.dart';
|
||||
import 'number_cell.dart';
|
||||
@ -9,7 +15,7 @@ import 'selection_cell/selection_cell.dart';
|
||||
import 'text_cell.dart';
|
||||
|
||||
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);
|
||||
|
||||
@ -51,9 +57,157 @@ abstract class GridCellWidget extends HoverWidget {
|
||||
}
|
||||
|
||||
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() {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -57,6 +57,7 @@ class _CheckboxCellState extends State<CheckboxCell> {
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
widget.requestFocus.removeAllListener();
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
@ -22,8 +22,7 @@ class NumberCell extends GridCellWidget {
|
||||
class _NumberCellState extends State<NumberCell> {
|
||||
late NumberCellBloc _cellBloc;
|
||||
late TextEditingController _controller;
|
||||
late FocusNode _focusNode;
|
||||
VoidCallback? _focusListener;
|
||||
late CellSingleFocusNode _focusNode;
|
||||
Timer? _delayOperation;
|
||||
|
||||
@override
|
||||
@ -31,11 +30,8 @@ class _NumberCellState extends State<NumberCell> {
|
||||
final cellContext = widget.cellContextBuilder.build();
|
||||
_cellBloc = getIt<NumberCellBloc>(param1: cellContext)..add(const NumberCellEvent.initial());
|
||||
_controller = TextEditingController(text: _cellBloc.state.content);
|
||||
_focusNode = FocusNode();
|
||||
_focusNode.addListener(() {
|
||||
widget.onFocus.value = _focusNode.hasFocus;
|
||||
focusChanged();
|
||||
});
|
||||
_focusNode = CellSingleFocusNode();
|
||||
_listenFocusNode();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@ -55,7 +51,7 @@ class _NumberCellState extends State<NumberCell> {
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
onEditingComplete: () => _focusNode.unfocus(),
|
||||
maxLines: 1,
|
||||
maxLines: null,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
@ -70,15 +66,22 @@ class _NumberCellState extends State<NumberCell> {
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
if (_focusListener != null) {
|
||||
widget.requestFocus.removeListener(_focusListener!);
|
||||
}
|
||||
widget.requestFocus.removeAllListener();
|
||||
_delayOperation?.cancel();
|
||||
_cellBloc.close();
|
||||
_focusNode.removeSingleListener();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant NumberCell oldWidget) {
|
||||
if (oldWidget != widget) {
|
||||
_listenFocusNode();
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
Future<void> focusChanged() async {
|
||||
if (mounted) {
|
||||
_delayOperation?.cancel();
|
||||
@ -95,18 +98,19 @@ class _NumberCellState extends State<NumberCell> {
|
||||
}
|
||||
}
|
||||
|
||||
void _listenCellRequestFocus(BuildContext context) {
|
||||
if (_focusListener != null) {
|
||||
widget.requestFocus.removeListener(_focusListener!);
|
||||
}
|
||||
void _listenFocusNode() {
|
||||
widget.onFocus.value = _focusNode.hasFocus;
|
||||
_focusNode.setSingleListener(() {
|
||||
widget.onFocus.value = _focusNode.hasFocus;
|
||||
focusChanged();
|
||||
});
|
||||
}
|
||||
|
||||
focusListener() {
|
||||
void _listenCellRequestFocus(BuildContext context) {
|
||||
widget.requestFocus.addListener(() {
|
||||
if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) {
|
||||
FocusScope.of(context).requestFocus(_focusNode);
|
||||
}
|
||||
}
|
||||
|
||||
_focusListener = focusListener;
|
||||
widget.requestFocus.addListener(focusListener);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
export 'cell_builder.dart';
|
||||
export 'cell_container.dart';
|
||||
export 'text_cell.dart';
|
||||
export 'number_cell.dart';
|
||||
export 'date_cell.dart';
|
||||
|
@ -66,15 +66,25 @@ class SelectOptionTag extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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),
|
||||
return ChoiceChip(
|
||||
pressElevation: 1,
|
||||
label: FlowyText.medium(option.name, fontSize: 12),
|
||||
selectedColor: option.color.make(context),
|
||||
backgroundColor: option.color.make(context),
|
||||
labelPadding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
selected: true,
|
||||
onSelected: (_) {},
|
||||
);
|
||||
|
||||
// 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),
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
// ignore: unused_import
|
||||
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_bloc/flutter_bloc.dart';
|
||||
|
||||
@ -44,51 +45,29 @@ class _SingleSelectCellState extends State<SingleSelectCell> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// Log.trace("init widget $hashCode");
|
||||
final cellContext = _buildCellContext();
|
||||
final cellContext = widget.cellContextBuilder.build() as GridSelectOptionCellContext;
|
||||
_cellBloc = getIt<SelectionCellBloc>(param1: cellContext)..add(const SelectionCellEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
// Log.trace("build widget $hashCode");
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<SelectionCellBloc, SelectionCellState>(
|
||||
builder: (context, state) {
|
||||
List<Widget> children = [];
|
||||
children.addAll(state.selectedOptions.map((option) => SelectOptionTag(option: option)).toList());
|
||||
|
||||
if (children.isEmpty && widget.cellStyle != null) {
|
||||
children.add(FlowyText.medium(widget.cellStyle!.placeholder, fontSize: 14, color: theme.shader3));
|
||||
}
|
||||
return SizedBox.expand(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
widget.onFocus.value = true;
|
||||
SelectOptionCellEditor.show(
|
||||
context,
|
||||
_buildCellContext(),
|
||||
() => widget.onFocus.value = false,
|
||||
);
|
||||
},
|
||||
child: ClipRRect(child: Row(children: children)),
|
||||
),
|
||||
);
|
||||
return _SelectOptionCell(
|
||||
selectOptions: state.selectedOptions,
|
||||
cellStyle: widget.cellStyle,
|
||||
onFocus: (value) => widget.onFocus.value = value,
|
||||
cellContextBuilder: widget.cellContextBuilder);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GridSelectOptionCellContext _buildCellContext() {
|
||||
return widget.cellContextBuilder.build() as GridSelectOptionCellContext;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
// Log.trace("dispose widget $hashCode");
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
@ -120,7 +99,7 @@ class _MultiSelectCellState extends State<MultiSelectCell> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellContext = _buildCellContext();
|
||||
final cellContext = widget.cellContextBuilder.build() as GridSelectOptionCellContext;
|
||||
_cellBloc = getIt<SelectionCellBloc>(param1: cellContext)..add(const SelectionCellEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
@ -131,25 +110,11 @@ class _MultiSelectCellState extends State<MultiSelectCell> {
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<SelectionCellBloc, SelectionCellState>(
|
||||
builder: (context, state) {
|
||||
List<Widget> children = state.selectedOptions.map((option) => SelectOptionTag(option: option)).toList();
|
||||
|
||||
if (children.isEmpty && widget.cellStyle != null) {
|
||||
children.add(FlowyText.medium(widget.cellStyle!.placeholder, fontSize: 14));
|
||||
}
|
||||
|
||||
return SizedBox.expand(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
widget.onFocus.value = true;
|
||||
SelectOptionCellEditor.show(
|
||||
context,
|
||||
_buildCellContext(),
|
||||
() => widget.onFocus.value = false,
|
||||
);
|
||||
},
|
||||
child: ClipRRect(child: Row(children: children)),
|
||||
),
|
||||
);
|
||||
return _SelectOptionCell(
|
||||
selectOptions: state.selectedOptions,
|
||||
cellStyle: widget.cellStyle,
|
||||
onFocus: (value) => widget.onFocus.value = value,
|
||||
cellContextBuilder: widget.cellContextBuilder);
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -160,8 +125,51 @@ class _MultiSelectCellState extends State<MultiSelectCell> {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
GridSelectOptionCellContext _buildCellContext() {
|
||||
return widget.cellContextBuilder.build() as GridSelectOptionCellContext;
|
||||
class _SelectOptionCell extends StatelessWidget {
|
||||
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));
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -184,41 +184,50 @@ class _SelectOptionCell extends StatelessWidget {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return SizedBox(
|
||||
height: GridSize.typeOptionItemHeight,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
context.read<SelectOptionEditorBloc>().add(SelectOptionEditorEvent.selectOption(option.id));
|
||||
},
|
||||
child: FlowyHover(
|
||||
style: HoverStyle(hoverColor: theme.hover),
|
||||
builder: (_, onHover) {
|
||||
List<Widget> children = [
|
||||
SelectOptionTag(option: option, isSelected: isSelected),
|
||||
const Spacer(),
|
||||
];
|
||||
|
||||
if (isSelected) {
|
||||
children.add(svgWidget("grid/checkmark"));
|
||||
}
|
||||
|
||||
if (onHover) {
|
||||
children.add(FlowyIconButton(
|
||||
width: 30,
|
||||
onPressed: () => _showEditPannel(context),
|
||||
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
|
||||
icon: svgWidget("editor/details", color: theme.iconColor),
|
||||
));
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
child: Row(children: children),
|
||||
);
|
||||
},
|
||||
),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
_body(theme, context),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
context.read<SelectOptionEditorBloc>().add(SelectOptionEditorEvent.selectOption(option.id));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
FlowyHover _body(AppTheme theme, BuildContext context) {
|
||||
return FlowyHover(
|
||||
style: HoverStyle(hoverColor: theme.hover),
|
||||
builder: (_, onHover) {
|
||||
List<Widget> children = [
|
||||
SelectOptionTag(option: option, isSelected: isSelected),
|
||||
const Spacer(),
|
||||
];
|
||||
|
||||
if (isSelected) {
|
||||
children.add(svgWidget("grid/checkmark"));
|
||||
}
|
||||
|
||||
if (onHover) {
|
||||
children.add(FlowyIconButton(
|
||||
width: 30,
|
||||
onPressed: () => _showEditPannel(context),
|
||||
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
|
||||
icon: svgWidget("editor/details", color: theme.iconColor),
|
||||
));
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
child: Row(children: children),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditPannel(BuildContext context) {
|
||||
final pannel = EditSelectOptionPannel(
|
||||
option: option,
|
||||
|
@ -94,7 +94,7 @@ class SelectOptionTextField extends StatelessWidget {
|
||||
child: SingleChildScrollView(
|
||||
controller: sc,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(children: children),
|
||||
child: Wrap(children: children, spacing: 4),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -35,8 +35,7 @@ class GridTextCell extends GridCellWidget {
|
||||
class _GridTextCellState extends State<GridTextCell> {
|
||||
late TextCellBloc _cellBloc;
|
||||
late TextEditingController _controller;
|
||||
late FocusNode _focusNode;
|
||||
VoidCallback? _focusListener;
|
||||
late CellSingleFocusNode _focusNode;
|
||||
Timer? _delayOperation;
|
||||
|
||||
@override
|
||||
@ -45,72 +44,74 @@ class _GridTextCellState extends State<GridTextCell> {
|
||||
_cellBloc = getIt<TextCellBloc>(param1: cellContext);
|
||||
_cellBloc.add(const TextCellEvent.initial());
|
||||
_controller = TextEditingController(text: _cellBloc.state.content);
|
||||
_focusNode = FocusNode();
|
||||
_focusNode.addListener(() {
|
||||
widget.onFocus.value = _focusNode.hasFocus;
|
||||
focusChanged();
|
||||
});
|
||||
_focusNode = CellSingleFocusNode();
|
||||
|
||||
_listenFocusNode();
|
||||
_listenRequestFocus(context);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_listenCellRequestFocus(context);
|
||||
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocConsumer<TextCellBloc, TextCellState>(
|
||||
child: BlocListener<TextCellBloc, TextCellState>(
|
||||
listener: (context, state) {
|
||||
if (_controller.text != state.content) {
|
||||
_controller.text = state.content;
|
||||
}
|
||||
},
|
||||
buildWhen: (previous, current) => previous.content != current.content,
|
||||
builder: (context, state) {
|
||||
return TextField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
onChanged: (value) => focusChanged(),
|
||||
onEditingComplete: () => _focusNode.unfocus(),
|
||||
maxLines: 1,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
border: InputBorder.none,
|
||||
hintText: widget.cellStyle?.placeholder,
|
||||
isDense: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
onChanged: (value) => focusChanged(),
|
||||
onEditingComplete: () => _focusNode.unfocus(),
|
||||
maxLines: null,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
border: InputBorder.none,
|
||||
hintText: widget.cellStyle?.placeholder,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _listenCellRequestFocus(BuildContext context) {
|
||||
if (_focusListener != null) {
|
||||
widget.requestFocus.removeListener(_focusListener!);
|
||||
}
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
widget.requestFocus.removeAllListener();
|
||||
_delayOperation?.cancel();
|
||||
_cellBloc.close();
|
||||
_focusNode.removeSingleListener();
|
||||
_focusNode.dispose();
|
||||
|
||||
focusListener() {
|
||||
if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) {
|
||||
FocusScope.of(context).requestFocus(_focusNode);
|
||||
}
|
||||
}
|
||||
|
||||
_focusListener = focusListener;
|
||||
widget.requestFocus.addListener(focusListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
if (_focusListener != null) {
|
||||
widget.requestFocus.removeListener(_focusListener!);
|
||||
void didUpdateWidget(covariant GridTextCell oldWidget) {
|
||||
if (oldWidget != widget) {
|
||||
_listenFocusNode();
|
||||
}
|
||||
_delayOperation?.cancel();
|
||||
_cellBloc.close();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -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/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -48,19 +49,13 @@ class _GridRowWidgetState extends State<GridRowWidget> {
|
||||
child: BlocBuilder<RowBloc, RowState>(
|
||||
buildWhen: (p, c) => p.rowData.height != c.rowData.height,
|
||||
builder: (context, state) {
|
||||
final children = [
|
||||
const _RowLeading(),
|
||||
_RowCells(cellCache: widget.cellCache, onExpand: () => onExpandCell(context)),
|
||||
const _RowTrailing(),
|
||||
];
|
||||
|
||||
final child = Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: children,
|
||||
return Row(
|
||||
children: [
|
||||
const _RowLeading(),
|
||||
Expanded(child: _RowCells(cellCache: widget.cellCache, onExpand: () => _expandRow(context))),
|
||||
const _RowTrailing(),
|
||||
],
|
||||
);
|
||||
|
||||
return SizedBox(height: 42, child: child);
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -73,7 +68,7 @@ class _GridRowWidgetState extends State<GridRowWidget> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void onExpandCell(BuildContext context) {
|
||||
void _expandRow(BuildContext context) {
|
||||
final page = RowDetailPage(
|
||||
rowData: widget.rowData,
|
||||
rowCache: widget.rowCache,
|
||||
@ -159,13 +154,15 @@ class _RowCells extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<RowBloc, RowState>(
|
||||
buildWhen: (previous, current) => previous.cellDataMap.length != current.cellDataMap.length,
|
||||
buildWhen: (previous, current) => !listEquals(previous.snapshots, current.snapshots),
|
||||
builder: (context, state) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: _makeCells(context, state.cellDataMap),
|
||||
);
|
||||
return IntrinsicHeight(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: _makeCells(context, state.gridCellMap),
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -209,11 +206,14 @@ class _CellExpander extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return FlowyIconButton(
|
||||
width: 20,
|
||||
onPressed: onExpand,
|
||||
iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2),
|
||||
icon: svgWidget("grid/expander", color: theme.main1),
|
||||
return FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: FlowyIconButton(
|
||||
onPressed: onExpand,
|
||||
iconPadding: const EdgeInsets.fromLTRB(6, 6, 6, 6),
|
||||
fillColor: theme.surface,
|
||||
icon: svgWidget("grid/expander", color: theme.main1),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user