feat: using wrap to auto expand Multi-select or single select

This commit is contained in:
appflowy 2022-04-29 22:03:53 +08:00
parent 40443ced80
commit 1ad7e0ece2
11 changed files with 160 additions and 153 deletions

View file

@ -9,8 +9,8 @@ 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 => 12 * scale;
static double get cellVPadding => 8 * scale; static double get cellVPadding => 12 * 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';
@ -57,3 +63,114 @@ class GridCellRequestFocusNotifier extends ChangeNotifier {
} }
abstract class GridCellStyle {} abstract class GridCellStyle {}
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.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

@ -36,7 +36,7 @@ class _CheckboxCellState extends State<CheckboxCell> {
builder: (context, state) { builder: (context, state) {
final icon = state.isSelected ? svgWidget('editor/editor_check') : svgWidget('editor/editor_uncheck'); final icon = state.isSelected ? svgWidget('editor/editor_check') : svgWidget('editor/editor_uncheck');
return SizedBox( return SizedBox(
height: 42, height: 20,
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: FlowyIconButton( child: FlowyIconButton(

View file

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

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

@ -44,7 +44,6 @@ class _SingleSelectCellState extends State<SingleSelectCell> {
@override @override
void initState() { void initState() {
// Log.trace("init widget $hashCode");
final cellContext = _buildCellContext(); 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();
@ -64,8 +63,7 @@ class _SingleSelectCellState extends State<SingleSelectCell> {
if (children.isEmpty && widget.cellStyle != null) { if (children.isEmpty && widget.cellStyle != null) {
children.add(FlowyText.medium(widget.cellStyle!.placeholder, fontSize: 14, color: theme.shader3)); children.add(FlowyText.medium(widget.cellStyle!.placeholder, fontSize: 14, color: theme.shader3));
} }
return SizedBox( return SizedBox.expand(
height: 69,
child: InkWell( child: InkWell(
onTap: () { onTap: () {
widget.onFocus.value = true; widget.onFocus.value = true;
@ -75,7 +73,7 @@ class _SingleSelectCellState extends State<SingleSelectCell> {
() => widget.onFocus.value = false, () => widget.onFocus.value = false,
); );
}, },
child: ClipRRect(child: Row(children: children)), child: Center(child: Wrap(children: children)),
), ),
); );
}, },
@ -89,7 +87,6 @@ class _SingleSelectCellState extends State<SingleSelectCell> {
@override @override
Future<void> dispose() async { Future<void> dispose() async {
// Log.trace("dispose widget $hashCode");
_cellBloc.close(); _cellBloc.close();
super.dispose(); super.dispose();
} }
@ -148,7 +145,7 @@ class _MultiSelectCellState extends State<MultiSelectCell> {
() => widget.onFocus.value = false, () => widget.onFocus.value = false,
); );
}, },
child: ClipRRect(child: Row(children: children)), child: Wrap(children: children, spacing: 4, runSpacing: 4),
), ),
); );
}, },

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

@ -68,21 +68,18 @@ class _GridTextCellState extends State<GridTextCell> {
}, },
buildWhen: (previous, current) => previous.content != current.content, buildWhen: (previous, current) => previous.content != current.content,
builder: (context, state) { builder: (context, state) {
return SizedBox( return TextField(
height: 42, controller: _controller,
child: TextField( focusNode: _focusNode,
controller: _controller, onChanged: (value) => focusChanged(),
focusNode: _focusNode, onEditingComplete: () => _focusNode.unfocus(),
onChanged: (value) => focusChanged(), maxLines: null,
onEditingComplete: () => _focusNode.unfocus(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
maxLines: 1, decoration: InputDecoration(
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), contentPadding: EdgeInsets.zero,
decoration: InputDecoration( border: InputBorder.none,
contentPadding: EdgeInsets.zero, hintText: widget.cellStyle?.placeholder,
border: InputBorder.none, isDense: true,
hintText: widget.cellStyle?.placeholder,
isDense: true,
),
), ),
); );
}, },

View file

@ -206,9 +206,11 @@ class _CellExpander extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.watch<AppTheme>(); final theme = context.watch<AppTheme>();
return FlowyIconButton( return FlowyIconButton(
width: 20, width: 30,
height: 24,
onPressed: onExpand, onPressed: onExpand,
iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2), iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
fillColor: theme.surface,
icon: svgWidget("grid/expander", color: theme.main1), icon: svgWidget("grid/expander", color: theme.main1),
); );
} }