Merge pull request #1085 from AppFlowy-IO/fix/popover_bugs

Fix/popover bugs
This commit is contained in:
Nathan.fooo
2022-09-18 22:20:16 +08:00
committed by GitHub
20 changed files with 405 additions and 381 deletions

View File

@ -23,7 +23,7 @@ class EditableRowNotifier {
EditableRowNotifier({required bool isEditing}) EditableRowNotifier({required bool isEditing})
: isEditing = ValueNotifier(isEditing); : isEditing = ValueNotifier(isEditing);
void insertCell( void bindCell(
GridCellIdentifier cellIdentifier, GridCellIdentifier cellIdentifier,
EditableCellNotifier notifier, EditableCellNotifier notifier,
) { ) {
@ -59,7 +59,7 @@ class EditableRowNotifier {
_cells.values.first.isCellEditing.value = false; _cells.values.first.isCellEditing.value = false;
} }
void clear() { void unbind() {
for (final notifier in _cells.values) { for (final notifier in _cells.values) {
notifier.dispose(); notifier.dispose();
} }

View File

@ -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:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'define.dart'; import 'define.dart';
class BoardNumberCell extends StatefulWidget { class BoardNumberCell extends StatefulWidget {

View File

@ -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/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/extension.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -26,9 +28,11 @@ class BoardSelectOptionCell extends StatefulWidget with EditableCell {
class _BoardSelectOptionCellState extends State<BoardSelectOptionCell> { class _BoardSelectOptionCellState extends State<BoardSelectOptionCell> {
late BoardSelectOptionCellBloc _cellBloc; late BoardSelectOptionCellBloc _cellBloc;
late PopoverController _popover;
@override @override
void initState() { void initState() {
_popover = PopoverController();
final cellController = final cellController =
widget.cellControllerBuilder.build() as GridSelectOptionCellController; widget.cellControllerBuilder.build() as GridSelectOptionCellController;
_cellBloc = BoardSelectOptionCellBloc(cellController: cellController) _cellBloc = BoardSelectOptionCellBloc(cellController: cellController)
@ -41,43 +45,60 @@ class _BoardSelectOptionCellState extends State<BoardSelectOptionCell> {
return BlocProvider.value( return BlocProvider.value(
value: _cellBloc, value: _cellBloc,
child: BlocBuilder<BoardSelectOptionCellBloc, BoardSelectOptionCellState>( child: BlocBuilder<BoardSelectOptionCellBloc, BoardSelectOptionCellState>(
buildWhen: (previous, current) { buildWhen: (previous, current) {
return previous.selectedOptions != current.selectedOptions; return previous.selectedOptions != current.selectedOptions;
}, }, builder: (context, state) {
builder: (context, state) { // Returns SizedBox if the content of the cell is empty
if (state.selectedOptions if (_isEmpty(state)) return const SizedBox();
.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();
return IntrinsicHeight( final children = state.selectedOptions.map(
child: Padding( (option) {
padding: const EdgeInsets.symmetric(vertical: 6), final tag = SelectOptionTag.fromOption(
child: SizedBox.expand( context: context,
child: Wrap(spacing: 4, runSpacing: 2, children: children), 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,
); );
} }

View File

@ -1,6 +1,5 @@
import 'package:app_flowy/plugins/board/application/card/card_bloc.dart'; 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/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:app_flowy/plugins/grid/presentation/widgets/row/row_action_sheet.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
@ -64,10 +63,16 @@ class _BoardCardState extends State<BoardCard> {
value: _cardBloc, value: _cardBloc,
child: BlocBuilder<BoardCardBloc, BoardCardState>( child: BlocBuilder<BoardCardBloc, BoardCardState>(
buildWhen: (previous, current) { 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 || if (previous.cells.length != current.cells.length ||
previous.isEditing != current.isEditing) { previous.isEditing != current.isEditing) {
return true; 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); return !listEquals(previous.cells, current.cells);
}, },
builder: (context, state) { builder: (context, state) {
@ -75,21 +80,16 @@ class _BoardCardState extends State<BoardCard> {
buildAccessoryWhen: () => state.isEditing == false, buildAccessoryWhen: () => state.isEditing == false,
accessoryBuilder: (context) { accessoryBuilder: (context) {
return [ return [
_CardEditOption( _CardEditOption(rowNotifier: rowNotifier),
startEditing: () => rowNotifier.becomeFirstResponder(),
),
const _CardMoreOption(), const _CardMoreOption(),
]; ];
}, },
onTap: (context) { onTap: (context) => widget.openCard(context),
widget.openCard(context); child: _CellColumn(
}, groupId: widget.groupId,
child: Column( rowNotifier: rowNotifier,
mainAxisSize: MainAxisSize.min, cellBuilder: widget.cellBuilder,
children: _makeCells( cells: state.cells,
context,
state.cells.map((cell) => cell.identifier).toList(),
),
), ),
); );
}, },
@ -97,36 +97,62 @@ class _BoardCardState extends State<BoardCard> {
); );
} }
@override
Future<void> dispose() async {
rowNotifier.dispose();
_cardBloc.close();
super.dispose();
}
}
class _CellColumn extends StatelessWidget {
final String groupId;
final BoardCellBuilder cellBuilder;
final EditableRowNotifier rowNotifier;
final List<BoardCellEquatable> 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<Widget> _makeCells( List<Widget> _makeCells(
BuildContext context, BuildContext context,
List<GridCellIdentifier> cells, List<BoardCellEquatable> cells,
) { ) {
final List<Widget> children = []; final List<Widget> children = [];
rowNotifier.clear(); // Remove all the cell listeners.
rowNotifier.unbind();
cells.asMap().forEach( cells.asMap().forEach(
(int index, GridCellIdentifier cellId) { (int index, BoardCellEquatable cell) {
EditableCellNotifier cellNotifier; final isEditing = index == 0 ? rowNotifier.isEditing.value : false;
final cellNotifier = EditableCellNotifier(isEditing: isEditing);
if (index == 0) { if (index == 0) {
// Only use the first cell to receive user's input when click the edit // Only use the first cell to receive user's input when click the edit
// button // button
cellNotifier = EditableCellNotifier( rowNotifier.bindCell(cell.identifier, cellNotifier);
isEditing: rowNotifier.isEditing.value,
);
rowNotifier.insertCell(cellId, cellNotifier);
} else {
cellNotifier = EditableCellNotifier();
} }
Widget child = widget.cellBuilder.buildCell( final child = Padding(
widget.groupId, key: cell.identifier.key(),
cellId,
cellNotifier,
);
child = Padding(
key: cellId.key(),
padding: const EdgeInsets.only(left: 4, right: 4), padding: const EdgeInsets.only(left: 4, right: 4),
child: child, child: cellBuilder.buildCell(
groupId,
cell.identifier,
cellNotifier,
),
); );
children.add(child); children.add(child);
@ -134,13 +160,6 @@ class _BoardCardState extends State<BoardCard> {
); );
return children; return children;
} }
@override
Future<void> dispose() async {
rowNotifier.dispose();
_cardBloc.close();
super.dispose();
}
} }
class _CardMoreOption extends StatelessWidget with CardAccessory { class _CardMoreOption extends StatelessWidget with CardAccessory {
@ -164,9 +183,9 @@ class _CardMoreOption extends StatelessWidget with CardAccessory {
} }
class _CardEditOption extends StatelessWidget with CardAccessory { class _CardEditOption extends StatelessWidget with CardAccessory {
final VoidCallback startEditing; final EditableRowNotifier rowNotifier;
const _CardEditOption({ const _CardEditOption({
required this.startEditing, required this.rowNotifier,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -183,6 +202,6 @@ class _CardEditOption extends StatelessWidget with CardAccessory {
@override @override
void onTap(BuildContext context) { void onTap(BuildContext context) {
startEditing(); rowNotifier.becomeFirstResponder();
} }
} }

View File

@ -72,50 +72,44 @@ class CardAccessoryContainer extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.read<AppTheme>(); final theme = context.read<AppTheme>();
final children = accessories.map((accessory) { 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( return GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () => accessory.onTap(context), onTap: () => accessory.onTap(context),
child: hover, child: _wrapHover(theme, accessory),
); );
}).toList(); }).toList();
return _wrapDecoration(context, Row(children: children));
}
return Container( FlowyHover _wrapHover(AppTheme theme, CardAccessory accessory) {
clipBehavior: Clip.hardEdge, return FlowyHover(
decoration: _makeBoxDecoration(context), style: HoverStyle(
child: Row(children: children), hoverColor: theme.hover,
backgroundColor: theme.surface,
borderRadius: BorderRadius.zero,
),
builder: (_, onHover) => SizedBox(
width: 24,
height: 24,
child: accessory,
),
); );
} }
}
BoxDecoration _makeBoxDecoration(BuildContext context) { Widget _wrapDecoration(BuildContext context, Widget child) {
final theme = context.read<AppTheme>(); final theme = context.read<AppTheme>();
final borderSide = BorderSide(color: theme.shader6, width: 1.0); final borderSide = BorderSide(color: theme.shader6, width: 1.0);
return BoxDecoration( final decoration = BoxDecoration(
color: Colors.transparent, color: Colors.transparent,
border: Border.fromBorderSide(borderSide), border: Border.fromBorderSide(borderSide),
// boxShadow: const [ borderRadius: const BorderRadius.all(Radius.circular(4)),
// BoxShadow( );
// color: Colors.transparent, return Container(
// spreadRadius: 0, clipBehavior: Clip.hardEdge,
// blurRadius: 5, decoration: decoration,
// offset: Offset.zero, child: child,
// ) );
// ], }
borderRadius: const BorderRadius.all(Radius.circular(4)),
);
} }
class _CardEnterRegion extends StatelessWidget { class _CardEnterRegion extends StatelessWidget {

View File

@ -164,67 +164,65 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.watch<AppTheme>(); final theme = context.watch<AppTheme>();
final Widget child; Widget child = _buildOptions(theme, context);
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(),
),
);
}
return Stack( return Stack(
alignment: AlignmentDirectional.center, alignment: AlignmentDirectional.center,
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
AppFlowyStylePopover( _wrapPopover(child),
controller: _popover, InkWell(onTap: () => _popover.show()),
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();
}),
], ],
); );
} }
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);
}
} }

View File

@ -25,17 +25,13 @@ import 'text_field.dart';
const double _editorPanelWidth = 300; const double _editorPanelWidth = 300;
class SelectOptionCellEditor extends StatelessWidget with FlowyOverlayDelegate { class SelectOptionCellEditor extends StatelessWidget {
final GridSelectOptionCellController cellController; final GridSelectOptionCellController cellController;
final VoidCallback? onDismissed;
static double editorPanelWidth = 300; static double editorPanelWidth = 300;
const SelectOptionCellEditor({ const SelectOptionCellEditor({required this.cellController, Key? key})
required this.cellController, : super(key: key);
this.onDismissed,
Key? key,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { 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 { class _OptionList extends StatelessWidget {

View File

@ -3,6 +3,7 @@ import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
import 'package:appflowy_popover/popover.dart'; import 'package:appflowy_popover/popover.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/button.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/hover.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
@ -29,7 +30,8 @@ class GridFieldCell extends StatelessWidget {
}, },
child: BlocBuilder<FieldCellBloc, FieldCellState>( child: BlocBuilder<FieldCellBloc, FieldCellState>(
builder: (context, state) { builder: (context, state) {
final button = Popover( final button = AppFlowyStylePopover(
constraints: BoxConstraints.loose(const Size(240, 840)),
direction: PopoverDirection.bottomWithLeftAligned, direction: PopoverDirection.bottomWithLeftAligned,
triggerActions: PopoverTriggerActionFlags.click, triggerActions: PopoverTriggerActionFlags.click,
offset: const Offset(0, 10), offset: const Offset(0, 10),

View File

@ -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/startup/startup.dart';
import 'package:app_flowy/plugins/grid/application/prelude.dart'; import 'package:app_flowy/plugins/grid/application/prelude.dart';
import 'package:app_flowy/workspace/presentation/widgets/dialogs.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/image.dart';
import 'package:flowy_infra/theme.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/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
@ -32,38 +32,32 @@ class _GridFieldCellActionSheetState extends State<GridFieldCellActionSheet> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_showFieldEditor) { if (_showFieldEditor) {
final field = widget.cellContext.field; final field = widget.cellContext.field;
return OverlayContainer( return FieldEditor(
constraints: BoxConstraints.loose(const Size(240, 200)), gridId: widget.cellContext.gridId,
child: FieldEditor( fieldName: field.name,
typeOptionLoader: FieldTypeOptionLoader(
gridId: widget.cellContext.gridId, gridId: widget.cellContext.gridId,
fieldName: field.name, field: field,
typeOptionLoader: FieldTypeOptionLoader(
gridId: widget.cellContext.gridId,
field: field,
),
), ),
); );
} }
return BlocProvider( return BlocProvider(
create: (context) => create: (context) =>
getIt<FieldActionSheetBloc>(param1: widget.cellContext), getIt<FieldActionSheetBloc>(param1: widget.cellContext),
child: OverlayContainer( child: SingleChildScrollView(
constraints: BoxConstraints.loose(const Size(240, 200)), child: Column(
child: SingleChildScrollView( children: [
child: Column( _EditFieldButton(
children: [ cellContext: widget.cellContext,
_EditFieldButton( onTap: () {
cellContext: widget.cellContext, setState(() {
onTap: () { _showFieldEditor = true;
setState(() { });
_showFieldEditor = true; },
}); ),
}, const VSpace(6),
), _FieldOperationList(widget.cellContext, () {}),
const VSpace(6), ],
_FieldOperationList(widget.cellContext, () {}),
],
),
), ),
), ),
); );
@ -159,8 +153,11 @@ class FieldActionCell extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.watch<AppTheme>(); final theme = context.watch<AppTheme>();
return FlowyButton( return FlowyButton(
text: FlowyText.medium(action.title(), text: FlowyText.medium(
fontSize: 12, color: enable ? null : theme.shader4), action.title(),
fontSize: 12,
color: enable ? null : theme.shader4,
),
hoverColor: theme.hover, hoverColor: theme.hover,
onTap: () { onTap: () {
if (enable) { if (enable) {
@ -168,8 +165,10 @@ class FieldActionCell extends StatelessWidget {
onTap(); onTap();
} }
}, },
leftIcon: svgWidget(action.iconName(), leftIcon: svgWidget(
color: enable ? theme.iconColor : theme.disableIconColor), action.iconName(),
color: enable ? theme.iconColor : theme.disableIconColor,
),
); );
} }
} }
@ -216,6 +215,7 @@ extension _FieldActionExtension on FieldAction {
.add(const FieldActionSheetEvent.duplicateField()); .add(const FieldActionSheetEvent.duplicateField());
break; break;
case FieldAction.delete: case FieldAction.delete:
PopoverContainer.of(context).close();
NavigatorAlertDialog( NavigatorAlertDialog(
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
confirm: () { confirm: () {

View File

@ -7,12 +7,12 @@ import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.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/button.dart';
import 'package:flowy_infra_ui/style_widget/text.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_infra_ui/widget/spacing.dart';
import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/log.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/generated/locale_keys.g.dart';
import 'field_name_input.dart';
import 'field_type_option_editor.dart'; import 'field_type_option_editor.dart';
class FieldEditor extends StatefulWidget { class FieldEditor extends StatefulWidget {
@ -44,6 +44,12 @@ class _FieldEditorState extends State<FieldEditor> {
super.initState(); super.initState();
} }
@override
void dispose() {
popoverMutex.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
@ -58,21 +64,14 @@ class _FieldEditorState extends State<FieldEditor> {
return ListView( return ListView(
shrinkWrap: true, shrinkWrap: true,
children: [ children: [
FlowyText.medium(LocaleKeys.grid_field_editProperty.tr(), FlowyText.medium(
fontSize: 12), LocaleKeys.grid_field_editProperty.tr(),
const VSpace(10), fontSize: 12,
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),
);
},
), ),
const VSpace(10), const VSpace(10),
_FieldNameTextField(popoverMutex: popoverMutex),
const VSpace(10),
..._addDeleteFieldButton(state),
_FieldTypeOptionCell(popoverMutex: popoverMutex), _FieldTypeOptionCell(popoverMutex: popoverMutex),
], ],
); );
@ -80,6 +79,23 @@ class _FieldEditorState extends State<FieldEditor> {
), ),
); );
} }
List<Widget> _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 { class _FieldTypeOptionCell extends StatelessWidget {
@ -111,25 +127,89 @@ class _FieldTypeOptionCell extends StatelessWidget {
} }
} }
class _FieldNameCell extends StatelessWidget { class _FieldNameTextField extends StatefulWidget {
const _FieldNameCell({Key? key}) : super(key: key); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<FieldEditorBloc, FieldEditorState>( final theme = context.watch<AppTheme>();
builder: (context, state) {
return FieldNameTextField( controller.text = context.read<FieldEditorBloc>().state.name;
name: state.name, return BlocListener<FieldEditorBloc, FieldEditorState>(
errorText: context.read<FieldEditorBloc>().state.errorText, listenWhen: (previous, current) => previous.name != current.name,
onNameChanged: (newName) { listener: (context, state) {
context controller.text = state.name;
.read<FieldEditorBloc>()
.add(FieldEditorEvent.updateName(newName));
},
);
}, },
child: BlocBuilder<FieldEditorBloc, FieldEditorState>(
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<FieldEditorBloc>().state.errorText,
onChanged: (newName) {
context
.read<FieldEditorBloc>()
.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 { class _DeleteFieldButton extends StatelessWidget {
@ -171,12 +251,10 @@ class _DeleteFieldButton extends StatelessWidget {
popupBuilder: (popupContext) { popupBuilder: (popupContext) {
return PopoverAlertView( return PopoverAlertView(
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
cancel: () => popoverMutex.state?.close(), cancel: () {},
confirm: () { confirm: () {
onDeleted?.call(); onDeleted?.call();
popoverMutex.state?.close();
}, },
popoverMutex: popoverMutex,
); );
}, },
child: widget, child: widget,

View File

@ -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<FieldNameTextField> createState() => _FieldNameTextFieldState();
}
class _FieldNameTextFieldState extends State<FieldNameTextField> {
late String name;
TextEditingController controller = TextEditingController();
@override
void initState() {
controller.text = widget.name;
super.initState();
}
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
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);
}
}

View File

@ -47,10 +47,6 @@ class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate {
), ),
); );
} }
static String identifier() {
return (FieldTypeList).toString();
}
} }
class FieldTypeCell extends StatelessWidget { class FieldTypeCell extends StatelessWidget {

View File

@ -66,7 +66,8 @@ class FieldTypeOptionEditor extends StatelessWidget {
height: GridSize.typeOptionItemHeight, height: GridSize.typeOptionItemHeight,
child: AppFlowyStylePopover( child: AppFlowyStylePopover(
constraints: BoxConstraints.loose(const Size(460, 440)), constraints: BoxConstraints.loose(const Size(460, 440)),
triggerActions: PopoverTriggerActionFlags.click, triggerActions:
PopoverTriggerActionFlags.click | PopoverTriggerActionFlags.hover,
mutex: popoverMutex, mutex: popoverMutex,
offset: const Offset(20, 0), offset: const Offset(20, 0),
popupBuilder: (context) { popupBuilder: (context) {

View File

@ -50,7 +50,9 @@ Widget? makeTypeOptionWidget({
required PopoverMutex popoverMutex, required PopoverMutex popoverMutex,
}) { }) {
final builder = makeTypeOptionWidgetBuilder( final builder = makeTypeOptionWidgetBuilder(
dataController: dataController, popoverMutex: popoverMutex); dataController: dataController,
popoverMutex: popoverMutex,
);
return builder.build(context); return builder.build(context);
} }

View File

@ -75,7 +75,7 @@ class DateTypeOptionWidget extends TypeOptionWidget {
context context
.read<DateTypeOptionBloc>() .read<DateTypeOptionBloc>()
.add(DateTypeOptionEvent.didSelectDateFormat(format)); .add(DateTypeOptionEvent.didSelectDateFormat(format));
PopoverContainer.of(popoverContext).closeAll(); PopoverContainer.of(popoverContext).close();
}, },
); );
}, },
@ -97,7 +97,7 @@ class DateTypeOptionWidget extends TypeOptionWidget {
context context
.read<DateTypeOptionBloc>() .read<DateTypeOptionBloc>()
.add(DateTypeOptionEvent.didSelectTimeFormat(format)); .add(DateTypeOptionEvent.didSelectTimeFormat(format));
PopoverContainer.of(popoverContext).closeAll(); PopoverContainer.of(popoverContext).close();
}, },
); );
}, },
@ -201,12 +201,10 @@ class DateFormatList extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cells = DateFormat.values.map((format) { final cells = DateFormat.values.map((format) {
return DateFormatCell( return DateFormatCell(
dateFormat: format, dateFormat: format,
onSelected: (format) { onSelected: onSelected,
onSelected(format); isSelected: selectedFormat == format,
FlowyOverlay.of(context).remove(DateFormatList.identifier()); );
},
isSelected: selectedFormat == format);
}).toList(); }).toList();
return SizedBox( return SizedBox(
@ -224,10 +222,6 @@ class DateFormatList extends StatelessWidget {
), ),
); );
} }
static String identifier() {
return (DateFormatList).toString();
}
} }
class DateFormatCell extends StatelessWidget { class DateFormatCell extends StatelessWidget {
@ -291,12 +285,10 @@ class TimeFormatList extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cells = TimeFormat.values.map((format) { final cells = TimeFormat.values.map((format) {
return TimeFormatCell( return TimeFormatCell(
isSelected: format == selectedFormat, isSelected: format == selectedFormat,
timeFormat: format, timeFormat: format,
onSelected: (format) { onSelected: onSelected,
onSelected(format); );
FlowyOverlay.of(context).remove(TimeFormatList.identifier());
});
}).toList(); }).toList();
return SizedBox( return SizedBox(
@ -314,10 +306,6 @@ class TimeFormatList extends StatelessWidget {
), ),
); );
} }
static String identifier() {
return (TimeFormatList).toString();
}
} }
class TimeFormatCell extends StatelessWidget { class TimeFormatCell extends StatelessWidget {

View File

@ -82,7 +82,7 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
context context
.read<NumberTypeOptionBloc>() .read<NumberTypeOptionBloc>()
.add(NumberTypeOptionEvent.didSelectFormat(format)); .add(NumberTypeOptionEvent.didSelectFormat(format));
PopoverContainer.of(popoverContext).closeAll(); PopoverContainer.of(popoverContext).close();
}, },
selectedFormat: state.typeOption.format, selectedFormat: state.typeOption.format,
); );
@ -123,8 +123,6 @@ class NumberFormatList extends StatelessWidget {
format: format, format: format,
onSelected: (format) { onSelected: (format) {
onSelected(format); onSelected(format);
FlowyOverlay.of(context)
.remove(NumberFormatList.identifier());
}); });
}).toList(); }).toList();
@ -147,10 +145,6 @@ class NumberFormatList extends StatelessWidget {
), ),
); );
} }
static String identifier() {
return (NumberFormatList).toString();
}
} }
class NumberFormatCell extends StatelessWidget { class NumberFormatCell extends StatelessWidget {

View File

@ -207,13 +207,13 @@ class _OptionCellState extends State<_OptionCell> {
context context
.read<SelectOptionTypeOptionBloc>() .read<SelectOptionTypeOptionBloc>()
.add(SelectOptionTypeOptionEvent.deleteOption(widget.option)); .add(SelectOptionTypeOptionEvent.deleteOption(widget.option));
PopoverContainer.of(popoverContext).closeAll(); PopoverContainer.of(popoverContext).close();
}, },
onUpdated: (updatedOption) { onUpdated: (updatedOption) {
context context
.read<SelectOptionTypeOptionBloc>() .read<SelectOptionTypeOptionBloc>()
.add(SelectOptionTypeOptionEvent.updateOption(updatedOption)); .add(SelectOptionTypeOptionEvent.updateOption(updatedOption));
PopoverContainer.of(popoverContext).closeAll(); PopoverContainer.of(popoverContext).close();
}, },
key: ValueKey(widget.option.id), key: ValueKey(widget.option.id),
); );

View File

@ -131,8 +131,10 @@ class _GridPropertyCell extends StatelessWidget {
return FieldEditor( return FieldEditor(
gridId: gridId, gridId: gridId,
fieldName: fieldContext.name, fieldName: fieldContext.name,
typeOptionLoader: typeOptionLoader: FieldTypeOptionLoader(
FieldTypeOptionLoader(gridId: gridId, field: fieldContext.field), gridId: gridId,
field: fieldContext.field,
),
); );
}, },
); );

View File

@ -1,4 +1,3 @@
import 'package:appflowy_popover/popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/text_style.dart'; import 'package:flowy_infra/text_style.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
@ -88,13 +87,11 @@ class _CreateTextFieldDialog extends State<NavigatorTextFieldDialog> {
} }
class PopoverAlertView extends StatelessWidget { class PopoverAlertView extends StatelessWidget {
final PopoverMutex popoverMutex;
final String title; final String title;
final void Function()? cancel; final void Function()? cancel;
final void Function()? confirm; final void Function()? confirm;
const PopoverAlertView({ const PopoverAlertView({
required this.popoverMutex,
required this.title, required this.title,
this.confirm, this.confirm,
this.cancel, this.cancel,

View File

@ -6,7 +6,42 @@ import 'package:flutter/services.dart';
/// If multiple popovers are exclusive, /// If multiple popovers are exclusive,
/// pass the same mutex to them. /// pass the same mutex to them.
class PopoverMutex { class PopoverMutex {
PopoverState? state; final ValueNotifier<PopoverState?> _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 { class PopoverController {
@ -109,11 +144,7 @@ class PopoverState extends State<Popover> {
close(); close();
if (widget.mutex != null) { if (widget.mutex != null) {
if (widget.mutex!.state != null && widget.mutex!.state != this) { widget.mutex?.state = this;
widget.mutex!.state!.close();
}
widget.mutex!.state = this;
} }
if (_popoverWithMask == null) { if (_popoverWithMask == null) {
@ -163,7 +194,7 @@ class PopoverState extends State<Popover> {
} }
if (widget.mutex?.state == this) { if (widget.mutex?.state == this) {
widget.mutex!.state = null; widget.mutex?._removeState();
} }
} }