From 1abf0b565f2191416bbf16199f100f350145c97e Mon Sep 17 00:00:00 2001 From: appflowy Date: Mon, 4 Apr 2022 11:10:41 +0800 Subject: [PATCH] chore: cell highlight --- .../app_flowy/assets/translations/en.json | 3 +- .../grid/src/widgets/cell/cell_container.dart | 95 +++++++++++---- .../{selection.dart => extension.dart} | 112 +++++++++++++++--- .../cell/selection_cell/selection_cell.dart | 14 ++- .../cell/selection_cell/selection_editor.dart | 94 ++++++++++++++- .../grid/src/widgets/cell/text_cell.dart | 46 +++---- .../grid/src/widgets/header/grid_header.dart | 37 +++--- .../type_option/edit_option_pannel.dart | 4 +- frontend/app_flowy/pubspec.lock | 7 ++ frontend/app_flowy/pubspec.yaml | 2 +- .../flowy-grid/src/services/grid_editor.rs | 4 +- 11 files changed, 321 insertions(+), 97 deletions(-) rename frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/{selection.dart => extension.dart} (52%) diff --git a/frontend/app_flowy/assets/translations/en.json b/frontend/app_flowy/assets/translations/en.json index 9d73711cc7..55e57dfdc7 100644 --- a/frontend/app_flowy/assets/translations/en.json +++ b/frontend/app_flowy/assets/translations/en.json @@ -185,7 +185,8 @@ "aquaColor": "Aqua", "blueColor": "Blue", "deleteTag": "Delete tag", - "colorPannelTitle": "Colors" + "colorPannelTitle": "Colors", + "pannelTitle": "Select an option" } } } diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart index 0bfcc10caa..8890dc7576 100755 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart @@ -1,9 +1,23 @@ -import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:provider/provider.dart'; -class CellContainer extends StatefulWidget { +import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart'; + +class CellStateNotifier extends ChangeNotifier { + bool _isFocus = false; + + set isFocus(bool value) { + if (_isFocus != value) { + _isFocus = value; + notifyListeners(); + } + } + + bool get isFocus => _isFocus; +} + +class CellContainer extends StatelessWidget { final Widget child; final double width; const CellContainer({ @@ -12,28 +26,65 @@ class CellContainer extends StatefulWidget { required this.width, }) : super(key: key); - @override - State createState() => _CellContainerState(); -} - -class _CellContainerState extends State { @override Widget build(BuildContext context) { - final theme = context.watch(); - final borderSide = BorderSide(color: theme.shader4, width: 0.4); - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () {}, - child: Container( - constraints: BoxConstraints( - maxWidth: widget.width, - ), - decoration: BoxDecoration( - border: Border(right: borderSide, bottom: borderSide), - ), - padding: GridSize.cellContentInsets, - child: Center(child: IntrinsicHeight(child: widget.child)), + return ChangeNotifierProvider( + create: (_) => CellStateNotifier(), + child: Consumer( + builder: (context, state, _) { + return Container( + constraints: BoxConstraints( + maxWidth: width, + ), + decoration: _makeBoxDecoration(context, state), + padding: GridSize.cellContentInsets, + child: Center(child: IntrinsicHeight(child: child)), + ); + }, ), ); } + + BoxDecoration _makeBoxDecoration(BuildContext context, CellStateNotifier state) { + final theme = context.watch(); + if (state.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)); + } + } +} + +abstract class GridCell extends StatefulWidget { + const GridCell({Key? key}) : super(key: key); + + void setFocus(BuildContext context, bool value) { + Provider.of(context, listen: false).isFocus = value; + } +} + +class CellFocusNode extends FocusNode { + VoidCallback? focusCallback; + + void addCallback(BuildContext context, VoidCallback callback) { + if (focusCallback != null) { + removeListener(focusCallback!); + } + focusCallback = () { + Provider.of(context, listen: false).isFocus = hasFocus; + callback(); + }; + + addListener(focusCallback!); + } + + @override + void dispose() { + if (focusCallback != null) { + removeListener(focusCallback!); + } + super.dispose(); + } } diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/extension.dart similarity index 52% rename from frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection.dart rename to frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/extension.dart index 8f0f3dfc78..7bf977d6a4 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/extension.dart @@ -5,23 +5,7 @@ import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; - -class SelectionBadge extends StatelessWidget { - final SelectOption option; - const SelectionBadge({required this.option, Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: option.color.make(context), - shape: BoxShape.rectangle, - borderRadius: BorderRadius.circular(6.0), - ), - child: FlowyText.medium(option.name, fontSize: 12), - ); - } -} +import 'package:textfield_tags/textfield_tags.dart'; extension SelectOptionColorExtension on SelectOptionColor { Color make(BuildContext context) { @@ -75,3 +59,97 @@ extension SelectOptionColorExtension on SelectOptionColor { } } } + +class SelectOptionTextField extends StatelessWidget { + final TextEditingController _controller; + final FocusNode _focusNode; + + SelectOptionTextField({ + TextEditingController? controller, + FocusNode? focusNode, + Key? key, + }) : _controller = controller ?? TextEditingController(), + _focusNode = focusNode ?? FocusNode(), + super(key: key); + + @override + Widget build(BuildContext context) { + return TextFieldTags( + textEditingController: _controller, + initialTags: ["abc", "bdf"], + focusNode: _focusNode, + textSeparators: const [' ', ','], + inputfieldBuilder: ( + BuildContext context, + TextEditingController editController, + FocusNode focusNode, + String? error, + void Function(String)? onChanged, + void Function(String)? onSubmitted, + ) { + return ((context, sc, tags, onTagDelegate) { + return TextField( + controller: editController, + focusNode: focusNode, + onChanged: (value) {}, + onEditingComplete: () => focusNode.unfocus(), + maxLines: 1, + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + decoration: InputDecoration( + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + isDense: true, + prefixIcon: _renderTags(tags, sc), + ), + ); + }); + }, + ); + } + + Widget? _renderTags(List tags, ScrollController sc) { + if (tags.isEmpty) { + return null; + } + + return SingleChildScrollView( + controller: sc, + scrollDirection: Axis.horizontal, + child: Row(children: [ + Container( + decoration: BoxDecoration( + color: Color.fromARGB(255, 74, 137, 92), + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(6.0), + ), + child: FlowyText.medium("efc", fontSize: 12), + ), + Container( + decoration: BoxDecoration( + color: Color.fromARGB(255, 74, 137, 92), + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(6.0), + ), + child: FlowyText.medium("abc", fontSize: 12), + ) + ]), + ); + } +} + +class SelectionBadge extends StatelessWidget { + final SelectOption option; + const SelectionBadge({required this.option, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: option.color.make(context), + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(6.0), + ), + child: FlowyText.medium(option.name, fontSize: 12), + ); + } +} diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_cell.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_cell.dart index 0b1b1828f3..8e7c59b02c 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_cell.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_cell.dart @@ -1,7 +1,10 @@ import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/workspace/application/grid/prelude.dart'; +import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart'; import 'package:flutter/material.dart'; +import 'extension.dart'; + class SingleSelectCell extends StatefulWidget { final FutureCellData cellData; @@ -15,22 +18,31 @@ class SingleSelectCell extends StatefulWidget { } class _SingleSelectCellState extends State { + late CellFocusNode _focusNode; late SelectionCellBloc _cellBloc; + late TextEditingController _controller; @override void initState() { _cellBloc = getIt(param1: widget.cellData); + _controller = TextEditingController(); + _focusNode = CellFocusNode(); super.initState(); } @override Widget build(BuildContext context) { - return Container(); + _focusNode.addCallback(context, () {}); + return SelectOptionTextField( + focusNode: _focusNode, + controller: _controller, + ); } @override Future dispose() async { _cellBloc.close(); + _focusNode.dispose(); super.dispose(); } } diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_editor.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_editor.dart index de25430066..98e1774425 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_editor.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_editor.dart @@ -1,37 +1,108 @@ import 'package:app_flowy/workspace/application/grid/cell_bloc/selection_editor_bloc.dart'; import 'package:app_flowy/workspace/application/grid/row/row_service.dart'; +import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart'; import 'package:flowy_infra/theme.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:app_flowy/generated/locale_keys.g.dart'; -import 'selection.dart'; +import 'extension.dart'; class SelectionEditor extends StatelessWidget { final GridCellData cellData; const SelectionEditor({required this.cellData, Key? key}) : super(key: key); + void show(BuildContext context) { + FlowyOverlay.of(context).insertWithAnchor( + widget: OverlayContainer( + child: this, + constraints: BoxConstraints.loose(const Size(240, 200)), + ), + identifier: toString(), + anchorContext: context, + anchorDirection: AnchorDirection.bottomWithLeftAligned, + ); + } + @override Widget build(BuildContext context) { return BlocProvider( create: (context) => SelectionEditorBloc(gridId: cellData.gridId, field: cellData.field), child: BlocBuilder( builder: (context, state) { - return Container(); + return Column( + children: const [ + _Title(), + VSpace(10), + _OptionList(), + ], + ); }, ), ); } } +class _OptionList extends StatelessWidget { + const _OptionList({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final cells = state.options.map((option) => _SelectionCell(option)).toList(); + final list = ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + itemCount: cells.length, + separatorBuilder: (context, index) { + return VSpace(GridSize.typeOptionSeparatorHeight); + }, + physics: StyledScrollPhysics(), + itemBuilder: (BuildContext context, int index) { + return cells[index]; + }, + ); + return list; + }, + ); + } +} + +class _Title extends StatelessWidget { + const _Title({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.typeOptionItemHeight, + child: FlowyText.medium(LocaleKeys.grid_selectOption_pannelTitle.tr(), fontSize: 12), + ); + } +} + class _SelectionCell extends StatelessWidget { final SelectOption option; - const _SelectionCell({required this.option, Key? key}) : super(key: key); + const _SelectionCell(this.option, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { final theme = context.watch(); + + // return FlowyButton( + // text: FlowyText.medium(fieldType.title(), fontSize: 12), + // hoverColor: theme.hover, + // onTap: () => onSelectField(fieldType), + // leftIcon: svgWidget(fieldType.iconName(), color: theme.iconColor), + // ); + return InkWell( onTap: () {}, child: FlowyHover( @@ -43,3 +114,20 @@ class _SelectionCell extends StatelessWidget { ); } } + +class SelectionBadge extends StatelessWidget { + final SelectOption option; + const SelectionBadge({required this.option, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: option.color.make(context), + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(6.0), + ), + child: FlowyText.medium(option.name, fontSize: 12), + ); + } +} diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart index e8c571bded..c4e272aeb4 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart @@ -1,13 +1,11 @@ import 'dart:async'; import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/workspace/application/grid/prelude.dart'; -import 'package:flowy_sdk/log.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'cell_container.dart'; -/// The interface of base cell. - -class GridTextCell extends StatefulWidget { +class GridTextCell extends GridCell { final FutureCellData cellData; const GridTextCell({ required this.cellData, @@ -19,21 +17,23 @@ class GridTextCell extends StatefulWidget { } class _GridTextCellState extends State { - late TextEditingController _controller; - Timer? _delayOperation; - final _focusNode = FocusNode(); late TextCellBloc _cellBloc; + late TextEditingController _controller; + late CellFocusNode _focusNode; + Timer? _delayOperation; @override void initState() { _cellBloc = getIt(param1: widget.cellData); _controller = TextEditingController(text: _cellBloc.state.content); - _focusNode.addListener(save); + _focusNode = CellFocusNode(); super.initState(); } @override Widget build(BuildContext context) { + _focusNode.addCallback(context, focusChanged); + return BlocProvider.value( value: _cellBloc, child: BlocConsumer( @@ -46,16 +46,8 @@ class _GridTextCellState extends State { return TextField( controller: _controller, focusNode: _focusNode, - onChanged: (value) { - Log.info("On change"); - save(); - }, - onEditingComplete: () { - Log.info("On complete"); - }, - onSubmitted: (value) { - Log.info("On submit"); - }, + onChanged: (value) => focusChanged(), + onEditingComplete: () => _focusNode.unfocus(), maxLines: 1, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), decoration: const InputDecoration( @@ -72,18 +64,18 @@ class _GridTextCellState extends State { @override Future dispose() async { _cellBloc.close(); - _focusNode.removeListener(save); _focusNode.dispose(); super.dispose(); } - Future save() async { - _delayOperation?.cancel(); - _delayOperation = Timer(const Duration(milliseconds: 300), () { - if (_cellBloc.isClosed == false) { - _cellBloc.add(TextCellEvent.updateText(_controller.text)); - } - }); - // and later, before the timer goes off... + Future focusChanged() async { + if (mounted) { + _delayOperation?.cancel(); + _delayOperation = Timer(const Duration(milliseconds: 300), () { + if (_cellBloc.isClosed == false) { + _cellBloc.add(TextCellEvent.updateText(_controller.text)); + } + }); + } } } diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/grid_header.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/grid_header.dart index 80a7a51d59..2a7baaa773 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/grid_header.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/grid_header.dart @@ -46,7 +46,7 @@ class _GridHeaderDelegate extends SliverPersistentHeaderDelegate { @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { - return _GridHeaderWidget(gridId: gridId, fields: fields, key: ObjectKey(fields)); + return _GridHeaderWidget(gridId: gridId, fields: fields); } @override @@ -73,27 +73,22 @@ class _GridHeaderWidget extends StatelessWidget { @override Widget build(BuildContext context) { final theme = context.watch(); - return BlocBuilder( - builder: (context, state) { - final cells = state.fields.map( - (field) => GridFieldCell( - GridFieldCellContext(gridId: gridId, field: field), - ), - ); - - final row = Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const _HeaderLeading(), - ...cells, - _HeaderTrailing(gridId: gridId), - ], - key: UniqueKey(), - ); - - return Container(height: GridSize.headerHeight, color: theme.surface, child: row); - }, + final cells = fields.map( + (field) => GridFieldCell( + GridFieldCellContext(gridId: gridId, field: field), + ), ); + + final row = Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const _HeaderLeading(), + ...cells, + _HeaderTrailing(gridId: gridId), + ], + ); + + return Container(height: GridSize.headerHeight, color: theme.surface, child: row); } } diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/edit_option_pannel.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/edit_option_pannel.dart index a69575a409..1893a77846 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/edit_option_pannel.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/edit_option_pannel.dart @@ -1,6 +1,6 @@ import 'package:app_flowy/workspace/application/grid/field/type_option/edit_option_bloc.dart'; import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart'; -import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection.dart'; +import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/extension.dart'; import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/type_option/widget.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; @@ -164,7 +164,7 @@ class _SelectOptionColorCell extends StatelessWidget { dimension: 16, child: Container( decoration: BoxDecoration( - color: color.color(context), + color: color.make(context), shape: BoxShape.circle, ), ), diff --git a/frontend/app_flowy/pubspec.lock b/frontend/app_flowy/pubspec.lock index abd76b4448..90e6dda24c 100644 --- a/frontend/app_flowy/pubspec.lock +++ b/frontend/app_flowy/pubspec.lock @@ -1148,6 +1148,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.11" + textfield_tags: + dependency: "direct main" + description: + name: textfield_tags + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" textstyle_extensions: dependency: transitive description: diff --git a/frontend/app_flowy/pubspec.yaml b/frontend/app_flowy/pubspec.yaml index ad1f6cd0e2..4d8ec095d5 100644 --- a/frontend/app_flowy/pubspec.yaml +++ b/frontend/app_flowy/pubspec.yaml @@ -67,7 +67,7 @@ dependencies: clipboard: ^0.1.3 connectivity_plus: 2.2.0 easy_localization: ^3.0.0 - + textfield_tags: ^2.0.0 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 443c1ee2bf..59840d1e4f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -303,12 +303,12 @@ impl ClientGridEditor { }; let field_metas = self.pad.read().await.get_field_metas(field_orders)?; - debug_assert!(field_metas.len() == expected_len); - if field_metas.len() != expected_len { + if expected_len != 0 && field_metas.len() != expected_len { tracing::error!( "This is a bug. The len of the field_metas should equal to {}", expected_len ); + debug_assert!(field_metas.len() == expected_len); } // field_metas.retain(|field_meta| field_meta.visibility); Ok(field_metas)