diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart index e979b2510e..3968c6463c 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart @@ -23,7 +23,7 @@ class EditableRowNotifier { EditableRowNotifier({required bool isEditing}) : isEditing = ValueNotifier(isEditing); - void insertCell( + void bindCell( GridCellIdentifier cellIdentifier, EditableCellNotifier notifier, ) { @@ -59,7 +59,7 @@ class EditableRowNotifier { _cells.values.first.isCellEditing.value = false; } - void clear() { + void unbind() { for (final notifier in _cells.values) { notifier.dispose(); } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart index 76a5f84bbf..907950fcc6 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart @@ -3,7 +3,6 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_servic import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; - import 'define.dart'; class BoardNumberCell extends StatefulWidget { diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index 6469a4e106..583bb08b7c 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -2,6 +2,8 @@ import 'package:app_flowy/plugins/board/application/card/board_select_option_cel import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart'; +import 'package:appflowy_popover/popover.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -26,9 +28,11 @@ class BoardSelectOptionCell extends StatefulWidget with EditableCell { class _BoardSelectOptionCellState extends State { late BoardSelectOptionCellBloc _cellBloc; + late PopoverController _popover; @override void initState() { + _popover = PopoverController(); final cellController = widget.cellControllerBuilder.build() as GridSelectOptionCellController; _cellBloc = BoardSelectOptionCellBloc(cellController: cellController) @@ -41,43 +45,60 @@ class _BoardSelectOptionCellState extends State { return BlocProvider.value( value: _cellBloc, child: BlocBuilder( - buildWhen: (previous, current) { - return previous.selectedOptions != current.selectedOptions; - }, - builder: (context, state) { - if (state.selectedOptions - .where((element) => element.id == widget.groupId) - .isNotEmpty || - state.selectedOptions.isEmpty) { - return const SizedBox(); - } else { - final children = state.selectedOptions - .map( - (option) => SelectOptionTag.fromOption( - context: context, - option: option, - onSelected: () { - SelectOptionCellEditor.show( - context: context, - cellController: widget.cellControllerBuilder.build() - as GridSelectOptionCellController, - ); - }, - ), - ) - .toList(); + buildWhen: (previous, current) { + return previous.selectedOptions != current.selectedOptions; + }, builder: (context, state) { + // Returns SizedBox if the content of the cell is empty + if (_isEmpty(state)) return const SizedBox(); - return IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: SizedBox.expand( - child: Wrap(spacing: 4, runSpacing: 2, children: children), - ), - ), + final children = state.selectedOptions.map( + (option) { + final tag = SelectOptionTag.fromOption( + context: context, + option: option, + onSelected: () => _popover.show(), ); - } - }, - ), + return _wrapPopover(tag); + }, + ).toList(); + + return IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: SizedBox.expand( + child: Wrap(spacing: 4, runSpacing: 2, children: children), + ), + ), + ); + }), + ); + } + + bool _isEmpty(BoardSelectOptionCellState state) { + // The cell should hide if the option id is equal to the groupId. + final isInGroup = state.selectedOptions + .where((element) => element.id == widget.groupId) + .isNotEmpty; + return isInGroup || state.selectedOptions.isEmpty; + } + + Widget _wrapPopover(Widget child) { + final constraints = BoxConstraints.loose(Size( + SelectOptionCellEditor.editorPanelWidth, + 300, + )); + return AppFlowyStylePopover( + controller: _popover, + constraints: constraints, + direction: PopoverDirection.bottomWithLeftAligned, + popupBuilder: (BuildContext context) { + return SelectOptionCellEditor( + cellController: widget.cellControllerBuilder.build() + as GridSelectOptionCellController, + ); + }, + onClose: () {}, + child: child, ); } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index f5111224e1..545fd70470 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -1,6 +1,5 @@ import 'package:app_flowy/plugins/board/application/card/card_bloc.dart'; import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart'; -import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_action_sheet.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; @@ -64,10 +63,16 @@ class _BoardCardState extends State { value: _cardBloc, child: BlocBuilder( buildWhen: (previous, current) { + // Rebuild when: + // 1.If the lenght of the cells is not the same + // 2.isEditing changed if (previous.cells.length != current.cells.length || previous.isEditing != current.isEditing) { return true; } + + // 3.Compare the content of the cells. The cells consisits of + // list of [BoardCellEquatable] that extends the [Equatable]. return !listEquals(previous.cells, current.cells); }, builder: (context, state) { @@ -75,21 +80,16 @@ class _BoardCardState extends State { buildAccessoryWhen: () => state.isEditing == false, accessoryBuilder: (context) { return [ - _CardEditOption( - startEditing: () => rowNotifier.becomeFirstResponder(), - ), + _CardEditOption(rowNotifier: rowNotifier), const _CardMoreOption(), ]; }, - onTap: (context) { - widget.openCard(context); - }, - child: Column( - mainAxisSize: MainAxisSize.min, - children: _makeCells( - context, - state.cells.map((cell) => cell.identifier).toList(), - ), + onTap: (context) => widget.openCard(context), + child: _CellColumn( + groupId: widget.groupId, + rowNotifier: rowNotifier, + cellBuilder: widget.cellBuilder, + cells: state.cells, ), ); }, @@ -97,36 +97,62 @@ class _BoardCardState extends State { ); } + @override + Future dispose() async { + rowNotifier.dispose(); + _cardBloc.close(); + super.dispose(); + } +} + +class _CellColumn extends StatelessWidget { + final String groupId; + final BoardCellBuilder cellBuilder; + final EditableRowNotifier rowNotifier; + final List cells; + const _CellColumn({ + required this.groupId, + required this.rowNotifier, + required this.cellBuilder, + required this.cells, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: _makeCells(context, cells), + ); + } + List _makeCells( BuildContext context, - List cells, + List cells, ) { final List children = []; - rowNotifier.clear(); + // Remove all the cell listeners. + rowNotifier.unbind(); + cells.asMap().forEach( - (int index, GridCellIdentifier cellId) { - EditableCellNotifier cellNotifier; + (int index, BoardCellEquatable cell) { + final isEditing = index == 0 ? rowNotifier.isEditing.value : false; + final cellNotifier = EditableCellNotifier(isEditing: isEditing); + if (index == 0) { // Only use the first cell to receive user's input when click the edit // button - cellNotifier = EditableCellNotifier( - isEditing: rowNotifier.isEditing.value, - ); - rowNotifier.insertCell(cellId, cellNotifier); - } else { - cellNotifier = EditableCellNotifier(); + rowNotifier.bindCell(cell.identifier, cellNotifier); } - Widget child = widget.cellBuilder.buildCell( - widget.groupId, - cellId, - cellNotifier, - ); - - child = Padding( - key: cellId.key(), + final child = Padding( + key: cell.identifier.key(), padding: const EdgeInsets.only(left: 4, right: 4), - child: child, + child: cellBuilder.buildCell( + groupId, + cell.identifier, + cellNotifier, + ), ); children.add(child); @@ -134,13 +160,6 @@ class _BoardCardState extends State { ); return children; } - - @override - Future dispose() async { - rowNotifier.dispose(); - _cardBloc.close(); - super.dispose(); - } } class _CardMoreOption extends StatelessWidget with CardAccessory { @@ -164,9 +183,9 @@ class _CardMoreOption extends StatelessWidget with CardAccessory { } class _CardEditOption extends StatelessWidget with CardAccessory { - final VoidCallback startEditing; + final EditableRowNotifier rowNotifier; const _CardEditOption({ - required this.startEditing, + required this.rowNotifier, Key? key, }) : super(key: key); @@ -183,6 +202,6 @@ class _CardEditOption extends StatelessWidget with CardAccessory { @override void onTap(BuildContext context) { - startEditing(); + rowNotifier.becomeFirstResponder(); } } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart index 7479f3044e..c79a582661 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart @@ -72,50 +72,44 @@ class CardAccessoryContainer extends StatelessWidget { Widget build(BuildContext context) { final theme = context.read(); final children = accessories.map((accessory) { - final hover = FlowyHover( - style: HoverStyle( - hoverColor: theme.hover, - backgroundColor: theme.surface, - borderRadius: BorderRadius.zero, - ), - builder: (_, onHover) => SizedBox( - width: 24, - height: 24, - child: accessory, - ), - ); return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => accessory.onTap(context), - child: hover, + child: _wrapHover(theme, accessory), ); }).toList(); + return _wrapDecoration(context, Row(children: children)); + } - return Container( - clipBehavior: Clip.hardEdge, - decoration: _makeBoxDecoration(context), - child: Row(children: children), + FlowyHover _wrapHover(AppTheme theme, CardAccessory accessory) { + return FlowyHover( + style: HoverStyle( + hoverColor: theme.hover, + backgroundColor: theme.surface, + borderRadius: BorderRadius.zero, + ), + builder: (_, onHover) => SizedBox( + width: 24, + height: 24, + child: accessory, + ), ); } -} -BoxDecoration _makeBoxDecoration(BuildContext context) { - final theme = context.read(); - final borderSide = BorderSide(color: theme.shader6, width: 1.0); - return BoxDecoration( - color: Colors.transparent, - border: Border.fromBorderSide(borderSide), - // boxShadow: const [ - // BoxShadow( - // color: Colors.transparent, - // spreadRadius: 0, - // blurRadius: 5, - // offset: Offset.zero, - // ) - // ], - - borderRadius: const BorderRadius.all(Radius.circular(4)), - ); + Widget _wrapDecoration(BuildContext context, Widget child) { + final theme = context.read(); + final borderSide = BorderSide(color: theme.shader6, width: 1.0); + final decoration = BoxDecoration( + color: Colors.transparent, + border: Border.fromBorderSide(borderSide), + borderRadius: const BorderRadius.all(Radius.circular(4)), + ); + return Container( + clipBehavior: Clip.hardEdge, + decoration: decoration, + child: child, + ); + } } class _CardEnterRegion extends StatelessWidget { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart index 2e65226289..3db0d75797 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart @@ -164,67 +164,65 @@ class _SelectOptionWrapState extends State { @override Widget build(BuildContext context) { final theme = context.watch(); - final Widget child; - if (widget.selectOptions.isEmpty && widget.cellStyle != null) { - child = Align( - alignment: Alignment.centerLeft, - child: FlowyText.medium( - widget.cellStyle!.placeholder, - fontSize: 14, - color: theme.shader3, - ), - ); - } else { - child = Align( - alignment: Alignment.centerLeft, - child: Wrap( - spacing: 4, - runSpacing: 2, - children: widget.selectOptions - .map((option) => SelectOptionTag.fromOption( - context: context, - option: option, - )) - .toList(), - ), - ); - } + Widget child = _buildOptions(theme, context); return Stack( alignment: AlignmentDirectional.center, fit: StackFit.expand, children: [ - AppFlowyStylePopover( - controller: _popover, - constraints: BoxConstraints.loose( - Size(SelectOptionCellEditor.editorPanelWidth, 300)), - offset: const Offset(0, 20), - direction: PopoverDirection.bottomWithLeftAligned, - // triggerActions: PopoverTriggerActionFlags.c, - popupBuilder: (BuildContext context) { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - widget.onFocus?.call(true); - }); - return SizedBox( - width: SelectOptionCellEditor.editorPanelWidth, - child: SelectOptionCellEditor( - cellController: widget.cellControllerBuilder.build() - as GridSelectOptionCellController, - onDismissed: () { - widget.onFocus?.call(false); - }, - ), - ); - }, - onClose: () { - widget.onFocus?.call(false); - }, - child: child, - ), - InkWell(onTap: () { - _popover.show(); - }), + _wrapPopover(child), + InkWell(onTap: () => _popover.show()), ], ); } + + Widget _wrapPopover(Widget child) { + final constraints = BoxConstraints.loose(Size( + SelectOptionCellEditor.editorPanelWidth, + 300, + )); + return AppFlowyStylePopover( + controller: _popover, + constraints: constraints, + direction: PopoverDirection.bottomWithLeftAligned, + popupBuilder: (BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.onFocus?.call(true); + }); + return SelectOptionCellEditor( + cellController: widget.cellControllerBuilder.build() + as GridSelectOptionCellController, + ); + }, + onClose: () => widget.onFocus?.call(false), + child: child, + ); + } + + Widget _buildOptions(AppTheme theme, BuildContext context) { + final Widget child; + if (widget.selectOptions.isEmpty && widget.cellStyle != null) { + child = FlowyText.medium( + widget.cellStyle!.placeholder, + fontSize: 14, + color: theme.shader3, + ); + } else { + final children = widget.selectOptions.map( + (option) { + return SelectOptionTag.fromOption( + context: context, + option: option, + ); + }, + ).toList(); + + child = Wrap( + spacing: 4, + runSpacing: 2, + children: children, + ); + } + return Align(alignment: Alignment.centerLeft, child: child); + } } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart index 1c0cad85b1..fecb5176f3 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart @@ -25,17 +25,13 @@ import 'text_field.dart'; const double _editorPanelWidth = 300; -class SelectOptionCellEditor extends StatelessWidget with FlowyOverlayDelegate { +class SelectOptionCellEditor extends StatelessWidget { final GridSelectOptionCellController cellController; - final VoidCallback? onDismissed; static double editorPanelWidth = 300; - const SelectOptionCellEditor({ - required this.cellController, - this.onDismissed, - Key? key, - }) : super(key: key); + const SelectOptionCellEditor({required this.cellController, Key? key}) + : super(key: key); @override Widget build(BuildContext context) { @@ -60,44 +56,6 @@ class SelectOptionCellEditor extends StatelessWidget with FlowyOverlayDelegate { ), ); } - - static void show({ - required BuildContext context, - required GridSelectOptionCellController cellController, - VoidCallback? onDismissed, - }) { - SelectOptionCellEditor.remove(context); - final editor = SelectOptionCellEditor( - cellController: cellController, - onDismissed: onDismissed, - ); - - // - FlowyOverlay.of(context).insertWithAnchor( - widget: OverlayContainer( - constraints: BoxConstraints.loose(const Size(_editorPanelWidth, 300)), - child: SizedBox(width: _editorPanelWidth, child: editor), - ), - identifier: SelectOptionCellEditor.identifier(), - anchorContext: context, - anchorDirection: AnchorDirection.bottomWithCenterAligned, - delegate: editor, - ); - } - - static void remove(BuildContext context) { - FlowyOverlay.of(context).remove(identifier()); - } - - static String identifier() { - return (SelectOptionCellEditor).toString(); - } - - @override - bool asBarrier() => true; - - @override - void didRemove() => onDismissed?.call(); } class _OptionList extends StatelessWidget { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart index 4436e61fb6..d7300ef799 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart @@ -3,6 +3,7 @@ import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; import 'package:appflowy_popover/popover.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; @@ -29,7 +30,8 @@ class GridFieldCell extends StatelessWidget { }, child: BlocBuilder( builder: (context, state) { - final button = Popover( + final button = AppFlowyStylePopover( + constraints: BoxConstraints.loose(const Size(240, 840)), direction: PopoverDirection.bottomWithLeftAligned, triggerActions: PopoverTriggerActionFlags.click, offset: const Offset(0, 10), diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart index 4d412ed530..6f3e583e35 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart @@ -3,9 +3,9 @@ import 'package:app_flowy/plugins/grid/presentation/widgets/header/field_editor. import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/plugins/grid/application/prelude.dart'; import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_popover/popover.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; @@ -32,38 +32,32 @@ class _GridFieldCellActionSheetState extends State { Widget build(BuildContext context) { if (_showFieldEditor) { final field = widget.cellContext.field; - return OverlayContainer( - constraints: BoxConstraints.loose(const Size(240, 200)), - child: FieldEditor( + return FieldEditor( + gridId: widget.cellContext.gridId, + fieldName: field.name, + typeOptionLoader: FieldTypeOptionLoader( gridId: widget.cellContext.gridId, - fieldName: field.name, - typeOptionLoader: FieldTypeOptionLoader( - gridId: widget.cellContext.gridId, - field: field, - ), + field: field, ), ); } return BlocProvider( create: (context) => getIt(param1: widget.cellContext), - child: OverlayContainer( - constraints: BoxConstraints.loose(const Size(240, 200)), - child: SingleChildScrollView( - child: Column( - children: [ - _EditFieldButton( - cellContext: widget.cellContext, - onTap: () { - setState(() { - _showFieldEditor = true; - }); - }, - ), - const VSpace(6), - _FieldOperationList(widget.cellContext, () {}), - ], - ), + child: SingleChildScrollView( + child: Column( + children: [ + _EditFieldButton( + cellContext: widget.cellContext, + onTap: () { + setState(() { + _showFieldEditor = true; + }); + }, + ), + const VSpace(6), + _FieldOperationList(widget.cellContext, () {}), + ], ), ), ); @@ -159,8 +153,11 @@ class FieldActionCell extends StatelessWidget { Widget build(BuildContext context) { final theme = context.watch(); return FlowyButton( - text: FlowyText.medium(action.title(), - fontSize: 12, color: enable ? null : theme.shader4), + text: FlowyText.medium( + action.title(), + fontSize: 12, + color: enable ? null : theme.shader4, + ), hoverColor: theme.hover, onTap: () { if (enable) { @@ -168,8 +165,10 @@ class FieldActionCell extends StatelessWidget { onTap(); } }, - leftIcon: svgWidget(action.iconName(), - color: enable ? theme.iconColor : theme.disableIconColor), + leftIcon: svgWidget( + action.iconName(), + color: enable ? theme.iconColor : theme.disableIconColor, + ), ); } } @@ -216,6 +215,7 @@ extension _FieldActionExtension on FieldAction { .add(const FieldActionSheetEvent.duplicateField()); break; case FieldAction.delete: + PopoverContainer.of(context).close(); NavigatorAlertDialog( title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), confirm: () { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart index cc836809b8..12058ccecc 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart @@ -7,12 +7,12 @@ import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/rounded_input_field.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_sdk/log.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:app_flowy/generated/locale_keys.g.dart'; -import 'field_name_input.dart'; import 'field_type_option_editor.dart'; class FieldEditor extends StatefulWidget { @@ -44,6 +44,12 @@ class _FieldEditorState extends State { super.initState(); } + @override + void dispose() { + popoverMutex.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return BlocProvider( @@ -58,21 +64,14 @@ class _FieldEditorState extends State { return ListView( shrinkWrap: true, children: [ - FlowyText.medium(LocaleKeys.grid_field_editProperty.tr(), - fontSize: 12), - const VSpace(10), - const _FieldNameCell(), - const VSpace(10), - _DeleteFieldButton( - popoverMutex: popoverMutex, - onDeleted: () { - state.field.fold( - () => Log.error('Can not delete the field'), - (field) => widget.onDeleted?.call(field.id), - ); - }, + FlowyText.medium( + LocaleKeys.grid_field_editProperty.tr(), + fontSize: 12, ), const VSpace(10), + _FieldNameTextField(popoverMutex: popoverMutex), + const VSpace(10), + ..._addDeleteFieldButton(state), _FieldTypeOptionCell(popoverMutex: popoverMutex), ], ); @@ -80,6 +79,23 @@ class _FieldEditorState extends State { ), ); } + + List _addDeleteFieldButton(FieldEditorState state) { + if (widget.onDeleted == null) { + return []; + } + return [ + _DeleteFieldButton( + popoverMutex: popoverMutex, + onDeleted: () { + state.field.fold( + () => Log.error('Can not delete the field'), + (field) => widget.onDeleted?.call(field.id), + ); + }, + ), + ]; + } } class _FieldTypeOptionCell extends StatelessWidget { @@ -111,25 +127,89 @@ class _FieldTypeOptionCell extends StatelessWidget { } } -class _FieldNameCell extends StatelessWidget { - const _FieldNameCell({Key? key}) : super(key: key); +class _FieldNameTextField extends StatefulWidget { + final PopoverMutex popoverMutex; + const _FieldNameTextField({ + required this.popoverMutex, + Key? key, + }) : super(key: key); + + @override + State<_FieldNameTextField> createState() => _FieldNameTextFieldState(); +} + +class _FieldNameTextFieldState extends State<_FieldNameTextField> { + late String name; + FocusNode focusNode = FocusNode(); + VoidCallback? _popoverCallback; + TextEditingController controller = TextEditingController(); + + @override + void initState() { + focusNode.addListener(() { + if (focusNode.hasFocus) { + widget.popoverMutex.close(); + } + }); + + super.initState(); + } @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return FieldNameTextField( - name: state.name, - errorText: context.read().state.errorText, - onNameChanged: (newName) { - context - .read() - .add(FieldEditorEvent.updateName(newName)); - }, - ); + final theme = context.watch(); + + controller.text = context.read().state.name; + return BlocListener( + listenWhen: (previous, current) => previous.name != current.name, + listener: (context, state) { + controller.text = state.name; }, + child: BlocBuilder( + builder: (context, state) { + listenOnPopoverChhanged(context); + + return RoundedInputField( + height: 36, + autoFocus: true, + focusNode: focusNode, + style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500), + controller: controller, + normalBorderColor: theme.shader4, + errorBorderColor: theme.red, + focusBorderColor: theme.main1, + cursorColor: theme.main1, + errorText: context.read().state.errorText, + onChanged: (newName) { + context + .read() + .add(FieldEditorEvent.updateName(newName)); + }, + ); + }, + ), ); } + + void listenOnPopoverChhanged(BuildContext context) { + if (_popoverCallback != null) { + widget.popoverMutex.removePopoverStateListener(_popoverCallback!); + } + _popoverCallback = widget.popoverMutex.listenOnPopoverStateChanged(() { + if (focusNode.hasFocus) { + final node = FocusScope.of(context); + node.unfocus(); + } + }); + } + + @override + void didUpdateWidget(covariant _FieldNameTextField oldWidget) { + controller.selection = TextSelection.fromPosition( + TextPosition(offset: controller.text.length)); + + super.didUpdateWidget(oldWidget); + } } class _DeleteFieldButton extends StatelessWidget { @@ -171,12 +251,10 @@ class _DeleteFieldButton extends StatelessWidget { popupBuilder: (popupContext) { return PopoverAlertView( title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), - cancel: () => popoverMutex.state?.close(), + cancel: () {}, confirm: () { onDeleted?.call(); - popoverMutex.state?.close(); }, - popoverMutex: popoverMutex, ); }, child: widget, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_name_input.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_name_input.dart deleted file mode 100644 index 365064e33f..0000000000 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_name_input.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flowy_infra/theme.dart'; -import 'package:flowy_infra_ui/widget/rounded_input_field.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class FieldNameTextField extends StatefulWidget { - final void Function(String) onNameChanged; - final String name; - final String errorText; - const FieldNameTextField({ - required this.name, - required this.errorText, - required this.onNameChanged, - Key? key, - }) : super(key: key); - - @override - State createState() => _FieldNameTextFieldState(); -} - -class _FieldNameTextFieldState extends State { - late String name; - TextEditingController controller = TextEditingController(); - - @override - void initState() { - controller.text = widget.name; - super.initState(); - } - - @override - Widget build(BuildContext context) { - final theme = context.watch(); - return RoundedInputField( - height: 36, - autoFocus: true, - style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500), - controller: controller, - normalBorderColor: theme.shader4, - errorBorderColor: theme.red, - focusBorderColor: theme.main1, - cursorColor: theme.main1, - errorText: widget.errorText, - onChanged: widget.onNameChanged, - ); - } - - @override - void didUpdateWidget(covariant FieldNameTextField oldWidget) { - controller.text = widget.name; - controller.selection = TextSelection.fromPosition( - TextPosition(offset: controller.text.length)); - - super.didUpdateWidget(oldWidget); - } -} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_list.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_list.dart index 0cb18d411b..2c6f262180 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_list.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_list.dart @@ -47,10 +47,6 @@ class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate { ), ); } - - static String identifier() { - return (FieldTypeList).toString(); - } } class FieldTypeCell extends StatelessWidget { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart index f541335844..fcbb211fc1 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart @@ -66,7 +66,8 @@ class FieldTypeOptionEditor extends StatelessWidget { height: GridSize.typeOptionItemHeight, child: AppFlowyStylePopover( constraints: BoxConstraints.loose(const Size(460, 440)), - triggerActions: PopoverTriggerActionFlags.click, + triggerActions: + PopoverTriggerActionFlags.click | PopoverTriggerActionFlags.hover, mutex: popoverMutex, offset: const Offset(20, 0), popupBuilder: (context) { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart index f01efda675..e00eda5ec0 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart @@ -50,7 +50,9 @@ Widget? makeTypeOptionWidget({ required PopoverMutex popoverMutex, }) { final builder = makeTypeOptionWidgetBuilder( - dataController: dataController, popoverMutex: popoverMutex); + dataController: dataController, + popoverMutex: popoverMutex, + ); return builder.build(context); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart index e8cbb00fba..2e9201324c 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart @@ -75,7 +75,7 @@ class DateTypeOptionWidget extends TypeOptionWidget { context .read() .add(DateTypeOptionEvent.didSelectDateFormat(format)); - PopoverContainer.of(popoverContext).closeAll(); + PopoverContainer.of(popoverContext).close(); }, ); }, @@ -97,7 +97,7 @@ class DateTypeOptionWidget extends TypeOptionWidget { context .read() .add(DateTypeOptionEvent.didSelectTimeFormat(format)); - PopoverContainer.of(popoverContext).closeAll(); + PopoverContainer.of(popoverContext).close(); }, ); }, @@ -201,12 +201,10 @@ class DateFormatList extends StatelessWidget { Widget build(BuildContext context) { final cells = DateFormat.values.map((format) { return DateFormatCell( - dateFormat: format, - onSelected: (format) { - onSelected(format); - FlowyOverlay.of(context).remove(DateFormatList.identifier()); - }, - isSelected: selectedFormat == format); + dateFormat: format, + onSelected: onSelected, + isSelected: selectedFormat == format, + ); }).toList(); return SizedBox( @@ -224,10 +222,6 @@ class DateFormatList extends StatelessWidget { ), ); } - - static String identifier() { - return (DateFormatList).toString(); - } } class DateFormatCell extends StatelessWidget { @@ -291,12 +285,10 @@ class TimeFormatList extends StatelessWidget { Widget build(BuildContext context) { final cells = TimeFormat.values.map((format) { return TimeFormatCell( - isSelected: format == selectedFormat, - timeFormat: format, - onSelected: (format) { - onSelected(format); - FlowyOverlay.of(context).remove(TimeFormatList.identifier()); - }); + isSelected: format == selectedFormat, + timeFormat: format, + onSelected: onSelected, + ); }).toList(); return SizedBox( @@ -314,10 +306,6 @@ class TimeFormatList extends StatelessWidget { ), ); } - - static String identifier() { - return (TimeFormatList).toString(); - } } class TimeFormatCell extends StatelessWidget { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart index 80627d062d..8651a18515 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart @@ -82,7 +82,7 @@ class NumberTypeOptionWidget extends TypeOptionWidget { context .read() .add(NumberTypeOptionEvent.didSelectFormat(format)); - PopoverContainer.of(popoverContext).closeAll(); + PopoverContainer.of(popoverContext).close(); }, selectedFormat: state.typeOption.format, ); @@ -123,8 +123,6 @@ class NumberFormatList extends StatelessWidget { format: format, onSelected: (format) { onSelected(format); - FlowyOverlay.of(context) - .remove(NumberFormatList.identifier()); }); }).toList(); @@ -147,10 +145,6 @@ class NumberFormatList extends StatelessWidget { ), ); } - - static String identifier() { - return (NumberFormatList).toString(); - } } class NumberFormatCell extends StatelessWidget { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option.dart index f8cf09d076..0a7dc481d1 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option.dart @@ -207,13 +207,13 @@ class _OptionCellState extends State<_OptionCell> { context .read() .add(SelectOptionTypeOptionEvent.deleteOption(widget.option)); - PopoverContainer.of(popoverContext).closeAll(); + PopoverContainer.of(popoverContext).close(); }, onUpdated: (updatedOption) { context .read() .add(SelectOptionTypeOptionEvent.updateOption(updatedOption)); - PopoverContainer.of(popoverContext).closeAll(); + PopoverContainer.of(popoverContext).close(); }, key: ValueKey(widget.option.id), ); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart index 724ec74cc1..39331ce151 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart @@ -131,8 +131,10 @@ class _GridPropertyCell extends StatelessWidget { return FieldEditor( gridId: gridId, fieldName: fieldContext.name, - typeOptionLoader: - FieldTypeOptionLoader(gridId: gridId, field: fieldContext.field), + typeOptionLoader: FieldTypeOptionLoader( + gridId: gridId, + field: fieldContext.field, + ), ); }, ); diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/dialogs.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/dialogs.dart index 4cb763e50d..296f64d86d 100644 --- a/frontend/app_flowy/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/dialogs.dart @@ -1,4 +1,3 @@ -import 'package:appflowy_popover/popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/text_style.dart'; import 'package:flowy_infra/theme.dart'; @@ -88,13 +87,11 @@ class _CreateTextFieldDialog extends State { } class PopoverAlertView extends StatelessWidget { - final PopoverMutex popoverMutex; final String title; final void Function()? cancel; final void Function()? confirm; const PopoverAlertView({ - required this.popoverMutex, required this.title, this.confirm, this.cancel, diff --git a/frontend/app_flowy/packages/appflowy_popover/lib/popover.dart b/frontend/app_flowy/packages/appflowy_popover/lib/popover.dart index 6f9f4e9f33..ff090e347e 100644 --- a/frontend/app_flowy/packages/appflowy_popover/lib/popover.dart +++ b/frontend/app_flowy/packages/appflowy_popover/lib/popover.dart @@ -6,7 +6,42 @@ import 'package:flutter/services.dart'; /// If multiple popovers are exclusive, /// pass the same mutex to them. class PopoverMutex { - PopoverState? state; + final ValueNotifier _stateNofitier = ValueNotifier(null); + PopoverMutex(); + + void removePopoverStateListener(VoidCallback listener) { + _stateNofitier.removeListener(listener); + } + + VoidCallback listenOnPopoverStateChanged(VoidCallback callback) { + listenerCallback() { + callback(); + } + + _stateNofitier.addListener(listenerCallback); + return listenerCallback; + } + + void close() { + _stateNofitier.value?.close(); + } + + PopoverState? get state => _stateNofitier.value; + + set state(PopoverState? newState) { + if (_stateNofitier.value != null && _stateNofitier.value != newState) { + _stateNofitier.value?.close(); + } + _stateNofitier.value = newState; + } + + void _removeState() { + _stateNofitier.value = null; + } + + void dispose() { + _stateNofitier.dispose(); + } } class PopoverController { @@ -109,11 +144,7 @@ class PopoverState extends State { close(); if (widget.mutex != null) { - if (widget.mutex!.state != null && widget.mutex!.state != this) { - widget.mutex!.state!.close(); - } - - widget.mutex!.state = this; + widget.mutex?.state = this; } if (_popoverWithMask == null) { @@ -163,7 +194,7 @@ class PopoverState extends State { } if (widget.mutex?.state == this) { - widget.mutex!.state = null; + widget.mutex?._removeState(); } }